Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file node_helpers.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341open!Core_kernelopen!Js_of_ocamltypet=|Textofstring|Elementof{tag_name:string;attributes:(string*string)list[@sexp.list];string_properties:(string*string)list[@sexp.list];handlers:(string*Handler.t)list[@sexp.list];key:stringoption[@sexp.option];children:tlist[@sexp.list]}|Widgetofstring[@@derivingsexp_of]letis_tag~tag=function|Element{tag_name;_}->String.equaltag_nametag|_->false;;lethas_class~cls=function|Element{attributes;_}->List.existsattributes~f:(function|"class",data->data|>String.split~on:' '|>List.exists~f:(String.equalcls)|_->false)|_->false;;letrecmapt~f=matchftwith|`Replace_witht->t|`Continue->(matchtwith|Text_|Widget_->t|Element{tag_name;attributes;string_properties;handlers;key;children}->letchildren=List.mapchildren~f:(funch->mapch~f)inElement{tag_name;attributes;string_properties;handlers;key;children});;typehidden_soup=Hidden_soup:_Soup.node->hidden_souptype'abreadcrumb_preference=|Don't_add_breadcrumbs:unitbreadcrumb_preference|Add_breadcrumbs:(Soup.elementSoup.node->t)breadcrumb_preferencemoduleSoup_id=Unique_id.Int()letsoup_id_key="soup-id"letto_lambda_soup(typea)t(breadcrumb_preference:abreadcrumb_preference):hidden_soup*a=lett_by_soup_id=String.Table.create()inletrecconvertt=matchtwith|Texts->Hidden_soup(Soup.create_texts)|Widgetw->Hidden_soup(Soup.create_element"widget"~attributes:["type_id",w])|Element{tag_name;attributes;string_properties;handlers;key;children}->letkey_attrs=matchkeywith|Somekey->["key",key]|None->[]inletsoup_id_attrs=matchbreadcrumb_preferencewith|Don't_add_breadcrumbs->[]|Add_breadcrumbs->letsoup_id=Soup_id.create()|>Soup_id.to_stringinHashtbl.add_exnt_by_soup_id~key:soup_id~data:t;[soup_id_key,soup_id]inlethandler_attrs=List.maphandlers~f:(fun(name,_)->name,"<event-handler>")inletattributes=[key_attrs;soup_id_attrs;handler_attrs;attributes;string_properties]|>List.concat|>String.Map.of_alist_exn(* Raise on duplicate attributes *)|>Map.to_alistinletelement=Soup.create_elementtag_name~attributesinList.iterchildren~f:(funchild->let(Hidden_soupchild)=convertchildinSoup.append_childelementchild);Hidden_soupelementin(convertt,matchbreadcrumb_preferencewith|Don't_add_breadcrumbs->()|Add_breadcrumbs->funsoup->(matchSoup.attributesoup_id_keysoupwith|None->raise_s[%message"Soup.node has no soup-id attribute"]|Somesoup_id->Hashtbl.find_exnt_by_soup_idsoup_id));;let_to_string_htmlt=letHidden_soupsoup,()=to_lambda_souptDon't_add_breadcrumbsinSoup.to_stringsoup;;letto_string_htmlt=letrecrecursebuffer~depth=letindent=String.init(depth*2)~f:(Fn.const' ')infunction|Texts->bprintfbuffer"%s%s"indents|Element{tag_name;attributes;string_properties;handlers;key;children}->bprintfbuffer"%s<%s"indenttag_name;Option.iterkey~f:(bprintfbuffer" @key=%s");List.iterattributes~f:(fun(k,v)->bprintfbuffer" %s=\"%s\""kv);List.iterstring_properties~f:(fun(k,v)->bprintfbuffer" #%s=\"%s\""kv);List.iterhandlers~f:(fun(k,_)->bprintfbuffer" %s={handler}"k);bprintfbuffer">";letchildren_should_collapse=List.for_allchildren~f:(function|Text_->true|_->false)&&List.foldchildren~init:0~f:(funaccchild->matchchildwith|Texts->acc+String.lengths|_->acc)<80-String.lengthindentinletdepth=ifchildren_should_collapsethen0elsedepth+1inList.iterchildren~f:(funchild->ifchildren_should_collapsethenbprintfbuffer" "elsebprintfbuffer"\n";recursebuffer~depthchild);ifchildren_should_collapsethenbprintfbuffer" "else(bprintfbuffer"\n";bprintfbuffer"%s"indent);bprintfbuffer"</%s>"tag_name|Widgets->bprintfbuffer"%s<widget id=%s />"indentsinletbuffer=Buffer.create100inrecursebuffer~depth:0t;Buffer.contentsbuffer;;letselectt~selector=letHidden_soupelement,find_t_by_soup_exn=to_lambda_souptAdd_breadcrumbsinletsoup=Soup.create_soup()inSoup.append_rootsoupelement;soup|>Soup.selectselector|>Soup.to_list|>List.map~f:find_t_by_soup_exn;;letselect_firstt~selector=selectt~selector|>List.hdletselect_first_exnt~selector=matchselect_firstt~selectorwith|Somenode->node|None->raise_s[%message"Failed to find element matching selector"(selector:string)~from_node:(to_string_htmlt:string)];;letunsafe_of_js_exn=letmake_text_node(text:Js.js_stringJs.t)=Text(Js.to_stringtext)inletmake_element_node(tag_name:Js.js_stringJs.t)(children:tJs.js_arrayJs.t)(handlers:(Js.js_stringJs.t*Js.Unsafe.any)Js.js_arrayJs.t)(attributes:(Js.js_stringJs.t*Js.js_stringJs.t)Js.js_arrayJs.t)(string_properties:(Js.js_stringJs.t*Js.js_stringJs.t)Js.js_arrayJs.t)(key:Js.js_stringJs.tJs.Opt.t)=lettag_name=tag_name|>Js.to_stringinletchildren=children|>Js.to_array|>Array.to_listinlethandlers=handlers|>Js.to_array|>Array.to_list|>List.map~f:(fun(s,h)->letname=Js.to_stringsinname,Handler.of_any_exnh~name)inletattributes=attributes|>Js.to_array|>Array.to_list|>List.map~f:(fun(k,v)->Js.to_stringk,Js.to_stringv)inletstring_properties=string_properties|>Js.to_array|>Array.to_list|>List.map~f:(fun(k,v)->Js.to_stringk,Js.to_stringv)inletkey=key|>Js.Opt.to_option|>Option.map~f:Js.to_stringinElement{tag_name;children;handlers;attributes;string_properties;key}inletmake_widget_node(type_id:_Type_equal.Id.t)=Widget(Type_equal.Id.nametype_id)inletraise_unknown_node_typenode_type=letnode_type=Js.to_stringnode_typeinraise_s[%message"unrecognized node type"(node_type:string)]inletf=Js.Unsafe.pure_js_expr{js|
// Convert analyzes a Vdom node that was produced by [Node.to_js] and walks the tree
// recursively, calling make_text_node, make_element_node, and make_widget_node depending
// on the type of node.
(function convert(node, make_text_node, make_element_node, make_widget_node, raise_unknown_node_type) {
switch (node.type) {
case 'VirtualText':
return make_text_node(node.text);
case 'Widget':
return make_widget_node(node.id);
case 'VirtualNode':
var attributes = node.properties.attributes || {};
var attr_list = Object.keys(attributes).map(function (key) {
return [0, key, attributes[key].toString()];
});
var children = node.children.map(function (node) {
return convert(node, make_text_node, make_element_node, raise_unknown_node_type);
});
var handlers =
Object.keys(node.properties)
.filter(function (key) {
// This is a bit of a hack, but it works for all the handlers that we
// have defined at the moment. Consider removing the 'on' check?
return key.startsWith("on") && typeof node.properties[key] === 'function';
})
.map(function (key) {
// [0, ...] is how to generate an OCaml tuple from the JavaScript side.
return [0, key, node.properties[key]];
});
var string_properties =
Object.keys(node.properties)
.filter(function (key) {
return typeof node.properties[key] === 'string';
})
.map(function (key) {
return [0, key, node.properties[key]]
});
return make_element_node(
node.tagName,
children,
handlers,
attr_list,
string_properties,
node.key || null);
default:
raise_unknown_node_type("" + node.type);
}
})
|js}infunvalue->Js.Unsafe.fun_callf[|value;Js.Unsafe.inject(Js.wrap_callbackmake_text_node);Js.Unsafe.inject(Js.wrap_callbackmake_element_node);Js.Unsafe.inject(Js.wrap_callbackmake_widget_node);Js.Unsafe.inject(Js.wrap_callbackraise_unknown_node_type)|];;letunsafe_convert_exnvdom_node=vdom_node|>Virtual_dom.Vdom.Node.unsafe_to_js|>Js.Unsafe.inject|>unsafe_of_js_exn;;letget_handlers(node:t)=matchnodewith|Element{handlers;tag_name=_;attributes=_;string_properties=_;key=_;children=_}->handlers|_->raise_s[%message"expected Element node"(node:t)];;lettrigger_many?extra_fieldsnode~event_names=letall_handlers=get_handlersnodeinletcount=List.countevent_names~f:(funevent_name->matchList.Assoc.findall_handlersevent_name~equal:String.equalwith|None->false|Somehandler->Handler.triggerhandler?extra_fields;true)inmatchcountwith|0->raise_s[%message"No handler found on element"(event_names:stringlist)]|_->();;lettrigger?extra_fieldsnode~event_name=trigger_many?extra_fieldsnode~event_names:[event_name];;moduleUser_actions=structletclick_onnode=trigger~event_name:"onclick"nodeletinput_textelement~text=lettag_name=matchelementwith|Element{tag_name;_}->tag_name|other->letnode=to_string_htmlotherinraise_s[%message(node:string)"is not an element"]inletvalue_element=(* When an [on_input] event is fired, in order to pull the value of
the element, [Virtual_dom.Vdom.Attr.on_input_event] looks at the
"target" property on the event and tries to coerce that value to one
of [input element, select element, textarea element]. This coercion
function is implemented in [Js_of_ocaml.Dom_html.CoerceTo], and the
way that the coercion function works is by comparing the value of
the [tagName] property on the event target to the string of the tag
name that the coercion is targeting.
By mocking out the [tagName] and [value] properties on the target of
the event, we can trick the virtual_dom code into handling our event
as though there was a real DOM element! *)Js.Unsafe.inject(object%jsvaltagName=Js.stringtag_namevalvalue=Js.stringtextend)inletextra_fields=["target",value_element]inletevent_names=["oninput";"onchange"]intrigger_manyelement~extra_fields~event_names;;end