123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487open!Coreopen!ImportopenBonsai_webopenSingle_factor_intfmoduletypeS=SmoduletypeItem=ItemmoduleMake(Item:Item)=structmoduleSearcher=struct(* Filters a set of items according to a search string *)moduleT=structmoduleInput=Item.SetmoduleResult=structtypet={items_matching_search:Item.Set.t;update_search:string->unitBonsai.Effect.t;current_search:string}endmoduleModel=StringmoduleAction=Stringletapply_action~inject:_~schedule_event:__input_modelnew_search=new_searchletcompute~injectall_itemssearch_string=letitems_matching_search=Set.filterall_items~f:(funitem->String.Caseless.is_substring(Item.to_stringitem)~substring:search_string)in{Result.items_matching_search;update_search=inject;current_search=search_string};;letname=Source_code_position.to_string[%here]endincludeTletbonsai~initialinput=Bonsai.of_module1(moduleT)~default_model:initialinput;;endmoduleT=structmoduleSelection_status=structtypet=|Selected|Unselected[@@derivingcompare,equal,sexp]lettoggle=function|Selected->Unselected|Unselected->Selected;;endmoduleView_config=structtypet={header:Vdom.Node.t;autofocus_search_box:bool;search_box_id:stringoption;extra_row_attrs:(is_focused:bool->Vdom.Attr.t)option}letcreate?extra_row_attrs?(autofocus_search_box=false)?id~header()={header;autofocus_search_box;search_box_id=id;extra_row_attrs};;endmoduleInput=structtypet={items_matching_search:Item.Set.t;update_search:string->unitBonsai.Effect.t;all_items:Item.Set.t;default_selection_status:Selection_status.t;current_search:string;view_config:View_config.t}endmoduleModel=structtypet={selection_status:Selection_status.tMap.M(Item).t;focused_item:Item.toption}[@@derivingcompare,equal,fields,sexp]letcreate?(selection_status=Item.Map.empty)?focused_item()={selection_status;focused_item};;endmoduleAction=structtypet=|Update_search_stringofstring|Set_item_selectedof{item:Item.t;status:Selection_status.t}|Set_all_selection_statusesofSelection_status.tItem.Map.t|Toggle_focused_item_selected|Set_focusofItem.toption|Move_focusof[`Next|`Prev]|Select_all|Select_none[@@derivingsexp_of]endmoduleResult=structtypet={view:Vdom.Node.t;view_for_testing:stringLazy.t;key_handler:Vdom_keyboard.Keyboard_event_handler.t;inject:Action.t->unitBonsai.Effect.t;selected_items:Item.Set.t}endletmove_in_setsetelement~dir=matchelementwith|None->(matchdirwith|`Prev->Set.max_eltset|`Next->Set.min_eltset)|Someelement->letsequence=matchdirwith|`Prev->Set.to_sequenceset~less_or_equal_to:element~order:`Decreasing|`Next->Set.to_sequenceset~greater_or_equal_to:element~order:`Increasingin(* The first element in the sequence will be [element], since the arguments that
we pass are "_or_equal_to". *)Sequence.nthsequence1;;letfocused_item(input:Input.t)(model:Model.t)=Option.bindmodel.focused_item~f:(funfocus->ifSet.meminput.items_matching_searchfocusthenSomefocuselseNone);;letis_item_selected(input:Input.t)(model:Model.t)~item=matchMap.findmodel.selection_statusitem|>Option.value~default:input.default_selection_statuswith|Selected->true|Unselected->false;;letselected_items(input:Input.t)(model:Model.t)=letexplicitly_selected=List.filter_map(Map.to_alistmodel.selection_status)~f:(fun(item,status)->matchstatuswith|Selected->Someitem|Unselected->None)|>Item.Set.of_listinletdefaults=matchinput.default_selection_statuswith|Unselected->Item.Set.empty|Selected->Set.diffinput.all_items(Set.of_map_keysmodel.selection_status)inSet.unionexplicitly_selecteddefaults;;letapply_action~inject:_~schedule_event(input:Input.t)(model:Model.t)(action:Action.t)=matchactionwith|Update_search_stringsearch_string->letfocused_item=Option.bindmodel.focused_item~f:(funfocused_item->ifnot(Set.meminput.items_matching_searchfocused_item)thenNoneelseSomefocused_item)inschedule_event(input.update_searchsearch_string);{modelwithfocused_item}|Set_item_selected{item;status}->{modelwithselection_status=Map.setmodel.selection_status~key:item~data:status}|Set_all_selection_statusesselection_status->{modelwithselection_status}|Toggle_focused_item_selected->(matchfocused_iteminputmodelwith|None->model|Somefocused_item->letselection_status=Map.updatemodel.selection_statusfocused_item~f:(funstatus->letstatus=Option.valuestatus~default:input.default_selection_statusinSelection_status.togglestatus)in{modelwithselection_status})|Set_focusitem->{modelwithfocused_item=item}|Move_focusdir->letfocused_item=move_in_setinput.items_matching_search(focused_iteminputmodel)~dirin(matchfocused_itemwith|None->model|Somefocused_item->{modelwithfocused_item=Somefocused_item})|Select_all->letselection_status=Set.foldinput.items_matching_search~init:model.selection_status~f:(funmapitem->Map.setmap~key:item~data:Selected)in{modelwithselection_status}|Select_none->letselection_status=Set.foldinput.items_matching_search~init:model.selection_status~f:(funmapitem->Map.setmap~key:item~data:Unselected)in{modelwithselection_status};;letview_for_testing(input:Input.t)(model:Model.t)=String.concat~sep:"\n"(sprintf"Search string: '%s'"input.current_search::List.map(Set.to_listinput.items_matching_search)~f:(funitem->letis_focused=matchmodel.focused_itemwith|None->false|Someitem'->Item.(=)itemitem'inletis_selected=is_item_selectedinputmodel~iteminsprintf!"%s %s %{Item}"(ifis_focusedthen"->"else" ")(ifis_selectedthen"*"else" ")item));;letsearch_box_view(input:Input.t)~inject~autofocus~id=letopenVdominleton_input=function|None->inject(Action.Update_search_string"")|Sometext->inject(Action.Update_search_stringtext)inletextra_attrs=Attr.combine(Attr.autofocusautofocus)(Option.value_mapid~f:Attr.id~default:Attr.empty)inVdom_input_widgets.Entry.text~value:(Someinput.current_search)~on_input~extra_attrs:[extra_attrs]();;letselect_all_and_none_view~inject=letopenVdominletlink~text~(action:Action.t)~class_=Node.a~attr:(Attr.many_without_merge[Attr.href"about:blank";Attr.on_click(funev->matchBonsai_web.am_within_disabled_fieldsetevwith|true->Effect.Prevent_default|false->Effect.Many[injectaction;Effect.Prevent_default]);Attr.class_class_])[Node.texttext]inNode.div~attr:(Attr.class_"multi-select-select-all-none")[Node.text"Select: ";link~text:"all"~action:Select_all~class_:"multi-select-select-all";Node.text"; ";link~text:"none"~action:Select_none~class_:"multi-select-select-none"];;letcheckboxes_view(input:Input.t)(model:Model.t)~extra_row_attrs~selected_items~inject=letopenVdominletfocused_item=focused_iteminputmodelinletcheckboxes=List.map(Set.to_listinput.items_matching_search)~f:(funitem->letcheckbox=leton_change=Attr.on_change(funev_new_value->letstatus=matchJs_of_ocaml.Js.Opt.to_optionev##.targetwith|None->Js_of_ocaml.Firebug.console##error"Target missing";assertfalse|Somet->letvalue=(Js_of_ocaml.Js.Unsafe.coercet)##.checkedinifJs_of_ocaml.Js.to_boolvaluethenSelection_status.SelectedelseUnselectedininject(Action.Set_item_selected{item;status}))inletchecked_attrs=[Attr.checked;Attr.bool_property"checked"true]inletunchecked_attrs=[Attr.bool_property"checked"false]inNode.input~attr:(Attr.many_without_merge([on_change;Attr.type_"checkbox"]@ifSet.memselected_itemsitemthenchecked_attrselseunchecked_attrs))[]inletis_focused=[%compare.equal:Item.toption](Someitem)focused_iteminletextra_attrs=extra_row_attrs~is_focusedinletfocus_attrs=ifis_focusedthenVdom.Attr.(many[extra_attrs;Vdom.Attr.class_"multi-select-item-focused"])elseextra_attrsinleton_click=Attr.on_click(funev->matchBonsai_web.am_within_disabled_fieldsetevwith|true->Effect.Ignore|false->Effect.Many[inject(Action.Set_focus(Someitem));injectToggle_focused_item_selected])inNode.div~attr:(Attr.many[on_click;Vdom.Attr.class_"multi-select-item";focus_attrs])[checkbox;Node.label[Node.text(Item.to_stringitem)]])inNode.div~attr:(Attr.class_"multi-select-checkboxes")checkboxes;;letview(input:Input.t)model~selected_items~inject=letopenVdominletselect_all_and_none_view=select_all_and_none_view~injectinletsearch_box=search_box_viewinput~inject~autofocus:input.view_config.autofocus_search_box~id:input.view_config.search_box_idinletextra_row_attrs=Option.valueinput.view_config.extra_row_attrs~default:(fun~is_focused:_->Vdom.Attr.empty)inletcheckboxes=checkboxes_viewinputmodel~selected_items~inject~extra_row_attrsinNode.div~attr:(Attr.class_"multi-select-container")[Node.div~attr:(Attr.class_"multi-select-header")[input.view_config.header];search_box;select_all_and_none_view;checkboxes];;letkey_handler~inject=letopenVdom_keyboardinletcommand?cond~keys~descriptionf=lethandler=letopenKeyboard_event_handler.Handlerinmatchcondwith|None->with_prevent_defaultf|Somecond->only_handle_ifcondf~prevent_default:()in{Keyboard_event_handler.Command.keys;description;group=None;handler}inletkey=Keystroke.create'inKeyboard_event_handler.of_command_list_exn[command~keys:[keyArrowUp]~description:"Move focus one item up"(fun_ev->inject(Action.Move_focus`Prev));command~keys:[keyArrowDown]~description:"Move focus one item down"(fun_ev->inject(Action.Move_focus`Next));command~keys:[keyEnter]~description:"Toggle whether focused item is selected"(fun_ev->injectAction.Toggle_focused_item_selected)];;letcompute~injectinputmodel=letselected_items=selected_itemsinputmodelin{Result.view_for_testing=lazy(view_for_testinginputmodel);view=viewinputmodel~selected_items~inject;key_handler=key_handler~inject;selected_items;inject};;letname=Source_code_position.to_string[%here]endincludeTletbonsai'input=Bonsai.of_module1(moduleT)inputmodule_=structtypet={all_items:Item.Set.t;default_selection_status:Selection_status.t;view_config:View_config.t}[@@derivingfields]endmoduleInitial_model_settings=structtypet={search_string:string;selection_status:Selection_status.tItem.Map.toption;focused_item:Item.toption}letcreate?(search_string="")?selection_status?focused_item()={search_string;selection_status;focused_item};;endletbonsai?(initial_model_settings=Initial_model_settings.create())?(default_selection_status=Value.returnSelection_status.Unselected)~view_configall_items=letopenBonsai.Let_syntaxinlet%subsearch_results=Searcher.bonsai~initial:initial_model_settings.search_stringall_itemsinletinput_for_t=let%mapall_items=all_itemsandview_config=view_configanddefault_selection_status=default_selection_statusand{Searcher.Result.items_matching_search;update_search;current_search}=search_resultsin{T.Input.items_matching_search;update_search;default_selection_status;all_items;current_search;view_config}inbonsai'~default_model:(T.Model.create?selection_status:initial_model_settings.selection_status?focused_item:initial_model_settings.focused_item())input_for_t;;end