Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file vdom_input_widgets.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023openCoreopenVirtual_dom.VdomincludeVdom_input_widgets_intfmoduleDecimal=structtypet=floatletinvariantt=ifnot(Float.is_finitet)thenfailwithf"Cannot represent non-finite float as decimal: %f"t();;letof_strings=lett=Float.of_stringsininvariantt;t;;letto_stringt=invariantt;sprintf"%.12g"t;;endmoduleValidated=structtype'at=|Initial(* This is used to avoid marking as invalid a field that hasn't ever been
touched by the user, to improve UX. *)|Validof{input:stringoption;value:'a}|Invalidof{input:string;last_valid:'aoption;error:string}[@@derivingequal,sexp,bin_io,compare]type'aupdate='at[@@derivingequal,sexp,bin_io,compare]letlift(typea)(moduleM:Stringable.Swithtypet=a)=(modulestructtypenonrect=atletto_string=function|Initial->""|Invalid{input;last_valid=_;error=_}->input|Valid{input;value}->(matchinputwith|Someinput->input|None->M.to_stringvalue);;letof_strings=tryValid{input=Somes;value=M.of_strings}with|exn->Invalid{input=s;last_valid=None;error=Exn.to_stringexn};;end:Stringable.Swithtypet=at);;letinitial_empty=Initialletreturnvalue=Valid{input=None;value}letget_current=function|Valid{input=_;value}->Somevalue|Invalid_|Initial->None;;letget_last=function|Valid{input=_;value}->Somevalue|Invalid{input=_;last_valid;error=_}->last_valid|Initial->None;;letget_error=function|Initial|Valid_->None|Invalid{input=_;last_valid=_;error}->Someerror;;letis_invalid=function|Invalid_->true|Valid_|Initial->false;;letis_initial_empty=function|Initial->true|_->false;;letupdateoldnew_=matchold,new_with|Initial,_->new_|_,Valid_->new_|Valid{input=_;value=old},Invalid{input;last_valid=_;error}->Invalid{input;last_valid=Someold;error}|(Invalid{input=_;last_valid;error=_},Invalid{input;last_valid=None;error})->Invalid{input;last_valid;error}|Invalid_,Invalid{input=_;last_valid=Some_;error=_}->new_|_,Initial->old;;endletmaybe_invalidvalidatedattrs=ifValidated.is_invalidvalidatedthenAttr.create"aria-invalid""true"::attrselseattrs;;moduleTime_compat=structmoduleOfday=structtypet=Time_ns.Ofday.tletof_string=Time_ns.Ofday.of_string(* The browser expects a HH:mm format with optional trailing ":ss" or ":ss.SSS";
[Time_ns.Ofday.to_string] provides precision in nanoseconds, which is too much. *)letto_string=Time_ns.Ofday.to_millisecond_stringendletzonedzone:(moduleStringable.Swithtypet=Time_ns.t)=(modulestructtypet=Time_ns.t(* Format from the browser: yyyy-MM-ddThh:mm *)letof_strings=letparts=String.split_on_charss~on:['T';':']inletdate=List.nth_exnparts0|>Date.of_stringinlethr=List.nth_exnparts1|>Int.of_stringinletmin=List.nth_exnparts2|>Int.of_stringinletofday=Time_ns.Ofday.create~hr~min()inTime_ns.of_date_ofday~zonedateofday;;letto_stringt=lets=Time_ns.to_string_iso8601_basic~zonetin(* The browser expect a yyyy-MM-ddThh:mm format and it allows
trailing ":ss" or ":ss.SSS".
to_string_iso8601_basic format: 2019-01-30T01:00:00.000000000+01:00
desired format after cutting: 2019-01-30T01:00:00
*)String.lsplit2_exn~on:'.'s|>Tuple2.get1;;end);;endletmaybe_disabled~disabledattrs=ifdisabledthenAttr.disabled::attrselseattrsletadd_attrsattrs'attrs=attrs@attrs'|>Attrs.merge_classes_and_stylesletstructural_list?(orientation=`Vertical)attrschildren=letlayout_style=matchorientationwith|`Vertical->Css_gen.(display`Block)|`Horizontal->Css_gen.(display`Inline_block)inNode.ul~attr:(Attr.many_without_merge([Attr.styleCss_gen.(create~field:"list-style"~value:"none"@>margin_left(`Px0))]|>add_attrsattrs))(List.mapchildren~f:(funchild->Node.li~attr:(Attr.stylelayout_style)[child]));;moduleValue_normalizing_hook=structmoduleUnsafe=Js_of_ocaml.Js.UnsafeopenJs_of_ocamlopenJs_of_ocaml.Dom_htmlletis_activeelement=letdocument_active_element=Unsafe.getdocument(Js.string"activeElement")inphys_equalelementdocument_active_element;;letvalue_property=Js.string"value"letget_valueelement:'aJs.t=Unsafe.getelementvalue_propertyletset_valueelementvalue=Unsafe.setelementvalue_propertyvalueletinstall_event_handlerelement~f=(* This event handler normalizes the value on the input element on the [change] event.
For a text entry, this means when the user presses enter, and when the user blurs
the element. Why don't we simply [to_string] the value in the model? Because for
some input elements, you can have a change event that fires after the value changes
but before [Incr_dom] can update the model. For example, this happens when you
press the up arrow on a number input. This leads to a bug where the value in the
model swaps back and forth with the value in the element. *)letchange_handler_=letvalue=Js.to_string(get_valueelement)inOption.iter(fvalue)~f:(funnormalized->set_valueelement(Js.stringnormalized));Js._trueinletchange_handler=Dom.handlerchange_handlerinaddEventListenerelementEvent.changechange_handlerJs._false;;moduleM=structmoduleState=structtypet={mutableevent_id:event_listener_id}endmoduleInput=structtypet={value:string;f:string->stringoption}letsexp_of_t{value;_}=Sexp.Atomvalueletcombine_leftright=rightendletinit{Input.value;f}element=ifnot(is_activeelement)thenset_valueelement(Js.stringvalue);letevent_id=install_event_handlerelement~fin{State.event_id};;leton_mount_input_state_element=()letdestroy_input{State.event_id}_element=removeEventListenerevent_idletupdate~old_input~new_inputstateelement=destroyold_inputstateelement;let{State.event_id}=initnew_inputelementinstate.State.event_id<-event_id;;endincludeAttr.Hooks.Make(M)(* [create value ~f] will set the "value" property to [value] if the element is not
focused and on each change, run the current value through [f] to re-set it. Again,
this only happens if the element is not focused. If [f] returns [None], no change
takes place. *)letcreatevalue~f=Attr.create_hook"value:normalized"(create{value;f})endmoduleDropdown=structletimpl?(extra_attrs=[])?(disabled=false)values~equal~selected~to_string~on_change=Node.select~attr:(Attr.many_without_merge([Attr.class_"widget-dropdown";Attr.on_change(fun_value->on_change(Int.of_stringvalue|>List.nth_exnvalues))]|>maybe_disabled~disabled|>add_attrsextra_attrs))(List.mapivalues~f:(funindexvalue->Node.option~attr:(Attr.many_without_merge[Attr.value(Int.to_stringindex);Attr.bool_property"selected"(equalvalueselected)])[Node.text(to_stringvalue)]));;letof_values(typet)?extra_attrs?disabled(moduleM:Equalwithtypet=t)values~selected~on_change=impl?extra_attrs?disabledvalues~equal:M.equal~selected~to_string:M.to_string~on_change;;letof_values_opt(typet)?extra_attrs?disabled(moduleM:Equalwithtypet=t)values~selected~on_change=letvalues=None::List.mapvalues~f:Option.someinletto_string=Option.value_map~default:""~f:M.to_stringinimpl?extra_attrs?disabledvalues~equal:[%equal:M.toption]~selected~to_string~on_change;;letof_enum(typet)?extra_attrs?disabled(moduleM:Enumwithtypet=t)~selected~on_change=impl?extra_attrs?disabledM.all~equal:M.equal~selected~to_string:M.to_string~on_change;;letof_enum_opt(typet)?extra_attrs?disabled(moduleM:Enumwithtypet=t)~selected~on_change=letvalues=None::List.mapM.all~f:Option.someinletto_string=Option.value_map~default:""~f:M.to_stringinimpl?extra_attrs?disabledvalues~equal:[%equal:M.toption]~selected~to_string~on_change;;endmoduleCheckbox=structletimpl?(extra_attrs=[])?(disabled=false)~is_checked~label~on_toggle()=Node.label~attr:(Attr.many_without_mergeextra_attrs)[Node.input~attr:(Attr.many_without_merge([Attr.type_"checkbox";Attr.on_click(fun_ev->on_toggle());Attr.bool_property"checked"is_checked]|>maybe_disabled~disabled))[];Node.textlabel];;letsimple?extra_attrs?disabled~is_checked~label~on_toggle()=Node.div~attr:(Attr.class_"checkbox-container")[impl?extra_attrs?disabled~is_checked~label~on_toggle()];;endmoduleChecklist=structletimpl?(extra_attrs=[])?(disabled=false)values~is_checked~on_toggle~to_string=structural_list([Attr.classes["widget-checklist";"checkbox-container"]]|>add_attrsextra_attrs)(List.mapvalues~f:(funitem->Checkbox.impl~extra_attrs~disabled~is_checked:(is_checkeditem)~label:(to_stringitem)~on_toggle:(fun()->on_toggleitem)()));;letof_values(typet)?extra_attrs?disabled(moduleM:Displaywithtypet=t)values~is_checked~on_toggle=impl?extra_attrs?disabledvalues~is_checked~on_toggle~to_string:M.to_string;;letof_enum(typet)?extra_attrs?disabled(moduleM:Enumwithtypet=t)~is_checked~on_toggle=impl?extra_attrs?disabledM.all~is_checked~on_toggle~to_string:M.to_string;;endmoduleMulti_select=structmoduleRepeated_click_behavior=structtypet=|No_action|Clear_all|Select_allendletimpl(typetcmp)?(repeated_click_behavior=Repeated_click_behavior.No_action)?(extra_attrs=[])?(disabled=false)?size(moduleM:Setwithtypet=tandtypecomparator_witness=cmp)values~selected~on_change=letopenJs_of_ocamlinletsize=Option.valuesize~default:(List.lengthvalues)inletattrs=[Attr.create"multiple""";Attr.create"size"(Int.to_stringsize);Attr.on_change(funevt(_:string)->lettarget=matchJs.Opt.to_option(Js.Opt.bindevt##.targetDom_html.CoerceTo.select)with|Sometarget->target|None->failwith"Multi_select [on_change] event fired with a missing target or target \
that was not a select element."inletcollection_to_listcollection=List.initcollection##.length~f:(funi->Js.Opt.get(collection##itemi)(fun()->assertfalse))inletoptions=collection_to_listtarget##.optionsinletselected_values=List.filter_map(List.zip_exnvaluesoptions)~f:(fun(value,option)->Option.some_if(Js.to_booloption##.selected)value)inon_change(Set.of_list(moduleM)selected_values))]@extra_attrs|>maybe_disabled~disabledinletoptions=List.mapvalues~f:(funvalue->letis_selected=Set.memselectedvalueinNode.option(* [Attr.bool_property] keeps the state of the option in sync by setting the JS
property. [Attr.selected] modifies the DOM attribute so that selected options
can be styled with CSS. [Attr.selected] alone does not update the state
properly if the model changes, so both are needed. *)~attr:(Attr.many_without_merge([Some(Attr.bool_property"selected"is_selected);Some(Attr.on_click(funevt->letwas_repeated_click=(not(Js.to_boolevt##.ctrlKey))&&Set.equalselected(Set.singleton(moduleM)value)inmatchwas_repeated_click,repeated_click_behaviorwith|false,_|true,No_action->Effect.Ignore|true,Clear_all->on_change(Set.empty(moduleM))|true,Select_all->on_change(Set.of_list(moduleM)values)))]|>List.filter_opt))[Node.text(M.to_stringvalue)])inNode.select~attr:(Attr.many_without_mergeattrs)options;;letof_values(typetcmp)?extra_attrs?repeated_click_behavior?disabled?size(moduleM:Setwithtypet=tandtypecomparator_witness=cmp)values~selected~on_change=impl?extra_attrs?repeated_click_behavior?disabled?size(moduleM)values~selected~on_change;;letof_enum(typetcmp)?extra_attrs?repeated_click_behavior?disabled?size(moduleM:Enum_setwithtypet=tandtypecomparator_witness=cmp)~selected~on_change=impl?extra_attrs?repeated_click_behavior?disabled?size(moduleM)M.all~selected~on_change;;endmoduleEntry=structmoduleCall_on_input_when=structtypet=|Text_changed|Enter_key_pressed_or_focus_lostletlistener=function|Text_changed->Attr.on_input|Enter_key_pressed_or_focus_lost->Attr.on_change;;endletnormalize(moduleM:Stringable.S)s=matchM.to_string(M.of_strings)with|exception_->Some""|v->Somev;;letmaybe_on_returnon_returnattrs=matchon_returnwith|None->attrs|Someon_return->Attr.on_keydown(funev->ifev##.keyCode=13thenon_return()elseEffect.Ignore)::attrs;;letinput_node?(extra_attrs=[])?(disabled=false)?(placeholder="")attrs=Node.input~attr:(Attr.many_without_merge(attrs|>add_attrs[Attr.placeholderplaceholder;Attr.create"spellcheck""false"]|>maybe_disabled~disabled|>add_attrsextra_attrs))[];;letraw?extra_attrs?disabled?placeholder?on_return~value~on_input()=[Attr.string_property"value"value;Attr.on_input(fun_ev->on_input)]|>maybe_on_returnon_return|>input_node?extra_attrs?disabled?placeholder;;letstringable_input_opt(typet)?extra_attrs?(call_on_input_when=Call_on_input_when.Text_changed)?disabled?placeholder?(should_normalize=true)(moduleM:Stringable.Swithtypet=t)~type_attrs~value~on_input=letvalue=letvalue=Option.value_map~f:M.to_stringvalue~default:""inifshould_normalizethenValue_normalizing_hook.createvalue~f:(normalize(moduleM))elseValue_normalizing_hook.createvalue~f:(constNone)in[Call_on_input_when.listenercall_on_input_when(fun_ev->function|""->on_inputNone|s->on_input(Option.try_with(fun()->M.of_strings)));value]|>add_attrstype_attrs|>input_node?extra_attrs?disabled?placeholder;;letof_stringable(typet)?extra_attrs?call_on_input_when?disabled?placeholder(moduleM:Stringable.Swithtypet=t)~value~on_input=stringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleM)~type_attrs:[Attr.type_"text"]~value~on_input;;letvalidated(typet)?extra_attrs?(call_on_input_when=Call_on_input_when.Text_changed)?disabled?placeholder?on_return(moduleM:Stringable.Swithtypet=t)~value~on_input=let(moduleV)=Validated.lift(moduleM)inletvalue_attr=match(value:V.t)with|Initial->Attr.string_property"value"""|_->Value_normalizing_hook.create(V.to_stringvalue)~f:(normalize(moduleV))in[Call_on_input_when.listenercall_on_input_when(fun_evs->on_input(V.of_strings));value_attr;Attr.type_"text"]|>maybe_on_returnon_return|>maybe_invalidvalue|>input_node?extra_attrs?disabled?placeholder;;lettext?extra_attrs?call_on_input_when?disabled?placeholder~value~on_input()=of_stringable?extra_attrs?call_on_input_when?disabled?placeholder(moduleString)~value~on_input;;letnumber(typet)?extra_attrs?call_on_input_when?disabled?placeholder(moduleM:Stringable.Swithtypet=t)~value~step~on_input=stringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleM)~type_attrs:[Attr.type_"number";Attr.create_float"step"step]~value~on_input;;letrange(typet)?extra_attrs?call_on_input_when?disabled?placeholder(moduleM:Stringable.Swithtypet=t)~value~step~on_input=stringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleM)~type_attrs:[Attr.type_"range";Attr.create_float"step"step]~value~on_input;;lettime?extra_attrs?call_on_input_when?disabled?placeholder~value~on_input()=stringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleTime_compat.Ofday)~should_normalize:false~type_attrs:[Attr.type_"time"]~value~on_input;;letdate?extra_attrs?call_on_input_when?disabled?placeholder~value~on_input()=stringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleDate)~should_normalize:false~type_attrs:[Attr.type_"date"]~value~on_input;;letdatetime_local?extra_attrs?call_on_input_when?disabled?placeholder?utc_offset~value~on_input()=lethours=Option.value_maputc_offset(* getTimezoneOffset returns the time zone difference, in minutes, from current
locale to UTC. Utc offset is the difference from UTC to current locale which
is where the minus comes from.
The minutes have to be converted to hours since that is the format
Time.Zone.of_utc_offset expects for the utc_offset. *)~default:((new%jsJs_of_ocaml.Js.date_now)##getTimezoneOffset/-60)~f:(funutc_offset->Time_ns.Span.to_hrutc_offset|>Float.to_int)inlet(moduleZoned_time)=Time_compat.zoned(Time.Zone.of_utc_offset~hours)instringable_input_opt?extra_attrs?call_on_input_when?disabled?placeholder(moduleZoned_time)~type_attrs:[Attr.type_"datetime-local"]~should_normalize:false~value~on_input;;lettext_area?(extra_attrs=[])?(call_on_input_when=Call_on_input_when.Text_changed)?(disabled=false)?(placeholder="")~value~on_input()=Node.textarea~attr:(Attr.many_without_merge([Attr.placeholderplaceholder;Call_on_input_when.listenercall_on_input_when(fun_evvalue->on_inputvalue);Value_normalizing_hook.createvalue~f:Option.return]|>maybe_disabled~disabled|>add_attrsextra_attrs))[];;(* According to
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color#Value the
value must be set in hex format and will always comes back in hex format. *)letcolor_picker?(extra_attr=Attr.empty)?(call_on_input_when=Call_on_input_when.Text_changed)?disabled~value~on_input()=let(`Hexvalue_)=valuein[Attr.(type_"color"@value_propvalue_@extra_attr);Call_on_input_when.listenercall_on_input_when(fun_evs->on_input(`Hexs))]|>input_node?disabled;;endmoduleButton=structletwith_validation?(extra_attrs=[])text~validation~on_click=matchvalidationwith|Okresult->Node.button~attr:(Attr.many_without_merge([Attr.on_click(fun_ev->on_clickresult);Attr.type_"button"]|>add_attrsextra_attrs))[Node.texttext]|Errorreason->Node.button~attr:(Attr.many_without_merge([Attr.disabled;Attr.type_"button";Attr.create"tooltip"reason;Attr.create"tooltip-position""top"]|>add_attrsextra_attrs))[Node.texttext];;letsimple?(extra_attrs=[])?(disabled=false)text~on_click=Node.button~attr:(Attr.many_without_merge([Attr.type_"button";Attr.on_click(fun_ev->on_click())]|>maybe_disabled~disabled|>add_attrsextra_attrs))[Node.texttext];;endmoduleRadio_buttons=structmoduleStyle=structtypet=|Native|Button_likeof{extra_attrs:checked:bool->Attr.tlist}letbarebones_button_like=Button_like{extra_attrs=(fun~checked->ifcheckedthen[Attr.styleCss_gen.(border~width:(`Px1)~color:(`Hex"#D0D0D0")~style:`Solid()@>background_color(`Hex"#404040")@>color(`Hex"#F7F7F7"))]else[Attr.styleCss_gen.(border~width:(`Px1)~color:(`Hex"#D0D0D0")~style:`Solid()@>background_color(`Hex"#EFEFEF"))])};;endlethide_native_inputs=Css_gen.(create~field:"appearance"~value:"none"@>uniform_margin(`Px0));;letimpl?(extra_attrs=[])?(disabled=false)?(style:Style.t=Native)~orientation~name~on_click~selected~to_string~equalvalues=letinput_attrs,label_attrs=matchstylewith|Native->[],fun~checked:_->[]|Button_like{extra_attrs}->[Attr.stylehide_native_inputs],extra_attrsinstructural_list~orientation([Attr.classes["widget-radio-buttons";"radio-button-container"]]|>add_attrsextra_attrs)(List.mapvalues~f:(funitem->letchecked=Option.value_mapselected~default:false~f:(equalitem)inNode.label~attr:(Attr.many_without_merge(label_attrs~checked))[Node.input~attr:(Attr.many_without_merge([Attr.type_"radio";Attr.namename;Attr.classes["radio-button"];Attr.on_click(fun_ev->on_clickitem);Attr.bool_property"checked"checked]@input_attrs|>maybe_disabled~disabled))[];Node.text(to_stringitem)]));;letof_values(typet)?extra_attrs?disabled?style(moduleE:Equalwithtypet=t)~name~on_click~selectedvalues=impl?extra_attrs?disabled?style~orientation:`Vertical~name~on_click~selected~to_string:E.to_string~equal:E.equalvalues;;letof_values_horizontal(typet)?extra_attrs?disabled?style(moduleE:Equalwithtypet=t)~name~on_click~selectedvalues=impl?extra_attrs?disabled?style~orientation:`Horizontal~name~on_click~selected~to_string:E.to_string~equal:E.equalvalues;;endmoduleFile_select=structmoduleJs=Js_of_ocaml.Jsletaccept_attrs=function|None->Attr.empty|Someaccepts->Attr.create"accept"(List.mapaccepts~f:(function|`Extensions->ifString.is_prefixs~prefix:"."thenselse"."^s|`Mimetypes->s)|>String.concat~sep:",");;letlist?(extra_attrs=[])?accept~on_input()=Node.input~attr:(Attr.many_without_merge([Attr.type_"file";accept_attrsaccept;Attr.create"multiple""";Attr.on_file_input(fun_evfile_list->letfiles=List.initfile_list##.length~f:(funi->file_list##itemi|>Js.Opt.to_option|>Option.value_exn~message:[%string"couldn't get file %{i#Int}"])inon_inputfiles)]|>add_attrsextra_attrs))[];;letsingle?(extra_attrs=[])?accept~on_input()=Node.input~attr:(Attr.many_without_merge([Attr.type_"file";accept_attrsaccept;Attr.on_file_input(fun_evfile_list->letfile=file_list##item0|>Js.Opt.to_optioninon_inputfile)]|>add_attrsextra_attrs))[];;end