Source file vdom_blit.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
open Js_browser
open Vdom
let debug = false
type Vdom.js_object += Ojs of Ojs.t
module BDecoder = struct
type step =
| Nth of int
| Key of string
type typ =
| TyList
| TyString
| TyBool
| TyInt
| TyFloat
type path = step list
type error_kind =
| Key_unbound of string
| Method_unbound of string
| Msg of string
| Type_error of typ * string
type error =
path * error_kind
let string_of_typ = function
| TyList -> "list"
| TyString -> "string"
| TyBool -> "boolean"
| TyInt -> "int"
| TyFloat -> "float"
let string_of_step = function
| Nth n -> string_of_int n
| Key s -> s
let string_of_path path =
String.concat "." (List.rev_map string_of_step path)
let string_of_error_kind = function
| Key_unbound s -> Printf.sprintf "unbound field: %S" s
| Method_unbound s -> Printf.sprintf "unbound method (or wrong number of arguments): %S" s
| Msg s -> s
| Type_error (expected, got) -> Printf.sprintf "cannot convert %s to %s" got (string_of_typ expected)
let string_of_error = function
| [], errk ->
string_of_error_kind errk
| (_ :: _ as path), errk ->
"At path [" ^ string_of_path path ^ "]: " ^ string_of_error_kind errk
type o = { path: path; obj: Ojs.t }
type 'a t = o -> ('a, error) result
let field key d o =
let rec aux o = function
| [] -> Ok o
| key :: keys ->
let obj = Ojs.get_prop_ascii o.obj key in
if obj = Ojs.unit_to_js () then
Error (o.path, Key_unbound key)
else aux {path = (Key key :: o.path); obj} keys
in
let keys = String.split_on_char '.' key in
Result.bind (aux o keys) d
let method_ name arg d o =
let arg =
List.map (function
| Decoder.StringArg s -> Ojs.string_to_js s
| BoolArg b -> Ojs.bool_to_js b
| FloatArg f -> Ojs.float_to_js f
| IntArg i -> Ojs.int_to_js i
) arg
in
let arg = Array.of_list arg in
try
let obj = Ojs.call o.obj name arg in
d {path = (Key name :: o.path); obj}
with
Ojs_exn.Error _ -> Error (o.path, Method_unbound name)
let bind (f : 'a -> 'b t) (d : 'a t) o =
match d o with
| Ok x -> f x o
| Error err -> Error err
let const msg _o = Ok msg
let fail msg {path; _} =
Error (path, Msg msg)
let try_ d o =
match d o with
| Ok x -> Ok (Some x)
| Error _ -> Ok None
let factor (f : ('a -> 'b t)) o =
Ok (fun x -> Result.map_error string_of_error (f x o))
let typerr ty path got =
Error (path, Type_error (ty, got))
let string {path; obj} =
match Ojs.type_of obj with
| "string" -> Ok (Ojs.string_of_js obj)
| ty -> typerr TyString path ty
let int {path; obj} =
match Ojs.type_of obj with
| "number" -> Ok (Ojs.int_of_js obj)
| ty -> typerr TyInt path ty
let float {path; obj} =
match Ojs.type_of obj with
| "number" -> Ok (Ojs.float_of_js obj)
| ty -> typerr TyFloat path ty
let bool {path; obj} =
match Ojs.type_of obj with
| "boolean" -> Ok (Ojs.bool_of_js obj)
| ty -> typerr TyBool path ty
let object_ {path=_; obj} = Ok (Ojs obj)
let mapi_list_result f l =
let rec loop i acc = function
| [] -> Ok (List.rev acc)
| x :: xs -> Result.bind (f i x) (fun x -> loop (i + 1) (x :: acc) xs)
in
loop 0 [] l
let list d o =
match Ojs.type_of o.obj with
| "object" ->
mapi_list_result
(fun i obj -> d {path = Nth i :: o.path; obj})
(Ojs.list_of_js Fun.id o.obj)
| ty -> typerr TyList o.path ty
let run d obj =
match d {path = []; obj} with
| Ok x -> Ok x
| Error err -> Error (string_of_error err)
let rec eval : type a. ?extra_fields:(string * Ojs.t) list -> a Decoder.t -> a t = fun ?( = []) -> function
| Field (key, d) ->
begin match List.assoc_opt key extra_fields with
| Some obj ->
fun o -> ((eval d) {path = (Key key :: o.path); obj})
| None ->
field key (eval d)
end
| Method (name, arg, d) -> method_ name arg (eval d)
| Bind (f, d) -> bind (fun a -> eval ~extra_fields (f a)) (eval ~extra_fields d)
| Factor f -> factor (fun a -> eval ~extra_fields (f a))
| Const msg -> const msg
| String -> string
| Int -> int
| Float -> float
| Bool -> bool
| Object -> object_
| List d -> list (eval d)
| Fail msg -> fail msg
| Try d -> try_ (eval ~extra_fields d)
let decode ?( = []) d = run (eval ~extra_fields d)
let decode_fail ?( = []) d o =
match decode ~extra_fields d o with
| Ok res -> res
| Error err -> failwith err
end
module Encoder = struct
type arg_value =
| StringArg of string
| BoolArg of bool
| FloatArg of float
type t =
| Unit
| String of string
| Int of int
| Float of float
| Bool of bool
| List of t list
| Obj of (string * t) list
| Fun of (arg_value list -> t)
let convert_arg o =
match Ojs.type_of o with
| "string" -> StringArg (Ojs.string_of_js o)
| "boolean" -> BoolArg (Ojs.bool_of_js o)
| "number" -> FloatArg (Ojs.float_of_js o)
| ty -> failwith ("Encoded function received an argument with unsupported type: " ^ ty)
let rec encode = function
| Unit -> Ojs.unit_to_js ()
| String s -> Ojs.string_to_js s
| Int i -> Ojs.int_to_js i
| Float f -> Ojs.float_to_js f
| Bool b -> Ojs.bool_to_js b
| List l -> Ojs.list_to_js encode l
| Obj l -> Ojs.obj (Array.of_list (List.map (fun (s, e) -> (s, encode e)) l))
| Fun f -> Ojs.fun_to_js_args (fun o -> encode (f (Ojs.list_of_js convert_arg o)))
end
module Cmd = struct
type 'msg ctx =
{
container: Js_browser.Element.t;
send_msg: ('msg -> unit);
after_redraw: (unit -> unit) -> unit;
}
let container ctx = ctx.container
let send_msg ctx = ctx.send_msg
let after_redraw ctx = ctx.after_redraw
type handler = {f: 'msg. 'msg ctx -> 'msg Vdom.Cmd.t -> bool}
let rec run: type t. ((unit -> unit) -> unit) -> handler list -> (t -> unit) -> Js_browser.Element.t -> t Cmd.t -> unit =
fun after h p elt -> function
| Cmd.Echo msg -> p msg
| Cmd.Batch l -> List.iter (run after h p elt) l
| Cmd.Map (f, cmd) -> run after h (fun x -> p (f x)) elt cmd
| Cmd.Bind (cmd, f) -> run after h (fun x -> run after h p elt (f x)) elt cmd
| x ->
let ctx = {send_msg = p; container = elt; after_redraw = after} in
let rec loop = function
| [] -> Printf.ksprintf failwith "No command handler found! (%s)" (Obj.Extension_constructor.name (Obj.Extension_constructor.of_val x))
| hd :: tl ->
if hd.f ctx x then ()
else loop tl
in
loop h
end
module Custom = struct
type t =
{
dom: Js_browser.Element.t;
sync: (Vdom.Custom.t -> bool);
dispose: (unit -> unit);
}
type event =
| Custom of Custom.event
| Encoded of { event_type: string; encoder: Encoder.t }
let blur_event = Encoded {event_type = "blur"; encoder = Obj []}
let input_event s = Encoded {event_type = "input"; encoder = Obj ["target", Obj ["value", String s]]}
let checked_event b = Encoded {event_type = "click"; encoder = Obj ["target", Obj ["checked", Bool b]]}
let change_event s = Encoded {event_type = "change"; encoder = Obj ["target", Obj ["value", String s]]}
let change_index_event i = Encoded {event_type = "change"; encoder = Obj ["target", Obj ["selectedIndex", Int i]]}
let custom_event e = Custom e
type ctx =
{
parent: Js_browser.Element.t;
send_event: (event -> unit);
after_redraw: ((unit -> unit) -> unit);
}
type handler = ctx -> Vdom.Custom.t -> t option
let make ?(dispose = ignore) ~sync dom =
{dom; sync; dispose}
let parent ctx = ctx.parent
let send_event ctx = ctx.send_event
let after_redraw ctx = ctx.after_redraw
let rec find_handler ctx x = function
| [] -> failwith "Vdom_blit: no custom element handler found"
| hd :: tl ->
begin match hd ctx x with
| Some f -> f
| None -> find_handler ctx x tl
end
let lookup ~parent ~process_custom ~after_redraw elt handlers =
let rec dom = lazy ((Lazy.force el).dom)
and send_event e = process_custom (Lazy.force dom) e
and el = lazy (find_handler {parent; send_event; after_redraw} elt handlers) in
Lazy.force el
end
type 'msg ctrl =
| BText of {vdom: 'msg vdom; dom: Element.t}
| BFragment of {vdom: 'msg vdom; doms: Element.t list; children: 'msg ctrl list}
| BElement of {vdom: 'msg vdom; dom: Element.t; children: 'msg ctrl list}
| BMap: {vdom: 'msg vdom; doms: Element.t list; f: ('submsg -> 'msg); child: 'submsg ctrl} -> 'msg ctrl
| BMemo: {vdom: 'msg vdom; doms: Element.t list; child: 'msg ctrl} -> 'msg ctrl
| BCustom of {vdom: 'msg vdom; elt: Custom.t; ns: string; propagate_events: bool}
let get_doms = function
| BText x -> [x.dom]
| BFragment x -> x.doms
| BElement x -> [x.dom]
| BMap x -> x.doms
| BMemo x -> x.doms
| BCustom x -> [x.elt.dom]
let get_vdom = function
| BText x -> x.vdom
| BFragment x -> x.vdom
| BElement x -> x.vdom
| BMap x -> x.vdom
| BMemo x -> x.vdom
| BCustom x -> x.vdom
let key_of_vdom = function
| Text {key; _}
| Fragment {key; _}
| Element {key; _}
| Map {key; _}
| Memo {key; _}
| Custom {key; _} ->
key
let eval_prop = function
| String x -> Ojs.string_to_js x
| Int x -> Ojs.int_to_js x
| Bool x -> Ojs.bool_to_js x
| Float x -> Ojs.float_to_js x
let string_of_prop = function
| String s -> s
| Int x -> string_of_int x
| Bool x -> string_of_bool x
| Float x -> string_of_float x
let same_prop v1 v2 =
v1 == v2 ||
match v1, v2 with
| String x1, String x2 -> x1 = x2
| Int x1, Int x2 -> x1 = x2
| Bool x1, Bool x2 -> x1 = x2
| Float x1, Float x2 -> x1 = x2
| _ -> false
let bmemo vdom child =
BMemo {vdom; doms = get_doms child; child}
let is_visible dom =
let bounding = Element.get_bounding_client_rect dom in
let top = Rect.top bounding in
let bottom = Rect.bottom bounding in
top >= 0.0 && bottom <= Window.inner_height window
let custom_attribute prop =
match prop with
| "scroll-to-show" ->
Some
(fun dom v ->
try
let align_top =
match v with
| Bool false -> false
| _ -> true
in
if not (is_visible dom) then
Element.scroll_into_view dom align_top;
with exn -> Printf.printf "scroll: %s\n%!" (Printexc.to_string exn)
)
| "autofocus" ->
Some
(fun dom v ->
let do_focus =
match v with
| String "if-visible" -> is_visible dom
| _ -> true
in
if do_focus then Element.focus dom
)
| "select" ->
Some (fun dom _ -> Element.select dom)
| "autosubmit" ->
Some (fun dom _ -> Element.submit dom)
| _ -> None
let async f =
ignore (Window.set_timeout window f 0)
let apply_effect_prop dom prop value =
match custom_attribute prop with
| None -> false
| Some f -> async (fun () -> f dom value); true
let set_style dom k v =
if String.length k > 0 && k.[0] = '-' then
Ojs.call
(Ojs.get_prop_ascii (Element.t_to_js dom) "style")
"setProperty"
[| Ojs.string_to_js k; v |] |> ignore
else
Ojs.set_prop_ascii
(Ojs.get_prop_ascii (Element.t_to_js dom) "style")
k
v
let apply_special_prop ns dom k v =
match ns, k, v with
| "http://www.w3.org/2000/svg", "className", String s ->
let class_obj = Ojs.get_prop_ascii (Element.t_to_js dom) "className" in
Ojs.set_prop_ascii class_obj "baseVal" (Ojs.string_to_js s);
true
| _ -> false
let js_empty_string = Ojs.string_to_js ""
let clear_special_prop ns dom k =
match ns, k with
| "http://www.w3.org/2000/svg", "className" ->
let class_obj = Ojs.get_prop_ascii (Element.t_to_js dom) "className" in
Ojs.set_prop_ascii class_obj "baseVal" js_empty_string;
true
| _ -> false
type env =
{
cmds: Cmd.handler list;
customs: Custom.handler list;
}
let empty = {cmds = []; customs = []}
let cmd h = {empty with cmds = [h]}
let custom h = {empty with customs = [h]}
let merge envs =
{
cmds = List.concat (List.map (fun e -> e.cmds) envs);
customs = List.concat (List.map (fun e -> e.customs) envs);
}
let global = ref empty
let register e = global := merge [e; !global]
type ctx =
{
process_custom: (Element.t -> Custom.event -> unit);
custom_handlers: Custom.handler list;
after_redraw: ((unit -> unit) -> unit);
add_listener: string -> unit;
}
let apply_attributes ctx ns dom attributes =
List.iter
(function
| Property (k, v) ->
if not (apply_special_prop ns dom k v) then
if not (apply_effect_prop dom k v) then
Ojs.set_prop_ascii (Element.t_to_js dom) k (eval_prop v)
| Style (k, v) -> set_style dom k (Ojs.string_to_js v)
| Attribute (k, v) ->
Element.set_attribute dom k v
| Handler Decoder {event_type; _} -> ctx.add_listener event_type
| Handler CustomEvent _ -> ()
)
attributes
let rec blit : 'msg. parent:_ -> ctx -> 'msg vdom -> 'msg ctrl =
fun ~parent ctx vdom ->
match vdom with
| Text {txt; key = _} ->
BText {vdom; dom = Document.create_text_node document txt}
| Fragment {children; key = _} ->
let children = List.map (blit ~parent ctx) children in
let doms = List.concat_map get_doms children in
BFragment {vdom; doms; children }
| Map {f; child; key = _} ->
let child = blit ~parent ctx child in
BMap {vdom; doms = get_doms child; f; child}
| Memo {f; arg; key = _} ->
bmemo vdom (blit ~parent ctx (f arg))
| Custom {elt; attributes; key = _; propagate_events} ->
let elt =
try Custom.lookup ~parent ~process_custom:ctx.process_custom ~after_redraw:ctx.after_redraw elt (ctx.custom_handlers @ (!global).customs);
with exn ->
Printf.printf "Error during vdom Custom %s lookup: %s\n%!"
(Obj.Extension_constructor.name (Obj.Extension_constructor.of_val elt))
(Printexc.to_string exn);
raise exn
in
let ns =
Ojs.option_of_js
Ojs.string_of_js
(Ojs.get_prop_ascii (Element.t_to_js elt.dom) "namespaceURI")
|> Option.value ~default:""
in
apply_attributes ctx ns elt.dom attributes;
BCustom {vdom; elt; ns; propagate_events}
| Element {ns; tag; children; attributes; key = _} ->
if debug then Printf.printf "create <%s>\n%!" tag;
let dom =
if ns = "" then Document.create_element document tag
else Document.create_element_ns document ns tag
in
let children = List.map (blit ~parent:dom ctx) children in
List.iter (fun c -> List.iter (Element.append_child dom) (get_doms c)) children;
apply_attributes ctx ns dom attributes;
BElement {vdom; dom; children}
let blit ~parent ctx vdom =
try blit ~parent ctx vdom
with exn ->
Printf.printf "Error during vdom blit: %s\n%!" (Printexc.to_string exn);
raise exn
let sync_props to_string same set clear l1 l2 =
let sort = List.sort (fun (k1, _) (k2, _) -> String.compare k1 k2) in
let l1 = sort l1 and l2 = sort l2 in
let rec loop l1 l2 =
match l1, l2 with
| [], [] -> ()
| (k1, v1) :: tl1, (k2, _) :: _ when k1 < k2 ->
if debug then Printf.printf "Property %s unset %s =>\n%!" k1 (to_string v1);
clear k1 v1;
loop tl1 l2
| (k1, v1) :: tl1, [] ->
if debug then Printf.printf "Property %s unset %s =>\n%!" k1 (to_string v1);
clear k1 v1;
loop tl1 []
| (k1, _) :: _, (k2, v2) :: tl2 when k2 < k1 ->
if debug then Printf.printf "Property %s set => %s\n%!" k2 (to_string v2);
set k2 v2;
loop l1 tl2
| [], (k2, v2) :: tl2 ->
if debug then Printf.printf "Property %s set => %s\n%!" k2 (to_string v2);
set k2 v2;
loop [] tl2
| (_k1, v1) :: tl1, (k2, v2) :: tl2 ->
if not (same v1 v2) then begin
if debug then Printf.printf "Property %s changed %s => %s\n%!" k2 (to_string v1) (to_string v2);
set k2 v2;
end;
loop tl1 tl2
in
loop l1 l2
let rec choose f = function
| [] -> []
| hd :: tl ->
match f hd with
| None -> choose f tl
| Some x -> x :: choose f tl
let js_zero = Ojs.int_to_js 0
let js_false = Ojs.bool_to_js false
let has_own_property o x =
let open Ojs in
bool_of_js (call o "hasOwnProperty" [| string_to_js x |])
let sync_attributes ctx ns dom a1 a2 =
let props = function Property (k, v) -> Some (k, v) | Style _ | Handler _ | Attribute _ -> None in
let set k v =
match k, v with
| "value", String s when s = Element.value dom -> ()
| _ ->
if not (apply_special_prop ns dom k v) then
if not (apply_effect_prop dom k v) then
Ojs.set_prop_ascii (Element.t_to_js dom) k (eval_prop v)
in
let clear k v =
if custom_attribute k = None then
if has_own_property (Element.t_to_js dom) k then
Ojs.delete_prop_ascii (Element.t_to_js dom) k
else if not (clear_special_prop ns dom k) then
Ojs.set_prop_ascii (Element.t_to_js dom) k
begin match v with
| String _ -> js_empty_string
| Int _ | Float _ -> js_zero
| Bool _ -> js_false
end
in
sync_props
string_of_prop
same_prop
set clear
(choose props a1)
(choose props a2);
let styles = function Style (k, v) -> Some (k, String v) | Property _ | Handler _ | Attribute _ -> None in
let set k v = set_style dom k (eval_prop v)in
let clear k _ = set_style dom k js_empty_string in
sync_props
string_of_prop
same_prop
set clear
(choose styles a1)
(choose styles a2);
let attrs = function Attribute (k, v) -> Some (k, v) | Style _ | Property _ | Handler _ -> None in
let set k v = Element.set_attribute dom k v in
let clear k _ = Element.remove_attribute dom k in
sync_props
Fun.id
(fun (s1: string) s2 -> s1 = s2)
set clear
(choose attrs a1)
(choose attrs a2);
List.iter
(function | Handler Decoder {event_type; _} -> ctx.add_listener event_type
| _ -> ())
a2
let rec dispose : type msg. msg ctrl -> unit = fun ctrl ->
match ctrl with
| BText _ -> ()
| BCustom {elt; _} -> elt.dispose ()
| BFragment {children; _}
| BElement {children; _} -> List.iter dispose children
| BMap {child; _} -> dispose child
| BMemo {child; _} -> dispose child
let print_element node =
if Element.null == node then
"null"
else
match Element.node_type node with
| Element.ELEMENT_NODE -> Element.outer_HTML node
| Element.TEXT_NODE -> Element.node_value node
| _ -> "??"
let remove_child parent child =
if debug then
Printf.printf "remove_child(%s, %s)\n" (print_element parent) (print_element child);
Element.remove_child parent child
let replace_child parent n o =
if debug then
Printf.printf "replace_child(%s, %s, %s)\n" (print_element parent) (print_element n) (print_element o);
Element.replace_child parent n o
let insert_before parent o n =
if debug then
Printf.printf "insert_before(%s, %s, %s)\n" (print_element parent) (print_element o) (print_element n);
Element.insert_before parent o n
let rec sync : type old_msg msg. ctx -> Element.t -> bool -> Element.t -> old_msg ctrl -> msg vdom -> msg ctrl =
fun ctx parent prev_move next old vdom ->
match old, vdom with
| _ when (vdom : msg vdom) == (Obj.magic (get_vdom old : old_msg vdom)) ->
(Obj.magic (old : old_msg ctrl) : msg ctrl)
| BText {vdom = Text {txt = s1; key = _}; dom}, Text {txt = s2; key = _} ->
if s1 <> s2 then Element.set_node_value dom s2;
BText {vdom; dom}
| BMap {child = c1; _}, Map {f; child = c2; key = _} ->
let child = sync ctx parent prev_move next c1 c2 in
BMap {vdom; doms = get_doms child; child; f}
| BMemo {child = c1; vdom = Memo {f = f1; arg = a1; key = _}; _}, Memo {f = f2; arg = a2; key = _} ->
if Obj.magic f1 == f2 && Obj.magic a1 == a2 then
bmemo vdom (Obj.magic (c1 : old_msg ctrl) : msg ctrl)
else
bmemo vdom (sync ctx parent prev_move next c1 (f2 a2))
| BCustom {vdom = Custom {key=key1; elt=arg1; attributes=a1; propagate_events = _}; propagate_events = _; elt; ns}, Custom {key=key2; elt=arg2; attributes=a2; propagate_events}
when key1 = key2 && (arg1 == arg2 || elt.sync arg2) ->
sync_attributes ctx ns elt.dom a1 a2;
BCustom {vdom; elt; ns; propagate_events}
| BFragment {vdom = Fragment e1; children; _}, Fragment e2 when e1.key = e2.key ->
let children = sync_children ctx parent prev_move next children e2.children in
let doms = List.concat_map get_doms children in
BFragment {vdom; doms; children }
| BElement {vdom = Element e1; dom; children}, Element e2 when e1.tag = e2.tag && e1.ns = e2.ns && e1.key = e2.key ->
let children = sync_children ctx dom false Element.null children e2.children in
sync_attributes ctx e1.ns dom e1.attributes e2.attributes;
BElement {vdom; dom; children}
| _ ->
let x = blit ~parent ctx vdom in
let rec loop l1 l2 =
match l1, l2 with
| h1::t1, h2::t2 ->
replace_child parent h1 h2;
loop t1 t2
| [], l ->
List.iter (remove_child parent) l
| l, [] ->
List.iter (fun c -> insert_before parent c next) l
in
loop (get_doms x) (get_doms old);
dispose old;
x
and sync_children : type old_msg msg. ctx -> Element.t -> bool -> Element.t -> old_msg ctrl list -> msg vdom list -> msg ctrl list =
fun ctx dom prev_move next old_children new_children ->
let old_children = Array.of_list old_children in
let new_children = Array.of_list new_children in
let by_key = Hashtbl.create 8 in
for i = Array.length old_children - 1 downto 0 do
let k = key_of_vdom (get_vdom old_children.(i)) in
Hashtbl.add by_key k i
done;
let indices = Array.make (Array.length new_children) (-1) in
for i = 0 to Array.length indices - 1 do
let k = key_of_vdom new_children.(i) in
match Hashtbl.find by_key k with
| exception Not_found -> ()
| idx ->
indices.(i) <- idx;
Hashtbl.remove by_key k;
done;
Hashtbl.iter
(fun _ i ->
if debug then Printf.printf "remove %i\n%!" i;
let to_remove = old_children.(i) in
List.iter (remove_child dom) (get_doms to_remove);
dispose to_remove
)
by_key;
let ctrls = ref [] in
let prev_move = ref prev_move in
let next = ref next in
for i = Array.length new_children - 1 downto 0 do
let idx = indices.(i) in
if debug then Printf.printf "old = %i; new = %i: " idx i;
let c =
if idx < 0 then begin
if debug then Printf.printf "create\n%!";
blit ~parent:dom ctx new_children.(i)
end
else begin
if debug then Printf.printf "sync&move\n%!";
sync ctx dom !prev_move !next old_children.(idx) new_children.(i)
end
in
let doms = get_doms c in
let rec last = function
| [] -> None
| [ c_dom ] -> Some c_dom
| _ :: tl -> last tl
in
match last doms with
| None -> ()
| Some right_most ->
begin
let move =
idx < 0 ||
((if i = Array.length new_children - 1 then idx <> Array.length old_children - 1
else !prev_move || indices.(i + 1) <> idx + 1)
&& Element.next_sibling right_most != !next)
in
if move then begin
if debug then Printf.printf "really move\n%!";
List.iter (fun c_dom ->
insert_before dom c_dom !next) doms;
end;
prev_move := move;
next := List.hd doms;
end;
ctrls := c :: !ctrls
done;
!ctrls
let sync ctx parent old vdom =
try sync ctx parent old vdom
with exn ->
Printf.printf "Error during vdom sync: %s\n%!" (Printexc.to_string exn);
raise exn
type 'msg find =
| NotFound
| Found: {mapper: ('inner_msg -> 'msg); inner: 'inner_msg ctrl; parent: 'msg find} -> 'msg find
let rec found: type inner_msg msg. (inner_msg -> msg) -> msg find -> Element.t -> inner_msg ctrl -> msg find =
fun mapper parent dom -> function
| BElement _ | BText _ | BCustom _ as inner -> Found {mapper; inner; parent}
| BFragment {children; _} ->
begin match List.find (fun c -> List.memq dom (get_doms c)) children with
| exception Not_found -> assert false
| c -> found mapper parent dom c
end
| BMap {f; child; _} -> found (fun x -> mapper (f x)) parent dom child
| BMemo {child; _} -> found mapper parent dom child
let rec vdom_of_dom: type msg. msg ctrl -> Element.t -> msg find = fun root dom ->
match Ojs.option_of_js Element.t_of_js (Element.t_to_js dom) with
| None -> NotFound
| Some dom when List.memq dom (get_doms root) ->
found Fun.id NotFound dom root
| Some dom ->
begin match vdom_of_dom root (Element.parent_node dom) with
| NotFound -> NotFound
| Found {mapper; inner = BElement {children; _}; _} as parent ->
begin match List.find (fun c -> List.memq dom (get_doms c)) children with
| exception Not_found -> NotFound
| c -> found mapper parent dom c
end
| Found {mapper; inner = BCustom {propagate_events; _} as inner; parent} ->
if propagate_events then
found mapper parent dom inner
else
NotFound
| _ -> assert false
end
type ('model, 'msg) app = {
dom: Js_browser.Element.t;
process: ('msg -> unit);
get: (unit -> 'model);
after_redraw: (unit -> unit) -> unit;
dispose: (unit -> unit);
}
let dom x = x.dom
let process x = x.process
let get x = x.get ()
let after_redraw x = x.after_redraw
let run (type msg model) ?(env = empty) ?container
({init = (model0, cmd0); update; view} : (model, msg) Vdom.app) =
let env = merge [env; !global] in
let container_created, container =
match container with
| None -> true, Document.create_element document "div"
| Some container -> false, container
in
let post_redraw = ref [] in
let after_redraw f = post_redraw := f :: !post_redraw in
let flush _ =
let l = List.rev !post_redraw in
post_redraw := [];
List.iter (fun f -> f ()) l
in
let process_custom_fwd = ref (fun _ _ -> assert false) in
let process_fwd = ref (fun _ -> assert false) in
let listeners = Hashtbl.create 8 in
let model = ref model0 in
let current = ref None in
let pending_redraw = ref false in
let view model =
try view model
with exn ->
Printf.printf "Error during vdom view: %s\n%!" (Printexc.to_string exn);
raise exn
in
let on_event evt =
let ty = Event.type_ evt in
try
let tgt = Element.t_of_js (Event.target evt) in
let apply_handler dom =
List.filter_map (fun attribute ->
match attribute with
| Handler (Decoder {event_type; decoder; map}) when ty = event_type ->
let {msg; prevent_default; stop_propagation} =
BDecoder.decode_fail
~extra_fields:["currentTarget", Element.t_to_js dom]
decoder
(Event.t_to_js evt)
in
if prevent_default then Event.prevent_default evt;
if stop_propagation then Event.stop_propagation evt;
begin match map msg with
| None -> None
| Some msg ->
Some (msg, stop_propagation)
end
| _ -> None)
in
let rec propagate = function
| Found {
mapper;
inner = ( BElement {vdom = Element {attributes; _}; dom; _}
| BCustom {vdom = Custom {attributes; _}; elt = {dom; _}; _} );
parent;
} ->
let stop_propagation =
List.fold_left
(fun stopped_propagation (msg, stop_propagation) ->
!process_fwd (mapper msg);
stopped_propagation || stop_propagation)
false
(apply_handler dom attributes)
in
if not stop_propagation then propagate parent
| _ ->
()
in
Option.iter (fun root ->
propagate (vdom_of_dom root tgt);
) !current;
if ty = "input" || ty = "blur" then
let f () =
Option.iter
(fun root ->
match vdom_of_dom root tgt with
| Found {mapper = _; inner = BElement {vdom = Element {attributes; _}; _}; _} ->
List.iter
(function
| Property ("value", String s2) when s2 <> Element.value tgt -> Element.set_value tgt s2
| Property ("checked", Bool s2) -> Element.set_checked tgt s2
| _ -> ()
)
attributes
| _ -> ()
) !current
in
if !pending_redraw then after_redraw f else f ()
with exn ->
Printf.printf "Error in event handler %S: %s\n%!" ty (Printexc.to_string exn)
in
let add_listener event_type =
if not (Hashtbl.mem listeners event_type) then begin
if debug then Printf.printf "Adding listener for %s\n%!" event_type;
let remove_listener = Element.add_cancellable_event_listener container (Event.NonStandard event_type) on_event true in
Hashtbl.add listeners event_type remove_listener
end
in
let ctx =
{
process_custom = (fun elt evt -> !process_custom_fwd elt evt);
custom_handlers = env.customs;
after_redraw;
add_listener;
}
in
let x = blit ~parent:container ctx (view model0) in
Window.request_animation_frame window flush;
current := Some x;
let redraw _ =
match !current with
| None -> ()
| Some root ->
pending_redraw := false;
let x = sync ctx container false Element.null root (view !model) in
current := Some x;
flush ()
in
let rec process msg =
try
let (new_model : model), (cmd : msg Vdom.Cmd.t) = update !model msg in
model := new_model;
run_cmd container cmd;
if not !pending_redraw then begin
pending_redraw := true;
Window.request_animation_frame window redraw
end
with exn ->
Printf.printf "Error during vdom process: %s\n%!" (Printexc.to_string exn);
raise exn
and run_cmd (parent : Js_browser.Element.t) cmd =
Cmd.run after_redraw (env.cmds @ (!global).cmds) process parent cmd
in
process_fwd := process;
List.iter (Element.append_child container) (get_doms x);
let process_custom tgt event =
Option.iter
(fun root ->
let process mapper attributes =
let select_handler attr =
match attr, event with
| Handler (Decoder {decoder; map; event_type = et1}), Custom.Encoded {encoder; event_type = et2} when et1 = et2 ->
let {msg; _} = BDecoder.decode_fail decoder (Encoder.encode encoder) in map msg
| Handler (CustomEvent f), Custom e -> f e
| _ -> None
in
let msgs = List.filter_map select_handler attributes in
List.iter (fun msg -> process (mapper msg)) msgs
in
begin match vdom_of_dom root tgt with
| Found {mapper; inner = BElement {vdom = Element {attributes; _}; _}; _} ->
process mapper attributes
| Found {mapper; inner = BCustom {vdom = Custom {attributes; _}; _}; _} ->
process mapper attributes
| _ ->
()
end
) !current
in
process_custom_fwd := process_custom;
run_cmd container cmd0;
let dispose () =
Option.iter
(fun root ->
current := None;
dispose root;
Hashtbl.iter (fun _ f -> f ()) listeners;
if container_created then
Element.remove container
else
Element.set_inner_HTML container ""
) !current
in
{dom = container; process; get = (fun () -> !model); after_redraw; dispose}
let dispose {dispose; _} = dispose ()