Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file attr.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631open!CoreopenJs_of_ocamlmoduleVdom_raw=Rawlet()=(* use the native-javascript implementation of float -> string with a fixed number of
numbers after the decimal place. *)Css_gen.Private.float_to_string_with_fixed:=Dom_float.to_string_fixed;;(** This has 3 kinds of constructors.
{v
- constructors for properties / attributes for which we
have written first class ocaml representations (so far only Style,
Class, and Handler)
- Those which we immediately convert into Js called Raw, which
in turn has two cases:
- Property for properties on the DOM
- Attribute for attributes on the DOM
- Hooks, which register callbacks on property addition and removal.
v}
Generally speaking one should avoid creating a property or attribute
for something for which we have a first class representation.
*)moduleEvent_handler=structtypet=|T:{type_id:'aType_equal.Id.t;handler:(#Dom_html.eventas'a)Js.t->unitUi_effect.t}->tletcombine(T{type_id=ltid;handler=lhandler})(T{type_id=rtid;handler=rhandler}asright)=(* If they are not the same witness, then it is a bug in virtual_dom, since
we do not expose [on] anymore which means this library can determined the
[Type_equal.Id] corresponding to each event. virtual_dom maintains the
invariant that any two events with the same name will produce handlers
that have the same [Type_equal.Id]. *)matchType_equal.Id.same_witnessltidrtidwith|SomeT->T{type_id=ltid;handler=(funvalue->Effect.sequence_as_sibling(lhandlervalue)~unless_stopped:(fun()->rhandlervalue))}|None->eprint_s[%message"BUG! Type-ids for event handlers differ"(ltid:_Type_equal.Id.t)(rtid:_Type_equal.Id.t)];right;;endtypet=|Propertyof{suppress_merge_warnings:bool;name:string;value:Js.Unsafe.any}|Attributeof{suppress_merge_warnings:bool;name:string;value:Js.Unsafe.any}|Handlerof{name:string;handler:Event_handler.t}|Hookof{name:string;hook:Hooks.t}|StyleofCss_gen.t|Classof(string,String.comparator_witness)Set.t|Manyoftlist|Many_only_merge_classes_and_stylesoftlist*(Css_gen.t->Css_gen.t)*(String.Set.t->String.Set.t)|Many_without_mergeoftlistletcreatenamevalue=Attribute{suppress_merge_warnings=false;name;value=Js.Unsafe.inject(Js.stringvalue)};;letcreate_floatnamevalue=Attribute{suppress_merge_warnings=false;name;value=Js.Unsafe.inject(Dom_float.to_js_stringvalue)};;letpropertynamevalue=Property{suppress_merge_warnings=false;name;value}letstring_propertynamevalue=Property{suppress_merge_warnings=false;name;value=Js.Unsafe.inject(Js.stringvalue)};;letbool_propertynamevalue=Property{suppress_merge_warnings=false;name;value=Js.Unsafe.inject(Js.boolvalue)};;letsuppress_merge_warnings=function|Attributeattribute->Attribute{attributewithsuppress_merge_warnings=true}|Propertyproperty->Property{propertywithsuppress_merge_warnings=true}|t->t;;letcreate_hooknamehook=Hook{name;hook}letmanyattrs=Manyattrsletmany_without_mergeattrs=Many_without_mergeattrsletempty=Many[]letcombineleftright=Many[left;right]let(@)=combineexternalojs_of_any:Js.Unsafe.any->Gen_js_api.Ojs.t="%identity"moduleUnmerged_warning_mode=structtypet=|No_warnings|All_warnings|Stop_after_quotaofintletwarning_count=ref0letcurrent=ref(Stop_after_quota100)letwarn_ss=incrwarning_count;match!currentwith|No_warnings->()|All_warnings->eprint_ss|Stop_after_quotaquota->letwarning_count=!warning_countinifwarning_count<=quotathen(eprint_ss;ifwarning_count=quotatheneprint_s[%message"WARNING: reached warning message quota; no more messages will be printed"(quota:int)]);;moduleFor_testing=structletreset_warning_count()=warning_count:=0endendtypemerge={styles:Css_gen.t;classes:Set.M(String).t;handlers:Event_handler.tMap.M(String).t;hooks:Hooks.tMap.M(String).t}letcombining_map_addmapkeyvalue~combine=Map.updatemapkey~f:(function|Someexisting_value->combine~keyexisting_valuevalue|None->value);;letempty_merge={styles=Css_gen.empty;classes=Set.empty(moduleString);handlers=Map.empty(moduleString);hooks=Map.empty(moduleString)};;letto_rawattr=letattrs=[attr]in(* When input elements have their value set to what it already is
the cursor gets moved to the end of the field even when the user
is editing in the middle. SoftSetHook (from ./soft-set-hook.js)
compares before setting, avoiding the problem just like in
https://github.com/Matt-Esch/virtual-dom/blob/947ecf92b67d25bb693a0f625fa8e90c099887d5/virtual-hyperscript/index.js#L43-L51
note that Elm's virtual-dom includes a workaround for this so
if we switch to that the workaround here will be unnecessary.
https://github.com/elm-lang/virtual-dom/blob/17b30fb7de48672565d6227d33c0176f075786db/src/Native/VirtualDom.js#L434-L439
*)letattrs_obj:Vdom_raw.Attrs.t=Vdom_raw.Attrs.create()in(* [take_second_*] is the trivial merge function (i.e. no merge at all); it
takes two attributes of the same kind, ignores a first, and emits
a warning if [warn_about_unmerged_attributes] is enabled. *)lettake_second_stylesfirstsecond=ifnot(Css_gen.is_emptyfirst)thenUnmerged_warning_mode.warn_s[%message"WARNING: not combining styles"(first:Css_gen.t)(second:Css_gen.t)];secondinlettake_second_classesfirstsecond=ifnot(Set.is_emptyfirst)thenUnmerged_warning_mode.warn_s[%message"WARNING: not combining classes"(first:String.Set.t)(second:String.Set.t)];secondinlettake_second_handler~key:name_firstsecond=Unmerged_warning_mode.warn_s[%message"WARNING: not combining handlers"(name:string)];secondinlettake_second_hook~key:name_firstsecond=Unmerged_warning_mode.warn_s[%message"WARNING: not combining hooks"(name:string)];secondin(* We merge attributes when they are written to the raw attribute object,
rather than when the user-facing merge functions ([many], [combine], and
[@]) are called. This strategy is better in both speed and memory usage,
since it means we do not need to concatenate the list of "unmergeable"
attributes (Property and Attribute); instead, we can iterate through the
tree of attributes and eagerly write unmergeable attributes to the
attribute object as we find them. If two unmergeable attributes have the
same name, the second will simply overwrite the first, as desired.
In order to preserve the existing behavior of the [Multi] module (that is,
it must be possible to merge classes and styles, but not hooks and
handlers), we introduce the workaround constructor
[Many_only_merge_classes_and_styles].
There are thus three cases that each have different merge behaviors:
- Simple lists - no merging
- Lists wrapped in a [Many] - merges classes, styles, hooks, and handlers
- Lists wrapped in a [Many_only_merge_classes_and_styles] - merges classes and styles
To avoid duplicating the match expression logic, we paremeterize it by the
merging behavior, since "no merge" really means "merge by taking the
second one". *)letrecmerge~combine_hook~combine_handler~combine_styles~combine_classesacc=List.fold~init:acc~f:(funaccattr->matchattrwith|Property{suppress_merge_warnings;name;value}->ifRaw.Attrs.has_propertyattrs_objname&¬suppress_merge_warningsthenUnmerged_warning_mode.warn_s[%message"WARNING: not combining properties"(name:string)];(matchnamewith|"value"->letsoftSetHookx:Gen_js_api.Ojs.t=Js.Unsafe.global##SoftSetHookxinletvalue=softSetHookvalueinVdom_raw.Attrs.set_propertyattrs_obj"value"value|name->Raw.Attrs.set_propertyattrs_objname(ojs_of_anyvalue));acc|Attribute{suppress_merge_warnings;name;value}->ifRaw.Attrs.has_attributeattrs_objname&¬suppress_merge_warningsthenUnmerged_warning_mode.warn_s[%message"WARNING: not combining attributes"(name:string)];Raw.Attrs.set_attributeattrs_objname(ojs_of_anyvalue);acc|Stylenew_styles->{accwithstyles=combine_stylesacc.stylesnew_styles}|Classnew_classes->{accwithclasses=combine_classesacc.classesnew_classes}|Hook{name;hook}->{accwithhooks=combining_map_addacc.hooksnamehook~combine:combine_hook}|Handler{name;handler}->{accwithhandlers=combining_map_addacc.handlersnamehandler~combine:combine_handler}|Manyattrs->letsub_merge=merge~combine_hook:(fun~key:_->Hooks.combine)~combine_handler:(fun~key:_->Event_handler.combine)~combine_styles:Css_gen.combine~combine_classes:Set.unionempty_mergeattrsin{styles=combine_stylesacc.stylessub_merge.styles;classes=combine_classesacc.classessub_merge.classes;handlers=Map.merge_skewedacc.handlerssub_merge.handlers~combine:combine_handler;hooks=Map.merge_skewedacc.hookssub_merge.hooks~combine:combine_hook}|Many_only_merge_classes_and_styles(attrs,map_styles,map_classes)->letsub_merge=merge~combine_hook:take_second_hook~combine_handler:take_second_handler~combine_styles:Css_gen.combine~combine_classes:Set.unionempty_mergeattrsin{styles=map_styles(combine_stylesacc.stylessub_merge.styles);classes=map_classes(combine_classesacc.classessub_merge.classes);handlers=Map.merge_skewedacc.handlerssub_merge.handlers~combine:combine_handler;hooks=Map.merge_skewedacc.hookssub_merge.hooks~combine:combine_hook}|Many_without_mergeattrs->letsub_merge=merge~combine_hook:take_second_hook~combine_handler:take_second_handler~combine_styles:take_second_styles~combine_classes:take_second_classesempty_mergeattrsin{styles=combine_stylesacc.stylessub_merge.styles;classes=combine_classesacc.classessub_merge.classes;handlers=Map.merge_skewedacc.handlerssub_merge.handlers~combine:combine_handler;hooks=Map.merge_skewedacc.hookssub_merge.hooks~combine:combine_hook})inletmerge=merge~combine_hook:take_second_hook~combine_handler:take_second_handler~combine_styles:take_second_styles~combine_classes:take_second_classesempty_mergeattrsinMap.iterimerge.hooks~f:(fun~key:name~data:hook->Raw.Attrs.set_propertyattrs_objname(ojs_of_any(Hooks.packhook)));Map.iterimerge.handlers~f:(fun~key:name~data:(Event_handler.T{handler;_})->letfe=Effect.Expert.handlee(handlere);Js._trueinRaw.Attrs.set_propertyattrs_obj("on"^name)(ojs_of_any(Js.Unsafe.inject(Dom.handlerf))));let()=ifnot(Css_gen.is_emptymerge.styles)then(letprops=Css_gen.to_string_listmerge.stylesinletobj=Gen_js_api.Ojs.empty_obj()inList.iterprops~f:(fun(k,v)->Gen_js_api.Ojs.set_prop_asciiobjk(Gen_js_api.Ojs.string_to_jsv));Raw.Attrs.set_propertyattrs_obj"style"obj)inlet()=ifSet.is_emptymerge.classesthen()elseRaw.Attrs.set_attributeattrs_obj"class"(Gen_js_api.Ojs.string_to_js(String.concat(Set.to_listmerge.classes)~sep:" "))inattrs_obj;;letto_rawattr=matchattrwith|Many[]|Many_without_merge[]->Raw.Attrs.create()|attr->to_rawattr;;letstylecss=Stylecssletvalid_class_names=letinvalid=String.is_emptys||String.existss~f:Char.is_whitespaceinnotinvalid;;let%test"valid"=valid_class_name"foo-bar"let%test"invalid-empty"=not(valid_class_name"")let%test"invalid-space"=not(valid_class_name"foo bar")letclass_classname=ifnot(valid_class_nameclassname)thenraise_s[%message"invalid classname"(classname:string)];Class(Set.singleton(moduleString)classname);;letclasses'classes=Classclassesletclassesclassnames=ifnot(List.for_all~f:valid_class_nameclassnames)thenraise_s[%message"invalid classnames"(classnames:stringlist)];classes'(Set.of_list(moduleString)classnames);;letids=create"id"sletnames=create"name"slethrefr=create"href"rlettargets=create"target"sletchecked=create"checked"""letselected=create"selected"""lethidden=create"hidden"""letreadonly=create"readonly"""letdisabled=create"disabled"""letplaceholderx=create"placeholder"xletautofocus=function|true->create"autofocus"""|false->empty;;letfor_x=create"for"xlettype_x=create"type"xletvaluex=create"value"xletvalue_propx=string_property"value"xlettabindexx=create"tabindex"(Int.to_stringx)lettitlex=create"title"xletsrcx=create"src"xletminx=create_float"min"xletmaxx=create_float"max"xletcolspanx=create"colspan"(Int.to_stringx)letrowspanx=create"rowspan"(Int.to_stringx)letdraggableb=create"draggable"(Bool.to_stringb)moduleType_id=struct(* We provide a trivial [to_sexp] function since we only want
to unify type ids and not convert types to ids *)letcreatename=Type_equal.Id.create~name(fun_->Sexplib.Sexp.List[])let(event:Dom_html.eventType_equal.Id.t)=create"event"let(focus:Dom_html.focusEventType_equal.Id.t)=create"focusEvent"let(mouse:Dom_html.mouseEventType_equal.Id.t)=create"mouseEvent"let(keyboard:Dom_html.keyboardEventType_equal.Id.t)=create"keyboardEvent"let(submit:Dom_html.submitEventType_equal.Id.t)=create"submitEvent"let(mousewheel:Dom_html.mousewheelEventType_equal.Id.t)=create"mousewheelEvent"let(clipboard:Dom_html.clipboardEventType_equal.Id.t)=create"clipboardEvent"let(drag:Dom_html.dragEventType_equal.Id.t)=create"dragEvent"let(pointer:Dom_html.pointerEventType_equal.Id.t)=create"pointerEvent"let(animation:Dom_html.animationEventType_equal.Id.t)=create"animationEvent"endletontype_idname(handler:#Dom_html.eventJs.t->unitUi_effect.t):t=Handler{name;handler=T{handler;type_id}};;leton_focus=onType_id.focus"focus"leton_blur=onType_id.focus"blur"leton_click=onType_id.mouse"click"leton_contextmenu=onType_id.mouse"contextmenu"leton_double_click=onType_id.mouse"dblclick"leton_drag=onType_id.drag"drag"leton_dragstart=onType_id.drag"dragstart"leton_dragend=onType_id.drag"dragend"leton_dragenter=onType_id.drag"dragenter"leton_dragleave=onType_id.drag"dragleave"leton_dragover=onType_id.drag"dragover"leton_drop=onType_id.drag"drop"leton_mousemove=onType_id.mouse"mousemove"leton_mouseup=onType_id.mouse"mouseup"leton_mousedown=onType_id.mouse"mousedown"leton_mouseenter=onType_id.mouse"mouseenter"leton_mouseleave=onType_id.mouse"mouseleave"leton_mouseover=onType_id.mouse"mouseover"leton_mouseout=onType_id.mouse"mouseout"leton_keyup=onType_id.keyboard"keyup"leton_keypress=onType_id.keyboard"keypress"leton_keydown=onType_id.keyboard"keydown"leton_scroll=onType_id.event"scroll"leton_submit=onType_id.submit"submit"leton_pointerdown=onType_id.pointer"pointerdown"leton_pointerup=onType_id.pointer"pointerup"leton_mousewheel=onType_id.mousewheel"mousewheel"leton_copy=onType_id.clipboard"copy"leton_cut=onType_id.clipboard"cut"leton_paste=onType_id.clipboard"paste"leton_reset=onType_id.event"reset"leton_animationend=onType_id.animation"animationend"letconst_ignore_=Effect.Ignoreclasstypevalue_element=objectinheritDom_html.elementmethodvalue:Js.js_stringJs.tJs.propendtypevalue_coercion=Dom_html.elementJs.t->value_elementJs.tJs.optletrun_coercioncoerciontargetprev=matchprevwith|Some_->prev|None->Js.Opt.to_option(coerciontarget);;letcoerce_value_elementtarget=letopenDom_html.CoerceToinNone|>run_coercion(input:>value_coercion)target|>run_coercion(select:>value_coercion)target|>run_coercion(textarea:>value_coercion)target;;leton_input_eventtype_ideventhandler=ontype_idevent(funev->Js.Opt.caseev##.targetconst_ignore(funtarget->Option.value_map(coerce_value_elementtarget)~default:Effect.Ignore~f:(funtarget->lettext=Js.to_stringtarget##.valueinhandlerevtext)));;leton_change=on_input_eventType_id.event"change"leton_input=on_input_eventType_id.event"input"letto_rawl=to_rawlleton_file_inputhandler=onType_id.event"input"(funev->Js.Opt.caseev##.targetconst_ignore(funtarget->Js.Opt.case(Dom_html.CoerceTo.inputtarget)const_ignore(funtarget->Js.Optdef.casetarget##.filesconst_ignore(funfiles->handlerevfiles))));;moduleAlways_focus_hook=structmoduleT=structmoduleState=UnitmoduleInput=structincludeUnitletcombine()()=()endletinit()_=()leton_mount()()element=element##focusletupdate~old_input:()~new_input:()()_=()letdestroy()()_=()endmoduleHook=Hooks.Make(T)letattr`Read_the_docs__this_hook_is_unpredictable=(* Append the id to the name of the hook to ensure that it is distinct
from all other focus hooks. *)create_hook"always-focus-hook"(Hook.create());;endmoduleSingle_focus_hook()=structmoduleT=structmoduleState=Unitlethas_been_used=reffalsemoduleInput=structtypet=(unitUi_effect.t[@sexp.opaque])[@@derivingsexp_of]letcombineleftright=Ui_effect.Many[left;right]endletinit__=()leton_mountevent()element=ifnot!has_been_usedthen(has_been_used:=true;element##focus;Effect.Expert.handle_non_dom_event_exnevent);;letupdate~old_input:_~new_input:_()_=()letdestroy_()_=()endmoduleHook=Hooks.Make(T)letattr`Read_the_docs__this_hook_is_unpredictable~after=(* Append the id to the name of the hook to ensure that it is distinct
from all other focus hooks. *)create_hook"single-focus-hook"(Hook.createafter);;endmoduleMulti=structtypeattr=ttypet=attrlistletmap_stylet~f=[Many_only_merge_classes_and_styles(t,f,Fn.id)]letadd_classtc=[Many_only_merge_classes_and_styles(t,Fn.id,funcs->Set.addcsc)];;letadd_stylets=map_stylet~f:(funss->Css_gen.combinesss)letmerge_classes_and_stylest=[Many_only_merge_classes_and_styles(t,Fn.id,Fn.id)];;endmoduleExpert=structletrecfilter_by_kindt~f=matchtwith|Property_->iff`Propertythentelseempty|Attribute_->iff`Attributethentelseempty|Hook_->iff`Hookthentelseempty|Handler_->iff`Handlerthentelseempty|Style_->iff`Stylethentelseempty|Class_->iff`Classthentelseempty|Manyattrs->Many(List.mapattrs~f:(filter_by_kind~f))|Many_only_merge_classes_and_styles(attrs,a,b)->Many_only_merge_classes_and_styles(List.mapattrs~f:(filter_by_kind~f),a,b)|Many_without_mergeattrs->Many_without_merge(List.mapattrs~f:(filter_by_kind~f));;letreccontains_namelooking_for=function|Property{name;_}|Attribute{name;_}|Hook{name;_}->String.equallooking_forname|Handler{name;_}->String.equal("on"^name)looking_for|Style_->String.equallooking_for"style"|Class_->String.equallooking_for"class"|Manyattrs|Many_only_merge_classes_and_styles(attrs,_,_)|Many_without_mergeattrs->List.exists~f:(contains_namelooking_for)attrs;;end