Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file bonsai_web_ui_drag_and_drop.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364open!CoreopenVirtual_domopenBonsaiopenBonsai.Let_syntaxopenJs_of_ocamlopenVdommodulePosition=structtypet={x:int;y:int}[@@derivingsexp,equal]endmoduleSize=structtypet={width:int;height:int}[@@derivingsexp,equal]endmoduleState_machine_model=structtype('source_id,'target_id)dragging={source:'source_id;target:'target_idoption(* If [has_moved] is false, then the mouse has been clicked, but we do
not yet consider the dragging to have started, so for all visual
purposes we are [Not_dragging]. *);has_moved:bool;offset:Position.t;position:Position.t;size:Size.t}[@@derivingsexp,equal]type('source_id,'target_id)t=|Not_dragging|Draggingof('source_id,'target_id)dragging[@@derivingsexp,equal]endmoduleAction=structtype('source_id,'target_id)t=|Started_dragof{source:'source_id;offset:Position.t;position:Position.t;size:Size.t}|Set_targetof'target_idoption|Finished_drag|Mouse_movedofPosition.t[@@derivingsexp,equal]endtype('source_id,'target_id)t={source:id:'source_id->Attr.t;drop_target:id:'target_id->Attr.t;sentinel:name:string->Attr.t;model:('source_id,'target_id)State_machine_model.t;inject:('source_id,'target_id)Action.tlist->unitEffect.t}[@@derivingfields]letbugmessage=letmessage=sprintf"BUG: %s. Report to Bonsai developers"messageineprint_s[%messagemessage];;moduleFor_testing=structmoduleAction=structtypet=|Start_dragofstring|Set_targetofstringoption|Finish_drag[@@derivingsexp,equal]letto_internal_actions(typesourcetarget)(moduleSource:Modelwithtypet=source)(moduleTarget:Modelwithtypet=target)=function|Start_dragsource->[Action.Started_drag{source=Sexp.of_stringsource|>Source.t_of_sexp;offset={x=0;y=0};position={x=0;y=0};size={width=0;height=0}};Action.Mouse_moved{x=0;y=0}]|Set_target(Sometarget)->[Set_target(Some(Sexp.of_stringtarget|>Target.t_of_sexp))]|Set_targetNone->[Set_targetNone]|Finish_drag->[Finished_drag];;endmoduleInject_hook=Attr.Hooks.Make(structmoduleState=UnitmoduleInput=structtypet=Action.t->unitUi_effect.t[@@derivingsexp]letcombine_second=secondendletinit__=()leton_mount_()_=()letupdate~old_input:_~new_input:_()_=()letdestroy_()_=()end)lettype_id=Inject_hook.For_testing.type_idendletadd_event_listener,remove_event_listener=letactive:Dom_html.event_listener_idBonsai.Private.Path.Map.tref=refBonsai.Private.Path.Map.emptyinletinstall=Bonsai.Effect.of_sync_fun(fun(typ,path,handler)->matchBonsai_web.am_running_howwith|`Node_test->print_endline"adding window event listener"|`Browser|`Browser_benchmark->letlistener=Dom_html.addEventListenerDom_html.windowtyp(Dom.handlerhandler)Js._trueinactive:=Map.update!activepath~f:(function_->listener);()|`Node|`Node_benchmark->())inletuninstall=Bonsai.Effect.of_sync_fun(funpath->matchBonsai_web.am_running_howwith|`Node_test->print_endline"removing window event listener"|`Browser|`Browser_benchmark->Map.find!activepath|>Option.iter~f:Dom_html.removeEventListener|`Node|`Node_benchmark->())in(funtyppathhandler->install(typ,path,handler)),uninstall;;letcreate(typesourcetarget)here~source_id:(moduleSource:Modelwithtypet=source)~target_id:(moduleTarget:Modelwithtypet=target)~on_drop=let%submodel,inject=Bonsai.state_machine1here(modulestructtypet=(Source.t,Target.t)State_machine_model.t[@@derivingsexp,equal]end)(modulestructtypet=(Source.t,Target.t)Action.tlist[@@derivingsexp,equal]end)on_drop~default_model:Not_dragging~apply_action:(fun~inject:_~schedule_eventon_dropmodelactions->List.foldactions~init:model~f:(funmodelaction->matchactionwith|Action.Started_drag{source;offset;position;size}->(matchmodelwith|State_machine_model.Not_dragging->()|Dragging_->bug"Started dragging before dragging finished");Dragging{source;offset;position;size;target=None;has_moved=false}|Set_targettarget->(matchmodelwith|State_machine_model.Not_dragging->Not_dragging|Draggingt->Dragging{twithtarget})|Finished_drag->(matchmodelwith|State_machine_model.Not_dragging|Dragging{target=None;_}->Not_dragging|Dragging{source;target=Sometarget;_}->schedule_event(on_dropsourcetarget);Not_dragging)|Mouse_movedposition->(matchmodelwith|State_machine_model.Not_dragging->Not_dragging|Draggingt->Dragging{twithposition;has_moved=true})))inlet%subsource=let%arrinject=injectinfun~id->Attr.many[Attr.on_pointerdown(funevent->letposition={Position.x=event##.clientX;y=event##.clientY}inletbounding_rect=(Js.Opt.to_optionevent##.currentTarget|>Option.value_exn)##getBoundingClientRectinletoptdef_floatx=x|>Js.Optdef.to_option|>Option.value_exn|>Int.of_floatinletwidth=optdef_floatbounding_rect##.widthinletheight=optdef_floatbounding_rect##.heightinlettop=Int.of_floatbounding_rect##.topinletleft=Int.of_floatbounding_rect##.leftinletsize={Size.width;height}inletoffset={Position.x=position.x-left;y=position.y-top}inmatchBonsai_web.am_within_disabled_fieldseteventwith|true->Effect.Ignore|false->inject[Started_drag{source=id;offset;position;size}]);Attr.style(Css_gen.user_select`None)]inlet%subpath=Bonsai.Private.pathinlet%subpath_for_pointermove=Bonsai.Private.pathinlet%subpath_for_pointerup=Bonsai.Private.pathinlet%subuniverse_suffix=let%arrpath=pathinBonsai.Private.Path.to_unique_identifier_stringpathinlet%subon_deactivate=let%arrpath_for_pointermove=path_for_pointermoveandpath_for_pointerup=path_for_pointerupinEffect.all_unit[remove_event_listenerpath_for_pointermove;remove_event_listenerpath_for_pointerup]inlet%subon_activate=let%arrinject=injectandpath_for_pointermove=path_for_pointermoveandpath_for_pointerup=path_for_pointerupanduniverse_suffix=universe_suffixinlet%bind.Effect()=add_event_listenerDom_html.Event.pointermovepath_for_pointermove(fun(event:Dom_html.pointerEventJs.t)->let(event:<composedPath:'aJs.js_arrayJs.tJs.meth;Dom_html.pointerEvent>Js.t)=Js.Unsafe.coerceeventin(* Why client coordinates and not page or screen coordinates. I've
tested with all three and client coordinates is clearly the
correct choice.
- page: If you scroll while dragging, the dragged element moves
away from your mouse because the diff between start and end
positions gets larger even though the mouse is stationary on
the screen.
- screen: If you move the mouse while dragging (which can
happen if you use window management keyboard shortcuts), the
dragged element stays in the same position relative to the
browser window, since the mouse didn't move, but this is not
good because the mouse window has moved away from the mouse.
- client: Scrolling or moving the window does not pull the
dragged element away from the mouse.
It makes sense that client coordinates is correct because the
dragged element itself uses fixed positioning, which is roughly
equivalent to client coordinates. *)letposition={Position.x=event##.clientX;y=event##.clientY}inletpath=Js.to_arrayevent##composedPath|>Array.to_listinlettarget=List.find_mappath~f:(funelement->let%bind.Optiondataset=Js.Opt.to_optionelement##.datasetinlet%map.Optiondrag_target=Js.Opt.to_option(Js.Unsafe.getdataset("dragTarget"^universe_suffix))inletdrag_target=Js.to_stringdrag_targetinTarget.t_of_sexp(Sexp.of_stringdrag_target))inEffect.Expert.handle_non_dom_event_exn(matchBonsai_web.am_within_disabled_fieldseteventwith|true->inject[Set_targetNone;Mouse_movedposition]|false->inject[Set_targettarget;Mouse_movedposition]);Js._true)inadd_event_listenerDom_html.Event.pointeruppath_for_pointerup(funevent->Effect.Expert.handle_non_dom_event_exn(matchBonsai_web.am_within_disabled_fieldseteventwith|true->inject[Set_targetNone;Finished_drag]|false->inject[Finished_drag]);Js._true)inlet%sub()=Bonsai.Edge.lifecycle~on_deactivate~on_activate()inlet%subsentinel=let%arrinject=injectinfun~name->Attr.many[Attr.create_hook"dnd-test-hook"(For_testing.Inject_hook.create(funaction->inject(action|>For_testing.Action.to_internal_actions(moduleSource)(moduleTarget))));Attr.create"data-dnd-name"name]inlet%subdrop_target=let%arrinject=injectanduniverse_suffix=universe_suffixinfun~id->Attr.many[Attr.on_pointerup(funevent->matchBonsai_web.am_within_disabled_fieldseteventwith|true->inject[Set_targetNone;Finished_drag]|false->inject[Finished_drag]);Attr.create("data-drag-target"^universe_suffix)(Sexp.to_string_mach(Target.sexp_of_tid))]inlet%arrmodel=modelandinject=injectandsource=sourceandsentinel=sentinelanddrop_target=drop_targetin{model;inject;source;drop_target;sentinel};;letdragged_elementt~f=match%subt>>|modelwith|Not_dragging|Dragging{has_moved=false;_}->Bonsai.constNode.None|Dragging({source;_}asdragging)->let%subitem=fsourceinlet%arr{position;offset;size;_}=dragginganditem=iteminletx=position.x-offset.xinlety=position.y-offset.yinNode.div~attr:(Attr.styleCss_gen.(position`Fixed~top:(`Px0)~left:(`Px0)@>create~field:"pointer-events"~value:"none"@>width(`Pxsize.width)@>height(`Pxsize.height)@>create~field:"transform"~value:[%string"translateY(%{y#Int}px) translateX(%{x#Int}px)"]))[item];;(* A cut-down version of [State_machine_model] for users of the library *)moduleModel=structtype('source_id,'target_id)t=|Not_dragging|Draggingof{source:'source_id;target:'target_idoption}[@@derivingsexp,equal]endletmodelt=matcht.modelwith|Not_dragging|Dragging{has_moved=false;_}->Model.Not_dragging|Dragging{source;target;_}->Dragging{source;target};;