├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE.md ├── LICENSE.virtual-dom ├── Makefile ├── README.md ├── THIRD-PARTY.txt ├── css_gen ├── README.md ├── dune └── src │ ├── css_gen.ml │ ├── css_gen.mli │ ├── css_parser.ml │ ├── css_parser.mli │ ├── css_tokenizer.ml │ ├── css_tokenizer.mli │ └── dune ├── dune ├── dune-project ├── example └── terminal │ ├── dune │ ├── example.html │ ├── terminal.ml │ └── terminal.mli ├── html5_history ├── dune ├── html5_history.ml └── html5_history.mli ├── input_widgets ├── README.md ├── example │ ├── README.md │ ├── dune │ ├── example.ml │ ├── example.mli │ ├── index.html │ └── jane-web-style.html ├── src │ ├── dune │ ├── import.ml │ ├── vdom_input_widgets.ml │ ├── vdom_input_widgets.mli │ └── vdom_input_widgets_intf.ml └── test │ ├── dune │ ├── test_datetime_local.ml │ ├── test_datetime_local.mli │ └── vdom_input_widgets_test.ml ├── jsdom_test ├── dune ├── test_hook_on_mount.ml ├── test_hook_on_mount.mli └── virtual_dom_jsdom_test.ml ├── jsoo_weak_collections ├── src │ ├── dune │ ├── gen_js_api.ml │ ├── jsoo_weak_collections.ml │ ├── weak_map.ml │ ├── weak_map.mli │ ├── weak_set.ml │ └── weak_set.mli └── test │ ├── dune │ └── jsoo_weak_collections_test.ml ├── keyboard ├── src │ ├── dune │ ├── grouped_help_text.ml │ ├── grouped_help_text.mli │ ├── help_text.ml │ ├── help_text.mli │ ├── import.ml │ ├── keyboard_event.ml │ ├── keyboard_event.mli │ ├── keyboard_event_handler.ml │ ├── keyboard_event_handler.mli │ ├── keystroke.ml │ ├── keystroke.mli │ ├── variable_keyboard_event_handler.ml │ ├── variable_keyboard_event_handler.mli │ └── vdom_keyboard.ml └── test │ ├── dune │ ├── test_vdom_keyboard.ml │ └── test_vdom_keyboard.mli ├── layout ├── dune ├── vdom_layout.ml └── vdom_layout.mli ├── lib ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── vendor │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── create-element.js │ ├── diff.js │ ├── docs.jsig │ ├── docs │ │ ├── README.md │ │ ├── css-animations.md │ │ ├── faq.md │ │ ├── hooks.md │ │ ├── thunk.md │ │ ├── vnode.md │ │ ├── vtext.md │ │ └── widget.md │ ├── h.js │ ├── index.js │ ├── package.json │ ├── patch.js │ ├── vdom │ │ ├── README.md │ │ ├── apply-properties.js │ │ ├── create-element.js │ │ ├── dom-index.js │ │ ├── patch-op.js │ │ ├── patch.js │ │ └── update-widget.js │ ├── virtual-hyperscript │ │ ├── README.md │ │ ├── hooks │ │ │ ├── attribute-hook.js │ │ │ ├── ev-hook.js │ │ │ ├── focus-hook.js │ │ │ └── soft-set-hook.js │ │ ├── index.js │ │ ├── parse-tag.js │ │ ├── svg-attribute-namespace.js │ │ └── svg.js │ ├── vnode │ │ ├── handle-thunk.js │ │ ├── is-thunk.js │ │ ├── is-vhook.js │ │ ├── is-vnode.js │ │ ├── is-vtext.js │ │ ├── is-widget.js │ │ ├── version.js │ │ ├── vnode.js │ │ ├── vpatch.js │ │ └── vtext.js │ └── vtree │ │ ├── README.md │ │ ├── diff-props.js │ │ └── diff.js ├── virtualdom.compiled.pretty.js └── virtualdom.js ├── src ├── attr.ml ├── attr.mli ├── dom_float.ml ├── dom_float.mli ├── dune ├── effect.ml ├── effect.mli ├── global_listeners.ml ├── global_listeners.mli ├── hooks.js ├── hooks.ml ├── hooks.mli ├── hooks_intf.ml ├── js_map.ml ├── js_map.mli ├── node.ml ├── node.mli ├── on_mount.ml ├── on_mount.mli ├── raw.ml ├── raw.mli ├── thunk.js ├── thunk.ml ├── thunk.mli ├── vdom.ml ├── vdom_node_with_map_children.ml ├── vdom_node_with_map_children.mli └── virtual_dom.ml ├── svg └── src │ ├── attr.ml │ ├── attr.mli │ ├── dune │ ├── node.ml │ ├── node.mli │ ├── virtual_dom_svg.ml │ └── virtual_dom_svg.mli ├── test ├── dune ├── helpers │ ├── dune │ ├── handler.ml │ ├── handler.mli │ ├── node_helpers.ml │ ├── node_helpers.mli │ └── virtual_dom_test_helpers.ml ├── import.ml ├── test.ml ├── test.mli ├── test_attr_multi.ml ├── test_attr_multi.mli ├── test_linter.ml ├── test_linter.mli ├── test_patch.ml ├── test_patch.mli ├── test_selector.ml ├── test_selector.mli ├── test_to_string.ml ├── test_to_string.mli ├── test_unmerged_warning_mode.ml ├── test_unmerged_warning_mode.mli ├── test_vdom_attr_lazy.ml ├── test_vdom_attr_lazy.mli ├── trigger.ml ├── trigger.mli └── virtual_dom_test.ml ├── tyxml ├── dune ├── gen_js_api.ml ├── tyxml_f.ml ├── virtual_dom_tyxml.ml └── virtual_dom_tyxml.mli ├── ui_effect ├── dune ├── ui_effect.ml ├── ui_effect.mli └── ui_effect_intf.ml ├── ui_effect_of_deferred ├── dune ├── ui_effect_of_deferred.ml └── ui_effect_of_deferred.mli └── virtual_dom.opam /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.install 3 | *.merlin 4 | _opam 5 | 6 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | profile=janestreet 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Release v0.17.0 2 | - css_gen 3 | - Generalize `css_global_values` to be polymorphic with a new variant for CSS variables. 4 | - Add `equal` and `sexp_grammar` derivations to various types including 5 | `css_global_values`, `Color.RGBA.t`, `Color.HSLA`, and `Color.t`. 6 | - Introduce `LCHA` module with a `create` function for LCH color representation. 7 | - Add `LCHA` type to `Color.t` variant. 8 | - Update `Color.t` variant to support `css_global_values` with type `t`. 9 | - Modify `Length` type to include a parameterized `css_global_values` type. 10 | - Add `equal` derivation to the `t` type in both `Css_gen` module and `Stable.V1` module. 11 | - vdom_input_widgets 12 | - Updates to `Checklist` module: 13 | - Renamed `extra_attrs` to `extra_container_attrs` in both `Checklist.of_values` and 14 | `Checklist.of_enum` functions 15 | - Added `extra_checkbox_attrs` parameter to both `Checklist.of_values` and 16 | `Checklist.of_enum` functions for customization based on checkbox state 17 | - Updates to `Entry` module: 18 | - Added an optional `key` parameter to the `Entry` function 19 | - Introduced `allow_updates_when_focused` parameter with options `Always` or `Never` 20 | to control updates when focused 21 | - Updates to `Radio_buttons` module: 22 | - Renamed `extra_attrs` to `extra_container_attrs` in both `Radio_buttons.of_values` 23 | and `Radio_buttons.of_values_horizontal` functions 24 | - Added `extra_button_attrs` parameter to both `Radio_buttons.of_values` and 25 | `Radio_buttons.of_values_horizontal` functions for customization based on button 26 | checked state 27 | - `Vdom.Global_listeners.beforeunload` function now supports a new effect option 28 | `Custom_best_effort` This option may or may not execute to completion upon tab close in 29 | Chrome 30 | - Vdom.Node 31 | - Add new variant and functions: 32 | - `Node.t` type now includes a `Fragment` variant to support lists of nodes. 33 | - `Node.fragment` function added to create a node from a list of nodes. 34 | - Extend `Node` module with new node creator functions: 35 | - `Node.b` 36 | - `Node.dialog` 37 | - `Node.small` 38 | - `Node.kbd` 39 | - `Node.form` 40 | 41 | ## Release v0.16.0 42 | 43 | The most significant change in this release is that most functions that create 44 | `Vdom.Node.t` have changed from taking a single attribute via a `?attr` 45 | argument to now take a list of attributes via the `?attrs` argument. This 46 | change was made to reduce the boilerplate at each callsite of invoking 47 | `Vdom.Attr.many` to add more than one attribute. 48 | 49 | A straightforward way to upgrade code to the new interface, without changing 50 | behavior, is to rename `~attr` to `~attrs` and wrap the argument into a 51 | singlton list. Additionally, in the case where the argument was an immediate 52 | invocation of `Vdom.Attr.many`, you can just remove that call and pass the list 53 | directly. 54 | 55 | We've applied this interface change to the main `virtual_dom` library, as well 56 | as `vdom_layout`. However, we took a different approach with 57 | `vdom_input_widgets`, since functions in that module already take a list of 58 | attributes, but they do not merge the attributes in those lists. We've added a 59 | `?merge_behavior` argument to those functions. To completely preserve behavior, 60 | you should pass `Legacy_dont_merge`; otherwise, the default `Merge` behavior 61 | will attempt to concatenate any styles or lists of classes in the attributes. 62 | 63 | More minor changes include: 64 | 65 | - `css_gen` changes: 66 | * Added `text_align` and `content_alignment` types. 67 | * Added `line_height`, `row_gap`, and `column_gap` functions. 68 | * `flex_container` now accepts `` `Default `` for the `?wrap` and 69 | `?direction` arguments. It also now accepts some new arguments: 70 | `?align_content`, `?row_gap`, and `?column_gap`. 71 | - Added new `html5_history` library, which provides a layer on top of the 72 | browser's history API. 73 | - Added `Vdom_keyboard.Keyboard_event_handler.get_action`. 74 | - `vdom_input_widgets` changes: 75 | * The `Dropdown` module now accepts `?extra_option_attrs` and `?placeholder` arguments. 76 | * The `on_toggle` argument to `Checkbox.simple` is now a plain effect instead 77 | of a function, since the function was expected to be pure anyway. 78 | * Renamed `Radio_buttons.Style` module to `Selectable_style`. 79 | * The `Checklist` module now accepts an argument `Selectable_style.t` argument. 80 | * Added `Entry.password`. 81 | - `virtual_dom` changes: 82 | * Added more attributes creation functions to the `Attr` module. 83 | * Added more event listener functions to the `Attr.Global_listeners` module. 84 | * Added `Node.Widget.to_vdom_for_testing`. 85 | * Added `Lazy` constructor to `Node.t`. 86 | * Added more node-creation functions to the `Node` module. 87 | * Added `Node.lazy_`, a function that allows for writing code to compute 88 | virtual-dom nodes that only runs if necessary. 89 | * `Node.input` no longer accepts a list of child nodes. Users must either 90 | stop passing the list, or switch to using the behavior-preserving 91 | `Node.input_deprecated` function. 92 | - Added `Virtual_dom_svg.Attr.stroke_dashoffset`. 93 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This repository contains open source software that is developed and 2 | maintained by [Jane Street][js]. 3 | 4 | Contributions to this project are welcome and should be submitted via 5 | GitHub pull requests. 6 | 7 | Signing contributions 8 | --------------------- 9 | 10 | We require that you sign your contributions. Your signature certifies 11 | that you wrote the patch or otherwise have the right to pass it on as 12 | an open-source patch. The rules are pretty simple: if you can certify 13 | the below (from [developercertificate.org][dco]): 14 | 15 | ``` 16 | Developer Certificate of Origin 17 | Version 1.1 18 | 19 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 20 | 1 Letterman Drive 21 | Suite D4700 22 | San Francisco, CA, 94129 23 | 24 | Everyone is permitted to copy and distribute verbatim copies of this 25 | license document, but changing it is not allowed. 26 | 27 | 28 | Developer's Certificate of Origin 1.1 29 | 30 | By making a contribution to this project, I certify that: 31 | 32 | (a) The contribution was created in whole or in part by me and I 33 | have the right to submit it under the open source license 34 | indicated in the file; or 35 | 36 | (b) The contribution is based upon previous work that, to the best 37 | of my knowledge, is covered under an appropriate open source 38 | license and I have the right under that license to submit that 39 | work with modifications, whether created in whole or in part 40 | by me, under the same open source license (unless I am 41 | permitted to submit under a different license), as indicated 42 | in the file; or 43 | 44 | (c) The contribution was provided directly to me by some other 45 | person who certified (a), (b) or (c) and I have not modified 46 | it. 47 | 48 | (d) I understand and agree that this project and the contribution 49 | are public and that a record of the contribution (including all 50 | personal information I submit with it, including my sign-off) is 51 | maintained indefinitely and may be redistributed consistent with 52 | this project or the open source license(s) involved. 53 | ``` 54 | 55 | Then you just add a line to every git commit message: 56 | 57 | ``` 58 | Signed-off-by: Joe Smith 59 | ``` 60 | 61 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 62 | 63 | If you set your `user.name` and `user.email` git configs, you can sign 64 | your commit automatically with git commit -s. 65 | 66 | [dco]: http://developercertificate.org/ 67 | [js]: https://opensource.janestreet.com/ 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016--2025 Jane Street Group, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.virtual-dom: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt-Esch. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INSTALL_ARGS := $(if $(PREFIX),--prefix $(PREFIX),) 2 | 3 | default: 4 | dune build 5 | 6 | install: 7 | dune install $(INSTALL_ARGS) 8 | 9 | uninstall: 10 | dune uninstall $(INSTALL_ARGS) 11 | 12 | reinstall: uninstall install 13 | 14 | clean: 15 | dune clean 16 | 17 | .PHONY: default install uninstall reinstall clean 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | "Virtual_dom: a virtual DOM diffing library" 2 | ============================================ 3 | 4 | This library is an OCaml wrapper of Matt Esch's [virtual-dom library](https://github.com/Matt-Esch/virtual-dom). 5 | It provides a simple, immutable representation of a desired state of 6 | the DOM, as well as primitives for updating the real DOM in the 7 | browser to match that, both by slamming the entire DOM in place, and 8 | by computing diffs between successive virtual-DOMs, and applying the 9 | resulting patch to the real DOM. 10 | 11 | It has been vendored internally as the external library has gone unsupported for many 12 | years and we needed some modifications. 13 | 14 | # Contributing 15 | 16 | Please exercise a high degree of caution if you modify this library. Our testing 17 | infrastructure doesn't support robust browser testing, so any changes here should be 18 | _thoroughly_ smoke tested on a variety of apps with different usecases and potential DOM 19 | ~ab~uses. We intend this code to be as stable/unchanging as possible because of the 20 | difficult testing situation and near ubiquitous use in web apps at Jane Street. 21 | -------------------------------------------------------------------------------- /THIRD-PARTY.txt: -------------------------------------------------------------------------------- 1 | The repository contains 3rd-party code in the following locations and 2 | under the following licenses: 3 | 4 | - virtual_dom/lib/virtualdom.compiled.js: License can be found in 5 | LICENSE.virtual-dom 6 | -------------------------------------------------------------------------------- /css_gen/README.md: -------------------------------------------------------------------------------- 1 | # Css_gen 2 | 3 | This library provides a nicely typed wrapper around CSS fields. 4 | -------------------------------------------------------------------------------- /css_gen/dune: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janestreet/virtual_dom/9b221deb066ff104fb1a37b33985c0964f426bf6/css_gen/dune -------------------------------------------------------------------------------- /css_gen/src/css_parser.mli: -------------------------------------------------------------------------------- 1 | open Core 2 | 3 | val validate_value : string -> unit Or_error.t 4 | val parse_declaration_list : string -> (string * string) list Or_error.t 5 | -------------------------------------------------------------------------------- /css_gen/src/css_tokenizer.mli: -------------------------------------------------------------------------------- 1 | (** A css3 tokenizer. 2 | 3 | See section 4.3 on this page: https://www.w3.org/TR/css-syntax-3/ 4 | 5 | Differences to the standard: 6 | - This does not implement BAD_STRING, BAD_URI, BAD_COMMENT That is we are not 7 | implementing any support for robustness in the face of bad input. Rationale is we 8 | rather learn that we wrote invalid CSS. 9 | - This does not support escape sequence outside of strings. 10 | - We didn't do anything to validate the unicode support. 11 | 12 | Also we generally only implement what we need to parse declaration lists. Not 13 | everything needed to parse complete css style sheets (e.g. cdo, cdc). *) 14 | open! Core 15 | 16 | type t 17 | 18 | val create : string -> t 19 | 20 | module Token : sig 21 | type t = 22 | | Ident 23 | | Function 24 | | Atkeyword 25 | | Hash 26 | | String 27 | | Uri 28 | | Delim 29 | | Number 30 | | Percentage 31 | | Dimension 32 | | White_space 33 | | Colon 34 | | Semi_colon 35 | | Comma 36 | | Lbracket 37 | | Rbracket 38 | | Lparen 39 | | Rparen 40 | | Lcurly 41 | | Rcurly 42 | | Comment 43 | | Eof 44 | | Error 45 | [@@deriving sexp] 46 | 47 | val equal : t -> t -> bool 48 | end 49 | 50 | val current : t -> Token.t 51 | 52 | (** Start and len of the current token. The eof token starts at String.length and has 53 | size 0. *) 54 | val slice : t -> int * int 55 | 56 | (** The textual representation of the current token. Note that it is exactly as given in 57 | the source. *) 58 | val current_text : t -> string 59 | 60 | (** source (create s) = s *) 61 | val source : t -> string 62 | 63 | (** Advance to the next token. Idempotent if the current token is eof. *) 64 | val next : t -> unit 65 | -------------------------------------------------------------------------------- /css_gen/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name css_gen) 3 | (public_name virtual_dom.css_gen) 4 | (libraries core) 5 | (preprocess 6 | (pps ppx_jane))) 7 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janestreet/virtual_dom/9b221deb066ff104fb1a37b33985c0964f426bf6/dune -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.17) 2 | -------------------------------------------------------------------------------- /example/terminal/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (modes byte exe) 3 | (names terminal) 4 | (libraries virtual_dom core js_of_ocaml sexplib) 5 | (preprocess 6 | (pps js_of_ocaml-ppx ppx_jane))) 7 | -------------------------------------------------------------------------------- /example/terminal/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/terminal/terminal.ml: -------------------------------------------------------------------------------- 1 | open Js_of_ocaml 2 | open Virtual_dom 3 | open Core 4 | open Vdom 5 | 6 | class type terminal = object 7 | method get_command_ : Js.js_string Js.t Js.meth 8 | method echo : Js.js_string Js.t -> unit Js.meth 9 | end 10 | 11 | let terminal = 12 | let id = Type_equal.Id.create ~name:"terminal-id" Sexplib.Conv.sexp_of_opaque in 13 | fun () -> 14 | let values = String.Table.create () in 15 | let interpret command (term : terminal Js.t) = 16 | match String.split ~on:' ' (Js.to_string command) with 17 | | [ "set"; key; data ] -> Hashtbl.set values ~key ~data 18 | | [ "get"; key ] -> 19 | let data = Hashtbl.find values key in 20 | term##echo (Js.string (sprintf !"%{sexp:string option}" data)) 21 | | _ -> term##echo (Js.string "Invalid command") 22 | in 23 | let completion term _command k = 24 | Js.Unsafe.global##.term := term; 25 | let command = term##get_command_ in 26 | Console.console##log command; 27 | let completions = 28 | match String.split ~on:' ' (Js.to_string command) with 29 | | [ prefix ] -> List.filter [ "get"; "set" ] ~f:(String.is_prefix ~prefix) 30 | | [ "get"; key ] -> 31 | List.filter (Hashtbl.keys values) ~f:(String.is_prefix ~prefix:key) 32 | | _ -> [] 33 | in 34 | let completions = Js.array (Array.of_list_rev_map ~f:Js.string completions) in 35 | Js.Unsafe.(fun_call k [| inject completions |]) 36 | in 37 | let options = 38 | object%js 39 | val prompt = Js.string "> " 40 | val completion = Js.wrap_callback completion 41 | val exit = Js._false 42 | val clear = Js._false 43 | end 44 | in 45 | let init () = 46 | let div : < terminal : _ -> _ -> unit Js.meth ; get : int -> _ Js.meth > Js.t = 47 | Js.Unsafe.global##jQuery (Js.string "
") 48 | in 49 | div##terminal (Js.wrap_callback interpret) options; 50 | (), div##get 0 51 | in 52 | Node.widget ~init ~id () 53 | ;; 54 | 55 | let view count = Node.div [ Node.div [ Node.text (Int.to_string count) ]; terminal () ] 56 | 57 | let () = 58 | Dom_html.window##.onload 59 | := Dom.handler (fun _ -> 60 | let count = ref 0 in 61 | let vdom = ref (view !count) in 62 | let elt = Node.to_dom !vdom in 63 | Dom.appendChild Dom_html.document##.body elt; 64 | Dom_html.window##setInterval 65 | (Js.wrap_callback (fun _ -> 66 | incr count; 67 | let current = view !count in 68 | let patch = Node.Patch.create ~previous:!vdom ~current in 69 | vdom := current; 70 | Node.Patch.apply patch elt |> (ignore : Dom_html.element Js.t -> unit))) 71 | (Js.float 30.) 72 | |> (ignore : Dom_html.interval_id -> unit); 73 | Js._false) 74 | ;; 75 | -------------------------------------------------------------------------------- /example/terminal/terminal.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /html5_history/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name html5_history) 3 | (public_name virtual_dom.html5_history) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries core js_of_ocaml core_kernel.bus base64 uri)) 7 | -------------------------------------------------------------------------------- /input_widgets/README.md: -------------------------------------------------------------------------------- 1 | # `Vdom_input_widgets` 2 | 3 | This is a collection of simple widgets that correspond to a single or small 4 | collection of `` that helps map the values into arbitrary types so that 5 | you don't have to worry about serializing and deserializing. It also correctly 6 | responds to upstream changes in the model by updating the property on the input. 7 | -------------------------------------------------------------------------------- /input_widgets/example/README.md: -------------------------------------------------------------------------------- 1 | To test this example, run `hostname; python -m SimpleHTTPServer` from this 2 | directory and open a browser to the url `http://$YOUR_HOSTNAME:8000/`. 3 | -------------------------------------------------------------------------------- /input_widgets/example/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (modes byte exe) 3 | (names example) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries async_kernel core css_gen incr_dom js_of_ocaml vdom_input_widgets)) 7 | -------------------------------------------------------------------------------- /input_widgets/example/example.mli: -------------------------------------------------------------------------------- 1 | (* only javascript file *) 2 | -------------------------------------------------------------------------------- /input_widgets/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /input_widgets/example/jane-web-style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /input_widgets/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name vdom_input_widgets) 3 | (public_name virtual_dom.input_widgets) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries core css_gen js_of_ocaml virtual_dom)) 7 | -------------------------------------------------------------------------------- /input_widgets/src/import.ml: -------------------------------------------------------------------------------- 1 | module Attr = Virtual_dom.Vdom.Attr 2 | module Js = Js_of_ocaml.Js 3 | module Node = Virtual_dom.Vdom.Node 4 | module Vdom = Virtual_dom.Vdom 5 | -------------------------------------------------------------------------------- /input_widgets/src/vdom_input_widgets_intf.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | 3 | module type Display = sig 4 | type t 5 | 6 | val to_string : t -> string 7 | end 8 | 9 | module type Equal = sig 10 | type t [@@deriving equal] 11 | 12 | include Display with type t := t 13 | end 14 | 15 | module type Enum = sig 16 | type t [@@deriving equal, enumerate] 17 | 18 | include Display with type t := t 19 | end 20 | 21 | module type Set = sig 22 | type t 23 | type comparator_witness 24 | 25 | include Comparator.S with type t := t and type comparator_witness := comparator_witness 26 | include Display with type t := t 27 | end 28 | 29 | module type Enum_set = sig 30 | type t [@@deriving enumerate] 31 | type comparator_witness 32 | 33 | include Comparator.S with type t := t and type comparator_witness := comparator_witness 34 | include Enum with type t := t 35 | end 36 | -------------------------------------------------------------------------------- /input_widgets/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name vdom_input_widgets_test) 3 | (libraries core expect_test_helpers_core virtual_dom vdom_input_widgets 4 | virtual_dom_test_helpers) 5 | (preprocess 6 | (pps ppx_jane))) 7 | -------------------------------------------------------------------------------- /input_widgets/test/test_datetime_local.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Vdom_input_widgets 3 | 4 | let%expect_test "check that datetime_local parses input time correctly" = 5 | let time = Time_ns.of_string "2025-01-15 13:30:10Z" in 6 | let last_input_value = ref (Some time) in 7 | let on_input value = 8 | last_input_value := value; 9 | Virtual_dom.Vdom.Effect.Ignore 10 | in 11 | let node = Entry.datetime_local ~value:(Some time) ~on_input () in 12 | let node_helper = Virtual_dom_test_helpers.Node_helpers.unsafe_convert_exn node in 13 | print_s [%message (last_input_value : Time_ns.t option ref)]; 14 | [%expect {| (last_input_value ((2025-01-15 08:30:10.000000000-05:00))) |}]; 15 | let update_time_and_show ~text = 16 | Virtual_dom_test_helpers.Node_helpers.User_actions.input_text node_helper ~text; 17 | print_s [%message (last_input_value : Time_ns.t option ref)] 18 | in 19 | update_time_and_show ~text:"2025-01-15T13:31"; 20 | [%expect {| (last_input_value ((2025-01-15 08:31:00.000000000-05:00))) |}]; 21 | update_time_and_show ~text:"invalid input"; 22 | [%expect {| (last_input_value ()) |}]; 23 | update_time_and_show ~text:"2025-01-15T13:30:20"; 24 | [%expect {| (last_input_value ((2025-01-15 08:30:20.000000000-05:00))) |}]; 25 | update_time_and_show ~text:"2025-01-15T13:30:20.123"; 26 | [%expect {| (last_input_value ((2025-01-15 08:30:20.123000000-05:00))) |}]; 27 | update_time_and_show ~text:"2025-01-15T13:30:20.123.123"; 28 | [%expect {| (last_input_value ()) |}] 29 | ;; 30 | -------------------------------------------------------------------------------- /input_widgets/test/test_datetime_local.mli: -------------------------------------------------------------------------------- 1 | (** Intentionally left blank *) 2 | -------------------------------------------------------------------------------- /input_widgets/test/vdom_input_widgets_test.ml: -------------------------------------------------------------------------------- 1 | module Test_datetime_local = Test_datetime_local 2 | -------------------------------------------------------------------------------- /jsdom_test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom_jsdom_test) 3 | (libraries async_js async_kernel bonsai_web bonsai_web_test 4 | bonsai_web_components.web_ui_toplayer bonsai_web_components.byo_toplayer 5 | capitalization core default_vdom_spec ppx_expect.config_types 6 | expect_test_helpers_core patdiff.expect_test_patdiff js_of_ocaml 7 | bonsai_web_test.jsdom testable_timeout vdom_keyboard 8 | virtual_dom_test_helpers with) 9 | (preprocess 10 | (pps ppx_jane bonsai.ppx_bonsai js_of_ocaml-ppx ppx_html))) 11 | -------------------------------------------------------------------------------- /jsdom_test/test_hook_on_mount.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /jsdom_test/virtual_dom_jsdom_test.ml: -------------------------------------------------------------------------------- 1 | module Test_hook_on_mount = Test_hook_on_mount 2 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name jsoo_weak_collections) 3 | (public_name virtual_dom.jsoo_weak_collections) 4 | (libraries js_of_ocaml gen_js_api) 5 | (preprocess 6 | (pps gen_js_api.ppx))) 7 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/gen_js_api.ml: -------------------------------------------------------------------------------- 1 | module Ojs = Ojs 2 | module Ojs_exn = Ojs_exn 3 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/jsoo_weak_collections.ml: -------------------------------------------------------------------------------- 1 | module Weak_map = Weak_map 2 | module Weak_set = Weak_set 3 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/weak_map.ml: -------------------------------------------------------------------------------- 1 | [@@@js.dummy "!! This code has been generated by gen_js_api !!"] 2 | [@@@ocaml.warning "-7-32-39"] 3 | 4 | open! Js_of_ocaml 5 | open! Gen_js_api 6 | 7 | type ('a, 'b) t = Ojs.t 8 | 9 | let rec t_of_js : 'a 'b. (Ojs.t -> 'a) -> (Ojs.t -> 'b) -> Ojs.t -> ('a, 'b) t = 10 | fun (type __a __b) (__a_of_js : Ojs.t -> __a) (__b_of_js : Ojs.t -> __b) (x2 : Ojs.t) -> 11 | x2 12 | 13 | and t_to_js : 'a 'b. ('a -> Ojs.t) -> ('b -> Ojs.t) -> ('a, 'b) t -> Ojs.t = 14 | fun (type __a __b) (__a_to_js : __a -> Ojs.t) (__b_to_js : __b -> Ojs.t) (x1 : Ojs.t) -> 15 | x1 16 | ;; 17 | 18 | let create : unit -> ('a, 'b) t = 19 | fun () -> 20 | t_of_js Obj.magic Obj.magic (Ojs.new_obj (Ojs.get_prop_ascii Ojs.global "WeakMap") [||]) 21 | ;; 22 | 23 | let set : ('a, 'b) t -> 'a -> 'b -> unit = 24 | fun (x7 : ('a, 'b) t) (x5 : 'a) (x6 : 'b) -> 25 | (ignore : _) 26 | (Ojs.call (t_to_js Obj.magic Obj.magic x7) "set" [| Obj.magic x5; Obj.magic x6 |]) 27 | ;; 28 | 29 | let get : ('a, 'b) t -> 'a -> 'b option = 30 | fun (x11 : ('a, 'b) t) (x10 : 'a) -> 31 | Ojs.option_of_js 32 | Obj.magic 33 | (Ojs.call (t_to_js Obj.magic Obj.magic x11) "get" [| Obj.magic x10 |]) 34 | ;; 35 | 36 | let delete : ('a, 'b) t -> 'a -> unit = 37 | fun (x16 : ('a, 'b) t) (x15 : 'a) -> 38 | (ignore : _) (Ojs.call (t_to_js Obj.magic Obj.magic x16) "delete" [| Obj.magic x15 |]) 39 | ;; 40 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/weak_map.mli: -------------------------------------------------------------------------------- 1 | open! Js_of_ocaml 2 | open! Gen_js_api 3 | 4 | (** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap *) 5 | 6 | type ('a, 'b) t 7 | 8 | val create : unit -> ('a, 'b) t [@@js.new "WeakMap"] 9 | val set : ('a, 'b) t -> 'a -> 'b -> unit [@@js.call] 10 | val get : ('a, 'b) t -> 'a -> 'b option [@@js.call] 11 | val delete : ('a, 'b) t -> 'a -> unit [@@js.call] 12 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/weak_set.ml: -------------------------------------------------------------------------------- 1 | [@@@js.dummy "!! This code has been generated by gen_js_api !!"] 2 | [@@@ocaml.warning "-7-32-39"] 3 | 4 | open! Js_of_ocaml 5 | open! Gen_js_api 6 | 7 | type 'a t = Ojs.t 8 | 9 | let rec t_of_js : 'a. (Ojs.t -> 'a) -> Ojs.t -> 'a t = 10 | fun (type __a) (__a_of_js : Ojs.t -> __a) (x2 : Ojs.t) -> x2 11 | 12 | and t_to_js : 'a. ('a -> Ojs.t) -> 'a t -> Ojs.t = 13 | fun (type __a) (__a_to_js : __a -> Ojs.t) (x1 : Ojs.t) -> x1 14 | ;; 15 | 16 | let create : unit -> 'a t = 17 | fun () -> t_of_js Obj.magic (Ojs.new_obj (Ojs.get_prop_ascii Ojs.global "WeakSet") [||]) 18 | ;; 19 | 20 | let add : 'a t -> 'a -> unit = 21 | fun (x5 : 'a t) (x4 : 'a) -> 22 | (ignore : _) (Ojs.call (t_to_js Obj.magic x5) "add" [| Obj.magic x4 |]) 23 | ;; 24 | 25 | let has : 'a t -> 'a -> bool = 26 | fun (x8 : 'a t) (x7 : 'a) -> 27 | Ojs.bool_of_js (Ojs.call (t_to_js Obj.magic x8) "has" [| Obj.magic x7 |]) 28 | ;; 29 | 30 | let delete : 'a t -> 'a -> unit = 31 | fun (x11 : 'a t) (x10 : 'a) -> 32 | (ignore : _) (Ojs.call (t_to_js Obj.magic x11) "delete" [| Obj.magic x10 |]) 33 | ;; 34 | -------------------------------------------------------------------------------- /jsoo_weak_collections/src/weak_set.mli: -------------------------------------------------------------------------------- 1 | open! Js_of_ocaml 2 | open! Gen_js_api 3 | 4 | (** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet *) 5 | 6 | type 'a t 7 | 8 | val create : unit -> 'a t [@@js.new "WeakSet"] 9 | val add : 'a t -> 'a -> unit [@@js.call] 10 | val has : 'a t -> 'a -> bool [@@js.call] 11 | val delete : 'a t -> 'a -> unit [@@js.call] 12 | -------------------------------------------------------------------------------- /jsoo_weak_collections/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name jsoo_weak_collections_test) 3 | (libraries core js_of_ocaml jsoo_weak_collections) 4 | (preprocess 5 | (pps ppx_jane js_of_ocaml-ppx))) 6 | -------------------------------------------------------------------------------- /jsoo_weak_collections/test/jsoo_weak_collections_test.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Js_of_ocaml 3 | open Jsoo_weak_collections 4 | 5 | let obj : unit Js.js_array Js.t = new%js Js.array_empty 6 | 7 | let%expect_test "weak-map operations" = 8 | let map = Weak_map.create () in 9 | assert (Option.is_none (Weak_map.get map obj)); 10 | Weak_map.set map obj 5; 11 | (match Weak_map.get map obj with 12 | | Some 5 -> () 13 | | _ -> assert false); 14 | Weak_map.delete map obj; 15 | assert (Option.is_none (Weak_map.get map obj)) 16 | ;; 17 | 18 | let%expect_test "weak-set operations" = 19 | let set = Weak_set.create () in 20 | assert (not (Weak_set.has set obj)); 21 | Weak_set.add set obj; 22 | assert (Weak_set.has set obj); 23 | Weak_set.delete set obj; 24 | assert (not (Weak_set.has set obj)) 25 | ;; 26 | -------------------------------------------------------------------------------- /keyboard/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name virtual_dom.keyboard) 3 | (name vdom_keyboard) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries core css_gen js_of_ocaml ui_effect virtual_dom)) 7 | -------------------------------------------------------------------------------- /keyboard/src/grouped_help_text.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | open Import 3 | module Group_name = String 4 | 5 | module View_spec = struct 6 | type t = 7 | { core_spec : Help_text.View_spec.t 8 | ; group_name : Group_name.t -> Vdom.Node.t 9 | } 10 | 11 | let plain = { core_spec = Help_text.View_spec.plain; group_name = Vdom.Node.text } 12 | 13 | let with_classes ~group_name_class ~key_class ~plain_text_class = 14 | let text_div class_ text = 15 | let open Vdom in 16 | Node.div ~attrs:[ Attr.class_ class_ ] [ Node.text text ] 17 | in 18 | { core_spec = Help_text.View_spec.with_classes ~key_class ~plain_text_class 19 | ; group_name = text_div group_name_class 20 | } 21 | ;; 22 | end 23 | 24 | module Command = Help_text.Command 25 | 26 | type t = 27 | { groups : Help_text.t Group_name.Map.t 28 | ; group_order : Group_name.t list 29 | } 30 | [@@deriving sexp, compare] 31 | 32 | let empty = { groups = Group_name.Map.empty; group_order = [] } 33 | let is_empty t = Map.is_empty t.groups 34 | 35 | let of_group_list_exn group_list = 36 | { groups = Group_name.Map.of_alist_exn group_list 37 | ; group_order = List.map group_list ~f:fst 38 | } 39 | ;; 40 | 41 | let add_group_exn t group_name commands = 42 | { groups = Map.add_exn t.groups ~key:group_name ~data:commands 43 | ; group_order = t.group_order @ [ group_name ] 44 | } 45 | ;; 46 | 47 | let of_command_list ?(custom_group_order = []) command_list = 48 | let groups = 49 | List.map custom_group_order ~f:(fun group_name -> group_name, []) 50 | |> Group_name.Map.of_alist_exn 51 | in 52 | let rev_group_order = List.rev custom_group_order in 53 | let groups, rev_group_order = 54 | List.fold 55 | command_list 56 | ~init:(groups, rev_group_order) 57 | ~f:(fun (groups, rev_group_order) (group_name, command) -> 58 | let rev_group_order = 59 | if Map.mem groups group_name 60 | then rev_group_order 61 | else group_name :: rev_group_order 62 | in 63 | let groups = 64 | Map.update groups group_name ~f:(fun commands -> 65 | let commands = Option.value commands ~default:[] in 66 | command :: commands) 67 | in 68 | groups, rev_group_order) 69 | in 70 | { groups = 71 | Map.filter_map groups ~f:(function 72 | | [] -> None 73 | | commands -> Some (Help_text.of_command_list (List.rev commands))) 74 | ; group_order = List.rev rev_group_order 75 | } 76 | ;; 77 | 78 | let add_command t group_name command = 79 | let group_order = 80 | if Map.mem t.groups group_name then t.group_order else t.group_order @ [ group_name ] 81 | in 82 | let groups = 83 | Map.update t.groups group_name ~f:(fun help_text -> 84 | let help_text = Option.value help_text ~default:Help_text.empty in 85 | Help_text.add_command help_text command) 86 | in 87 | { groups; group_order } 88 | ;; 89 | 90 | let groups t = 91 | List.filter_map t.group_order ~f:(fun group_name -> 92 | let open Option.Let_syntax in 93 | let%map group = Map.find t.groups group_name in 94 | group_name, group) 95 | ;; 96 | 97 | let commands t = 98 | List.concat_map (groups t) ~f:(fun (group_name, help_text) -> 99 | List.map (Help_text.commands help_text) ~f:(fun command -> group_name, command)) 100 | ;; 101 | 102 | let view t (view_spec : View_spec.t) = 103 | let open Vdom in 104 | let rows = 105 | List.concat_map (groups t) ~f:(fun (group_name, help_text) -> 106 | let group_name_row = 107 | Node.tr 108 | [ Node.td 109 | ~attrs: 110 | [ Attr.many_without_merge 111 | [ Attr.create "colspan" "2" 112 | ; Css_gen.text_align `Center |> Attr.style 113 | ] 114 | ] 115 | [ view_spec.group_name group_name ] 116 | ] 117 | in 118 | group_name_row :: Help_text.view_rows help_text view_spec.core_spec) 119 | in 120 | Vdom.Node.table rows 121 | ;; 122 | -------------------------------------------------------------------------------- /keyboard/src/grouped_help_text.mli: -------------------------------------------------------------------------------- 1 | open Core 2 | open Import 3 | 4 | (** A [Grouped_help_text.t] is similar to a [Help_text.t], but allows the user to organize 5 | the commands into groups. *) 6 | 7 | module Group_name : Identifiable 8 | 9 | (** [View_spec] is almost identical to [Help_text.View_spec], but additionally allows the 10 | user to customize how to display group names. *) 11 | module View_spec : sig 12 | type t = 13 | { core_spec : Help_text.View_spec.t 14 | ; group_name : Group_name.t -> Vdom.Node.t 15 | } 16 | 17 | val plain : t 18 | 19 | (** [with_classes] behaves the same as [Help_text.View_spec.with_classes] as far as the 20 | [core_spec], and additionally converts group names to text nodes and wraps them in 21 | divs with the given group name class. *) 22 | val with_classes 23 | : group_name_class:string 24 | -> key_class:string 25 | -> plain_text_class:string 26 | -> t 27 | end 28 | 29 | module Command = Help_text.Command 30 | 31 | type t [@@deriving sexp, compare] 32 | 33 | val empty : t 34 | val is_empty : t -> bool 35 | 36 | (** In the [*_exn] functions below, the group name is assumed to be unique for each help 37 | text group, and an exception is raised if duplicate group names are encountered. *) 38 | 39 | (** [of_group_list_exn] converts a list of help text groups into a grouped help text. *) 40 | val of_group_list_exn : (Group_name.t * Help_text.t) list -> t 41 | 42 | (** [add_group_exn] adds a new group to a grouped help text. This is linear in the number 43 | of groups already in the grouped help text. *) 44 | val add_group_exn : t -> Group_name.t -> Help_text.t -> t 45 | 46 | (** [groups] returns the help text groups in a grouped help text. *) 47 | val groups : t -> (Group_name.t * Help_text.t) list 48 | 49 | (** [of_command_list], [add_command], and [commands] are analogous to the corresponding 50 | [group] functions above, but deal with single commands instead of help text groups. 51 | 52 | Commands with the same group name are grouped together. 53 | 54 | By default, group order is determined by the order in which the groups first appear in 55 | the command list. However, if [custom_group_order] is given, it will be used to 56 | determine the group order instead. Any groups that appear in [custom_group_order] but 57 | not in the command list will be omitted. Any groups that appear in the command list 58 | but not in [custom_group_order] will be added to the end of [custom_group_order], in 59 | the order in which they first appear in the command list. 60 | 61 | [add_command] is linear in both the number of groups in the grouped help text and the 62 | number of commands already in its group. *) 63 | val of_command_list 64 | : ?custom_group_order:Group_name.t list 65 | -> (Group_name.t * Command.t) list 66 | -> t 67 | 68 | val add_command : t -> Group_name.t -> Command.t -> t 69 | val commands : t -> (Group_name.t * Command.t) list 70 | 71 | (** [view] displays a help text table with one row per command, organized into groups. 72 | Each group has a row containing the group name preceding the rows corresponding to the 73 | group's commands. *) 74 | val view : t -> View_spec.t -> Vdom.Node.t 75 | -------------------------------------------------------------------------------- /keyboard/src/help_text.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | open Import 3 | 4 | module View_spec = struct 5 | type t = 6 | { key : string -> Vdom.Node.t 7 | ; plain_text : string -> Vdom.Node.t 8 | } 9 | 10 | let plain = 11 | let open Vdom in 12 | { key = Node.text; plain_text = Node.text } 13 | ;; 14 | 15 | let with_classes ~key_class ~plain_text_class = 16 | let text_span class_ text = 17 | let open Vdom in 18 | Node.span ~attrs:[ Attr.class_ class_ ] [ Node.text text ] 19 | in 20 | { key = text_span key_class; plain_text = text_span plain_text_class } 21 | ;; 22 | end 23 | 24 | (* Dedup keystrokes that map to the same string, e.g. Enter and NumpadEnter. *) 25 | let dedup_keys keys = 26 | List.dedup_and_sort keys ~compare:(fun a b -> 27 | Comparable.lift String.compare ~f:Keystroke.to_string_hum a b) 28 | ;; 29 | 30 | (* If a command has "consecutive" keystrokes, we display only the first and the last in 31 | the range instead of listing out each one. Currently we only consider digits. *) 32 | let keys_are_consecutive (k0 : Keystroke.t) (k1 : Keystroke.t) = 33 | let extract_digit k = 34 | Option.try_with (fun () -> 35 | Keystroke.key k |> Keystroke.create' |> Keystroke.to_string_hum |> Int.of_string) 36 | in 37 | match extract_digit k0, extract_digit k1 with 38 | | None, _ | _, None -> false 39 | | Some digit0, Some digit1 -> 40 | digit0 + 1 = digit1 41 | && List.for_all 42 | Keystroke.[ ctrl; alt; shift; meta ] 43 | ~f:(fun modifier -> Bool.( = ) (modifier k0) (modifier k1)) 44 | ;; 45 | 46 | module Command = struct 47 | type t = 48 | { keys : Keystroke.t list 49 | ; description : string 50 | } 51 | [@@deriving sexp, compare] 52 | 53 | module Format = struct 54 | type t = 55 | [ `Keys of [ `Sep of string ] 56 | | `Description of (string -> string) option 57 | | `Text of string 58 | ] 59 | list 60 | 61 | let default = 62 | [ `Text "Press " 63 | ; `Keys (`Sep " or ") 64 | ; `Text " to " 65 | ; `Description (Some String.uncapitalize) 66 | ; `Text "." 67 | ] 68 | ;; 69 | end 70 | 71 | let view_keys t (view_spec : View_spec.t) ~sep = 72 | let keys = 73 | t.keys 74 | |> dedup_keys 75 | |> List.group ~break:(fun a b -> not (keys_are_consecutive a b)) 76 | |> List.map ~f:(function 77 | | [] -> [] 78 | | first_key :: keys -> 79 | let keys = first_key :: Option.to_list (List.last keys) in 80 | List.map keys ~f:Keystroke.to_string_hum 81 | |> List.map ~f:view_spec.key 82 | |> List.intersperse ~sep:(view_spec.plain_text " to ")) 83 | in 84 | List.intersperse keys ~sep:[ view_spec.plain_text sep ] |> List.concat 85 | ;; 86 | 87 | let view_description ?(f = Fn.id) t (view_spec : View_spec.t) = 88 | view_spec.plain_text (f t.description) 89 | ;; 90 | 91 | let view t view_spec format = 92 | let open Vdom in 93 | Node.div 94 | (List.concat_map format ~f:(function 95 | | `Keys (`Sep sep) -> view_keys t view_spec ~sep 96 | | `Description f -> [ view_description ?f t view_spec ] 97 | | `Text text -> [ view_spec.plain_text text ])) 98 | ;; 99 | end 100 | 101 | type t = Command.t list [@@deriving sexp, compare] 102 | 103 | let empty = [] 104 | let is_empty = List.is_empty 105 | let of_command_list = Fn.id 106 | let commands = Fn.id 107 | let add_command t command = t @ [ command ] 108 | 109 | let group_consecutive_commands (commands : Command.t list) = 110 | List.group commands ~break:(fun c0 c1 -> 111 | match dedup_keys c0.keys, dedup_keys c1.keys with 112 | | [ k0 ], [ k1 ] -> 113 | not (keys_are_consecutive k0 k1 && String.( = ) c0.description c1.description) 114 | | _ -> true) 115 | |> List.map ~f:(fun commands -> 116 | { Command.keys = List.concat_map commands ~f:(fun c -> c.keys) 117 | ; description = (List.hd_exn commands).description 118 | }) 119 | ;; 120 | 121 | let view_rows ?(sep = " or ") t (view_spec : View_spec.t) = 122 | let open Vdom in 123 | let align how = Css_gen.(text_align how) |> Attr.style in 124 | let commands = group_consecutive_commands (commands t) in 125 | List.map commands ~f:(fun command -> 126 | Node.tr 127 | [ Node.td 128 | ~attrs:[ align `Right ] 129 | (Command.view_keys command view_spec ~sep @ [ view_spec.plain_text " : " ]) 130 | ; Node.td ~attrs:[ align `Left ] [ Command.view_description command view_spec ] 131 | ]) 132 | ;; 133 | 134 | let view ?sep t view_spec = Vdom.Node.table (view_rows ?sep t view_spec) 135 | -------------------------------------------------------------------------------- /keyboard/src/help_text.mli: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | (** A [Help_text.t] represents the documentation for a collection of commands. It can be 4 | displayed as a Vdom node. 5 | 6 | This can be used to create a web ui help menu. *) 7 | 8 | module View_spec : sig 9 | (** A [View_spec.t] allows the user to customize how the help text is converted to a 10 | Vdom node. *) 11 | type t = 12 | { key : string -> Vdom.Node.t 13 | ; plain_text : string -> Vdom.Node.t 14 | } 15 | 16 | (** [plain] converts all help text parts to text nodes without further modification. *) 17 | val plain : t 18 | 19 | (** [with_classes] converts all help text parts to text nodes and wraps them in spans 20 | with the given classes. This allows the user to style the help text using CSS. *) 21 | val with_classes : key_class:string -> plain_text_class:string -> t 22 | end 23 | 24 | module Command : sig 25 | (** A [Command.t] represents the help text for a single command. *) 26 | 27 | type t = 28 | { keys : Keystroke.t list 29 | ; description : string 30 | } 31 | [@@deriving sexp] 32 | 33 | module Format : sig 34 | type t = 35 | [ `Keys of [ `Sep of string ] 36 | | (* separator between keys *) 37 | (* This function can be used to modify the description before displaying it, e.g. 38 | capitalizing/uncapitalizing it. *) 39 | `Description of 40 | (string -> string) option 41 | | `Text of string 42 | ] 43 | list 44 | 45 | (** The [default] format is: 46 | 47 | "Press <[key1] or [key2] or ... [keyn]> to ." *) 48 | val default : t 49 | end 50 | 51 | (** [view] displays a help text line for a single command. For instance, this can be 52 | used to display the help text for opening a help menu. *) 53 | val view : t -> View_spec.t -> Format.t -> Vdom.Node.t 54 | end 55 | 56 | type t [@@deriving sexp, compare] 57 | 58 | val empty : t 59 | val is_empty : t -> bool 60 | val of_command_list : Command.t list -> t 61 | 62 | (** [add_command] is linear in the number of commands in [t]. *) 63 | val add_command : t -> Command.t -> t 64 | 65 | val commands : t -> Command.t list 66 | 67 | (** [view] displays a help text table. Each row shows a list of keys and a description. 68 | Multiple commands with the same description may be combined into one row. 69 | 70 | [view_rows] is similar to [view], but returns a list of row nodes instead of wrapping 71 | them in a table node. *) 72 | val view : ?sep:string -> t -> View_spec.t -> Vdom.Node.t 73 | 74 | val view_rows : ?sep:string -> t -> View_spec.t -> Vdom.Node.t list 75 | -------------------------------------------------------------------------------- /keyboard/src/import.ml: -------------------------------------------------------------------------------- 1 | include Virtual_dom 2 | include Js_of_ocaml 3 | -------------------------------------------------------------------------------- /keyboard/src/keyboard_event.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Import 3 | open Dom_html 4 | 5 | type t = keyboardEvent Js.t 6 | 7 | module Keyboard_code = Keyboard_code 8 | 9 | let key e = Keyboard_code.of_event e 10 | 11 | (* This function looks silly (and it is): why not just use [Js.to_bool]? 12 | [robust_to_bool] handles [bool Js.t] that _might_ actually be [bool Js.t Js.Optdef.t]. 13 | 14 | This is an important distinction to make if someone lied when submitting a keyboard 15 | event and it doesn't have the properties that we expect. *) 16 | let robust_to_bool b = phys_equal b Js._true 17 | 18 | let modifier e ~states ~fallback = 19 | (* we don't trust that [fallback] is actually a [bool Js.t] because it was pulled 20 | out of an event that could be missing the fields that the DOM promises exists, 21 | so we use [robust_to_bool] instead. 22 | 23 | To my knowledge this can only happen if a user manually submits an event that is 24 | missing these fields, but it's better to be safe. *) 25 | match robust_to_bool fallback with 26 | | true -> true 27 | | false -> 28 | (* Some [KeyboardEvent]s don't have a getModifierState function for some reason. 29 | This issue was first noticed in a typeahead component, but it may be an issue 30 | for other input elements too. If we can't find this property, we bail. *) 31 | let has_get_modifier_state_function = 32 | Js.Unsafe.get e (Js.string "getModifierState") |> Js.Optdef.test 33 | in 34 | (match has_get_modifier_state_function with 35 | | false -> false 36 | | true -> 37 | List.exists states ~f:(fun state_name -> 38 | robust_to_bool (e##getModifierState (Js.string state_name)))) 39 | ;; 40 | 41 | let ctrl e = modifier e ~states:[ "Control"; "AltGraph" ] ~fallback:e##.ctrlKey 42 | let alt e = modifier e ~states:[ "Alt"; "AltGraph" ] ~fallback:e##.altKey 43 | let shift e = modifier e ~states:[ "Shift" ] ~fallback:e##.shiftKey 44 | let meta e = modifier e ~states:[ "Meta" ] ~fallback:e##.metaKey 45 | 46 | let match_modifiers ?ctrl:ctrl' ?alt:alt' ?shift:shift' ?meta:meta' e = 47 | List.for_all 48 | [ ctrl', ctrl e; alt', alt e; shift', shift e; meta', meta e ] 49 | ~f:(fun (cond, env) -> Option.value_map cond ~default:true ~f:(Bool.equal env)) 50 | ;; 51 | 52 | let no_modifiers e = match_modifiers ~ctrl:false ~alt:false ~shift:false ~meta:false e 53 | let map e ~f = f (`Ctrl (ctrl e), `Alt (alt e), `Shift (shift e), `Meta (meta e), key e) 54 | -------------------------------------------------------------------------------- /keyboard/src/keyboard_event.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Import 3 | open Dom_html 4 | 5 | type t = keyboardEvent Js.t 6 | 7 | module Keyboard_code = Keyboard_code 8 | 9 | val key : t -> Keyboard_code.t 10 | val ctrl : t -> bool 11 | val alt : t -> bool 12 | val shift : t -> bool 13 | val meta : t -> bool 14 | 15 | (** [match_modifiers] evaluates a [t]'s modifiers vs the function's arguments. If an 16 | argument is not specified then that modifier is not evaluated. *) 17 | val match_modifiers : ?ctrl:bool -> ?alt:bool -> ?shift:bool -> ?meta:bool -> t -> bool 18 | 19 | val no_modifiers : t -> bool 20 | 21 | val map 22 | : t 23 | -> f: 24 | ([ `Ctrl of bool ] 25 | * [ `Alt of bool ] 26 | * [ `Shift of bool ] 27 | * [ `Meta of bool ] 28 | * Keyboard_code.t 29 | -> 'a) 30 | -> 'a 31 | -------------------------------------------------------------------------------- /keyboard/src/keystroke.mli: -------------------------------------------------------------------------------- 1 | open Core 2 | open Js_of_ocaml 3 | 4 | module Keyboard_code : sig 5 | type t = Dom_html.Keyboard_code.t 6 | [@@deriving sexp, compare, bin_io, hash, enumerate, equal] 7 | 8 | val of_event : Dom_html.keyboardEvent Js.t -> t 9 | 10 | (** [to_location] returns the location of the key on the keyboard. 11 | https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location *) 12 | val to_location : t -> int 13 | 14 | (** [to_code_string] returns the value of [evt##.code] that this [Keyboard_code.t] 15 | represents; i.e. which physical key was pressed. Note that this may not account for 16 | character mappings. 17 | https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code *) 18 | val to_code_string : t -> string 19 | 20 | (** [to_key_code] returns the value of [evt##.keyCode] that this [Keyboard_code.t] 21 | represents. [keyCode] is deprecated in web standards. 22 | https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode *) 23 | val to_key_code : t -> int 24 | end 25 | 26 | type t [@@deriving sexp, compare, bin_io, hash, sexp_grammar] 27 | 28 | include Comparable.S_binable with type t := t 29 | include Hashable.S_binable with type t := t 30 | 31 | val create : Keyboard_code.t -> ctrl:bool -> alt:bool -> shift:bool -> meta:bool -> t 32 | val create' : ?ctrl:unit -> ?alt:unit -> ?shift:unit -> ?meta:unit -> Keyboard_code.t -> t 33 | val key : t -> Keyboard_code.t 34 | val ctrl : t -> bool 35 | val alt : t -> bool 36 | val shift : t -> bool 37 | val meta : t -> bool 38 | val of_event : Keyboard_event.t -> t 39 | val to_string_hum : t -> string 40 | 41 | (** The first string is either "Shift+" or "", depending on whether "Shift" needs to be 42 | displayed to indicate the keystrokes. The second is the keyboard code itself. For 43 | instance: 44 | 45 | - For '(', returns ["" , "("] instead of ["Shift+", "9"] 46 | - For 'A', returns ["Shift+" , "a"] *) 47 | val shift_string_and_keyboard_code_string : t -> string * string 48 | 49 | (** Same as [shift_string_and_keyboard_code_string], but indicates whether shift needs to 50 | be displayed in a variant rather than via "Shift+" vs "". *) 51 | val shift_and_keyboard_code_string 52 | : t 53 | -> [ `Display_shift | `Dont_display_shift ] * string 54 | -------------------------------------------------------------------------------- /keyboard/src/variable_keyboard_event_handler.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | open! Import 3 | module Const_handler = Keyboard_event_handler 4 | module Action = Const_handler.Action 5 | module Command = Const_handler.Command 6 | 7 | type 'env t = 8 | { handler : Const_handler.t 9 | ; variable_actions : 'env -> Action.t list 10 | } 11 | [@@deriving sexp_of] 12 | 13 | let empty_variable_actions _env = [] 14 | let empty = { handler = Const_handler.empty; variable_actions = empty_variable_actions } 15 | 16 | let of_const_handler ?(variable_actions = empty_variable_actions) handler = 17 | { handler; variable_actions } 18 | ;; 19 | 20 | let to_const_handler t env = 21 | let variable_actions = t.variable_actions env in 22 | List.fold variable_actions ~init:t.handler ~f:Const_handler.set_action 23 | ;; 24 | 25 | let map_handler t f arg = { t with handler = f t.handler arg } 26 | let add_action_exn t = map_handler t Const_handler.add_action_exn 27 | let add_command_exn t = map_handler t Const_handler.add_command_exn 28 | let add_disabled_key_exn t = map_handler t Const_handler.add_disabled_key_exn 29 | let set_action t = map_handler t Const_handler.set_action 30 | let set_command t = map_handler t Const_handler.set_command 31 | let set_disabled_key t = map_handler t Const_handler.set_disabled_key 32 | 33 | let add_variable_actions t actions = 34 | { t with variable_actions = (fun env -> t.variable_actions env @ actions env) } 35 | ;; 36 | 37 | let add_variable_commands t commands = 38 | add_variable_actions t (fun env -> List.map (commands env) ~f:Action.command) 39 | ;; 40 | 41 | let add_variable_disabled_keys t keys = 42 | add_variable_actions t (fun env -> List.map (keys env) ~f:Action.disabled_key) 43 | ;; 44 | 45 | module Variable_handler_command = struct 46 | type 'env t = 47 | { keys : Keystroke.t list 48 | ; description : string 49 | ; group : Grouped_help_text.Group_name.t option 50 | ; handler : 'env -> Const_handler.Handler.t 51 | } 52 | 53 | let to_const { keys; description; group; handler } env : Command.t = 54 | { keys; description; group; handler = handler env } 55 | ;; 56 | 57 | let get_help_text { keys; description; _ } : Help_text.Command.t = { keys; description } 58 | end 59 | 60 | module Variable_handler_action = struct 61 | type 'env t = 62 | | Command of 'env Variable_handler_command.t 63 | | Disabled_key of Keystroke.t 64 | 65 | let to_const action env : Action.t = 66 | match action with 67 | | Command command -> Command (Variable_handler_command.to_const command env) 68 | | Disabled_key key -> Disabled_key key 69 | ;; 70 | 71 | let get_help_text action : Help_text.Command.t = 72 | match action with 73 | | Command command -> Variable_handler_command.get_help_text command 74 | | Disabled_key key -> Keyboard_event_handler.Action.get_help_text (Disabled_key key) 75 | ;; 76 | end 77 | 78 | let add_variable_handler_action t action = 79 | add_variable_actions t (fun env -> [ Variable_handler_action.to_const action env ]) 80 | ;; 81 | 82 | let add_variable_handler_command t command = 83 | add_variable_handler_action t (Command command) 84 | ;; 85 | -------------------------------------------------------------------------------- /keyboard/src/variable_keyboard_event_handler.mli: -------------------------------------------------------------------------------- 1 | open Keyboard_event_handler 2 | 3 | (** A [Variable_keyboard_event_handler.t] provides a way of representing a keyboard event 4 | handler that contains both a (possibly empty) constant set of actions and a variable 5 | set of actions that depends on some ['env] variable. In order to handle keyboard 6 | events or produce help text, it must first be converted to a 7 | [Keyboard_event_handler.t] using the function [to_const_handler]. *) 8 | 9 | module Action = Action 10 | module Command = Command 11 | 12 | type 'env t [@@deriving sexp_of] 13 | 14 | val empty : 'env t 15 | 16 | val of_const_handler 17 | : ?variable_actions:('env -> Action.t list) 18 | -> Keyboard_event_handler.t 19 | -> 'env t 20 | 21 | (** [add_variable_actions], [add_variable_commands], and [add_variable_disabled_keys] add 22 | a new variable set of actions to a variable keyboard event handler. This does not 23 | replace any existing variable actions in the handler, but instead adds to them. *) 24 | val add_variable_actions : 'env t -> ('env -> Action.t list) -> 'env t 25 | 26 | val add_variable_commands : 'env t -> ('env -> Command.t list) -> 'env t 27 | val add_variable_disabled_keys : 'env t -> ('env -> Keystroke.t list) -> 'env t 28 | 29 | (** The [add_*_exn] and [set_*] functions below behave in the same was as the 30 | corresponding functions in [Keyboard_event_handler]. *) 31 | val add_action_exn : 'env t -> Action.t -> 'env t 32 | 33 | val add_command_exn : 'env t -> Command.t -> 'env t 34 | val add_disabled_key_exn : 'env t -> Keystroke.t -> 'env t 35 | val set_action : 'env t -> Action.t -> 'env t 36 | val set_command : 'env t -> Command.t -> 'env t 37 | val set_disabled_key : 'env t -> Keystroke.t -> 'env t 38 | 39 | (** [to_const_handler] evaluates the variable set of actions for the given ['env] value, 40 | and combines them with the constant set of actions to create a keyboard event handler. 41 | 42 | It is possible that for a given ['env] value, multiple actions are defined for the 43 | same key. In that case, the latest variable action is used when creating the constant 44 | keyboard event handler. *) 45 | val to_const_handler : 'env t -> 'env -> Keyboard_event_handler.t 46 | 47 | (** [Variable_handler_command] and [Variable_handler_action] provide a way of representing 48 | commands whose keys, description and group are constant, but whose handler varies with 49 | the ['env] variable. 50 | 51 | This is useful because it provides a way of generating help text for these commands 52 | that does not depend on or vary with the ['env] variable. *) 53 | module Variable_handler_command : sig 54 | type 'env t = 55 | { keys : Keystroke.t list 56 | ; description : string 57 | ; group : Grouped_help_text.Group_name.t option 58 | ; handler : 'env -> Handler.t 59 | } 60 | 61 | val get_help_text : _ t -> Help_text.Command.t 62 | end 63 | 64 | module Variable_handler_action : sig 65 | type 'env t = 66 | | Command of 'env Variable_handler_command.t 67 | | Disabled_key of Keystroke.t 68 | 69 | val get_help_text : _ t -> Help_text.Command.t 70 | end 71 | 72 | (** [add_variable_handler_action] and [add_variable_handler_command] are utility functions 73 | for adding variable handler actions to an existing variable keyboard event handler. 74 | Under the hood, the variable handler action is converted to a variable action of the 75 | form ['env -> Action.t]. *) 76 | val add_variable_handler_action : 'env t -> 'env Variable_handler_action.t -> 'env t 77 | 78 | val add_variable_handler_command : 'env t -> 'env Variable_handler_command.t -> 'env t 79 | -------------------------------------------------------------------------------- /keyboard/src/vdom_keyboard.ml: -------------------------------------------------------------------------------- 1 | module Grouped_help_text = Grouped_help_text 2 | module Help_text = Help_text 3 | module Keyboard_event_handler = Keyboard_event_handler 4 | module Keystroke = Keystroke 5 | module Variable_keyboard_event_handler = Variable_keyboard_event_handler 6 | module Keyboard_event = Keyboard_event 7 | 8 | let with_keyboard_handler node keyboard_handler = 9 | let open Virtual_dom.Vdom in 10 | Node.div 11 | ~attrs: 12 | [ Attr.on_keydown (fun event -> 13 | Keyboard_event_handler.handle_or_ignore_event keyboard_handler event) 14 | ] 15 | [ node ] 16 | ;; 17 | -------------------------------------------------------------------------------- /keyboard/test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name test_vdom_keyboard) 3 | (libraries vdom_keyboard core js_of_ocaml) 4 | (preprocess 5 | (pps ppx_jane js_of_ocaml-ppx))) 6 | -------------------------------------------------------------------------------- /keyboard/test/test_vdom_keyboard.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Vdom_keyboard 3 | module Keyboard_code = Keystroke.Keyboard_code 4 | open Js_of_ocaml 5 | 6 | let%expect_test _ = 7 | let (_ : unit list) = 8 | let%map.List keyboard_code = Keyboard_code.all in 9 | let key_code = Keyboard_code.to_key_code keyboard_code in 10 | let location = Keyboard_code.to_location keyboard_code in 11 | let roundtrip = 12 | Keyboard_code.of_event 13 | (Js.Unsafe.coerce 14 | (object%js 15 | val keyCode = key_code 16 | val location = location 17 | 18 | (* We need to include [key] and [code], so that there's some value for 19 | our [of_event] function to read. But we want to use values that wouldn't show 20 | up in real events, because we're testing just the [key_code] (and 21 | [location]) property. *) 22 | val key = Js.string "not a valid key" 23 | val code = Js.string "not a valid code" 24 | end)) 25 | in 26 | match keyboard_code with 27 | | NumpadEqual 28 | | NumLock 29 | | VolumeMute 30 | | VolumeDown 31 | | VolumeUp 32 | | MediaTrackPrevious 33 | | MediaTrackNext 34 | | MediaPlayPause 35 | | MediaStop 36 | | BrowserSearch 37 | | BrowserHome 38 | | BrowserFavorites 39 | | BrowserRefresh 40 | | BrowserStop 41 | | BrowserForward 42 | | BrowserBack 43 | | OSLeft 44 | | OSRight 45 | | IntlBackslash 46 | | IntlYen -> () 47 | | _ when Keyboard_code.equal keyboard_code roundtrip -> () 48 | | _ -> 49 | raise_s 50 | [%message 51 | (key_code : int) (keyboard_code : Keyboard_code.t) (roundtrip : Keyboard_code.t)] 52 | in 53 | () 54 | ;; 55 | 56 | let via_empty_object : Dom_html.keyboardEvent Js.t = Obj.magic (object%js end) 57 | 58 | let via_getmodifierstate : Dom_html.keyboardEvent Js.t = 59 | Obj.magic 60 | (object%js 61 | val getModifierState = Js.wrap_callback (fun _ -> Js._true) 62 | end) 63 | ;; 64 | 65 | let via_bad_getmodifierstate : Dom_html.keyboardEvent Js.t = 66 | Obj.magic 67 | (object%js 68 | val getModifierState = Js.wrap_callback (fun _ -> Js.string "foo") 69 | end) 70 | ;; 71 | 72 | let%expect_test "ctrl" = 73 | let via_property : Dom_html.keyboardEvent Js.t = 74 | Obj.magic 75 | (object%js 76 | val ctrlKey = Js.bool true 77 | end) 78 | in 79 | print_s [%message (Keyboard_event.ctrl via_property : bool)]; 80 | [%expect {| ("Keyboard_event.ctrl via_property" true) |}]; 81 | print_s [%message (Keyboard_event.ctrl via_empty_object : bool)]; 82 | [%expect {| ("Keyboard_event.ctrl via_empty_object" false) |}]; 83 | print_s [%message (Keyboard_event.ctrl via_getmodifierstate : bool)]; 84 | [%expect {| ("Keyboard_event.ctrl via_getmodifierstate" true) |}]; 85 | print_s [%message (Keyboard_event.ctrl via_bad_getmodifierstate : bool)]; 86 | [%expect {| ("Keyboard_event.ctrl via_bad_getmodifierstate" false) |}] 87 | ;; 88 | 89 | let%expect_test "alt" = 90 | let via_property : Dom_html.keyboardEvent Js.t = 91 | Obj.magic 92 | (object%js 93 | val altKey = Js.bool true 94 | end) 95 | in 96 | print_s [%message (Keyboard_event.alt via_property : bool)]; 97 | [%expect {| ("Keyboard_event.alt via_property" true) |}]; 98 | print_s [%message (Keyboard_event.alt via_empty_object : bool)]; 99 | [%expect {| ("Keyboard_event.alt via_empty_object" false) |}]; 100 | print_s [%message (Keyboard_event.alt via_getmodifierstate : bool)]; 101 | [%expect {| ("Keyboard_event.alt via_getmodifierstate" true) |}]; 102 | print_s [%message (Keyboard_event.alt via_bad_getmodifierstate : bool)]; 103 | [%expect {| ("Keyboard_event.alt via_bad_getmodifierstate" false) |}] 104 | ;; 105 | 106 | let%expect_test "shift" = 107 | let via_property : Dom_html.keyboardEvent Js.t = 108 | Obj.magic 109 | (object%js 110 | val shiftKey = Js.bool true 111 | end) 112 | in 113 | print_s [%message (Keyboard_event.shift via_property : bool)]; 114 | [%expect {| ("Keyboard_event.shift via_property" true) |}]; 115 | print_s [%message (Keyboard_event.shift via_empty_object : bool)]; 116 | [%expect {| ("Keyboard_event.shift via_empty_object" false) |}]; 117 | print_s [%message (Keyboard_event.shift via_getmodifierstate : bool)]; 118 | [%expect {| ("Keyboard_event.shift via_getmodifierstate" true) |}]; 119 | print_s [%message (Keyboard_event.shift via_bad_getmodifierstate : bool)]; 120 | [%expect {| ("Keyboard_event.shift via_bad_getmodifierstate" false) |}] 121 | ;; 122 | 123 | let%expect_test "meta" = 124 | let via_property : Dom_html.keyboardEvent Js.t = 125 | Obj.magic 126 | (object%js 127 | val metaKey = Js.bool true 128 | end) 129 | in 130 | print_s [%message (Keyboard_event.meta via_property : bool)]; 131 | [%expect {| ("Keyboard_event.meta via_property" true) |}]; 132 | print_s [%message (Keyboard_event.meta via_empty_object : bool)]; 133 | [%expect {| ("Keyboard_event.meta via_empty_object" false) |}]; 134 | print_s [%message (Keyboard_event.meta via_getmodifierstate : bool)]; 135 | [%expect {| ("Keyboard_event.meta via_getmodifierstate" true) |}]; 136 | print_s [%message (Keyboard_event.meta via_bad_getmodifierstate : bool)]; 137 | [%expect {| ("Keyboard_event.meta via_bad_getmodifierstate" false) |}] 138 | ;; 139 | -------------------------------------------------------------------------------- /keyboard/test/test_vdom_keyboard.mli: -------------------------------------------------------------------------------- 1 | (** Intentionally left blank *) 2 | -------------------------------------------------------------------------------- /layout/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name vdom_layout) 3 | (public_name virtual_dom.layout) 4 | (libraries core css_gen virtual_dom) 5 | (preprocess 6 | (pps ppx_jane))) 7 | -------------------------------------------------------------------------------- /layout/vdom_layout.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | module Attr = Virtual_dom.Vdom.Attr 3 | module Node = Virtual_dom.Vdom.Node 4 | 5 | let rec wrap_in_element_if_necessary node = 6 | let wrap_with_div children = 7 | let div = Node.div children in 8 | match div with 9 | | Node.Element e -> e 10 | | _ -> assert false 11 | in 12 | match node with 13 | | Node.Text _ | Node.Widget _ | Node.None -> wrap_with_div [ node ] 14 | | Fragment children -> wrap_with_div children 15 | | Node.Lazy { t; _ } -> wrap_in_element_if_necessary (Lazy.force t) 16 | | Node.Element e -> e 17 | ;; 18 | 19 | let add_style node ~style = 20 | let element = wrap_in_element_if_necessary node in 21 | Node.Element (Node.Element.add_style element style) 22 | ;; 23 | 24 | let map_style node ~f = 25 | let element = wrap_in_element_if_necessary node in 26 | Node.Element 27 | (Node.Element.map_attrs element ~f:(fun attrs -> 28 | Attr.many_without_merge (Attr.Multi.map_style [ attrs ] ~f))) 29 | ;; 30 | 31 | let grow n = add_style n ~style:Css_gen.(flex_item ~grow:1. ()) 32 | let grow_and_shrink n = add_style n ~style:Css_gen.(flex_item ~grow:1. ~shrink:1. ()) 33 | 34 | let scrollable n = 35 | add_style 36 | n 37 | ~style: 38 | Css_gen.(flex_item ~grow:1. ~shrink:1. () @> overflow_y `Auto @> overflow_x `Auto) 39 | ;; 40 | 41 | let spacer ?(attrs = []) ?min_width ?min_height () = 42 | let style = 43 | let mw = Option.value_map min_width ~default:Css_gen.empty ~f:Css_gen.min_width in 44 | let mh = Option.value_map min_height ~default:Css_gen.empty ~f:Css_gen.min_height in 45 | Css_gen.concat [ mw; mh ] 46 | in 47 | Node.span ~attrs:[ Attr.many_without_merge (Attr.style style :: attrs) ] [] 48 | ;; 49 | 50 | let as_box 51 | (direction : [ `Row | `Column ]) 52 | ?gap 53 | ?align_items 54 | (node_creator : Node.Aliases.node_creator) 55 | ?key 56 | ?attrs 57 | nodes 58 | = 59 | let nodes = 60 | match gap with 61 | | None -> nodes 62 | | Some gap_len -> 63 | let gap = 64 | match direction with 65 | | `Column -> spacer ~min_height:gap_len () 66 | | `Row -> spacer ~min_width:gap_len () 67 | in 68 | List.intersperse nodes ~sep:gap 69 | in 70 | let nodes = 71 | List.map 72 | nodes 73 | ~f: 74 | (map_style ~f:(fun style -> 75 | let has_flex_shrink_set = 76 | Css_gen.to_string_list style 77 | |> List.exists ~f:(fun (f, _) -> 78 | String.( = ) f "flex-shrink" || String.( = ) f "flex") 79 | in 80 | if has_flex_shrink_set 81 | then style 82 | else Css_gen.(style @> create ~field:"flex-shrink" ~value:"0"))) 83 | in 84 | let node = node_creator ?key ?attrs nodes in 85 | let direction = 86 | (direction :> [ `Row | `Column | `Row_reverse | `Column_reverse | `Default ]) 87 | in 88 | add_style node ~style:Css_gen.(flex_container ~direction ?align_items ()) 89 | ;; 90 | 91 | let as_hbox = as_box `Row 92 | let as_vbox = as_box `Column 93 | 94 | let hbox ?gap ?align_items ?key ?attrs children = 95 | as_hbox ?gap ?align_items Node.div ?key ?attrs children 96 | ;; 97 | 98 | let vbox ?gap ?align_items ?key ?attrs children = 99 | as_vbox ?gap ?align_items Node.div ?key ?attrs children 100 | ;; 101 | 102 | let body ?(direction = `Column) ?gap ?align_items ?key ?attrs nodes = 103 | let p100 = Percent.of_percentage 100.0 in 104 | as_box direction ?gap ?align_items Node.body ?key ?attrs nodes 105 | |> add_style 106 | ~style: 107 | Css_gen.( 108 | width (`Vw p100) 109 | @> height (`Vh p100) 110 | @> uniform_margin (`Px 0) 111 | @> uniform_padding (`Px 0)) 112 | ;; 113 | 114 | let on_grayed_out_background nodes = 115 | Node.div 116 | ~attrs: 117 | [ Attr.style 118 | Css_gen.( 119 | position ~left:(`Px 0) ~top:(`Px 0) `Fixed 120 | @> width Length.percent100 121 | @> height Length.percent100 122 | @> overflow `Auto 123 | @> background_color 124 | (`RGBA 125 | (Color.RGBA.create ~r:0 ~g:0 ~b:0 ~a:(Percent.of_percentage 40.) ()))) 126 | ] 127 | nodes 128 | ;; 129 | 130 | let modal ?(direction = `Row) ?gap ?align_items ?key ?(attrs = []) nodes = 131 | on_grayed_out_background 132 | [ as_box 133 | direction 134 | Node.div 135 | ?gap 136 | ?align_items 137 | ?key 138 | ~attrs: 139 | [ Attr.many_without_merge 140 | ([ Attr.style 141 | Css_gen.( 142 | margin_top (`Percent (Percent.of_percentage 15.0)) 143 | @> margin_left `Auto 144 | @> margin_right `Auto 145 | @> width (`Percent (Percent.of_percentage 80.0)) 146 | @> background_color (`Name "#fefefe") 147 | @> uniform_padding (`Px 20) 148 | @> border ~width:(`Px 2) ~style:`Solid ~color:(`Name "black") ()) 149 | ] 150 | @ attrs) 151 | ] 152 | nodes 153 | ] 154 | ;; 155 | -------------------------------------------------------------------------------- /layout/vdom_layout.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | module Attr := Virtual_dom.Vdom.Attr 3 | module Node := Virtual_dom.Vdom.Node 4 | 5 | type node_creator := ?key:string -> ?attrs:Attr.t list -> Node.t list -> Node.t 6 | 7 | (** If node is an Element, do nothing other than returning that Element. Otherwise put a 8 | div around the node and return the div's Element. *) 9 | val wrap_in_element_if_necessary : Node.t -> Node.Element.t 10 | 11 | (** Change the style attribute of a node. Calls [wrap_in_element_if_necessary]. *) 12 | val add_style : Node.t -> style:Css_gen.t -> Node.t 13 | 14 | (** Turn a node_creator into a node_creator that sets the style information on the node to 15 | be a flexbox. We also set the flex attribute flex-shrink to 0 if it isn't set (but we 16 | leave it alone if it happens to be set). *) 17 | val as_box 18 | : [ `Row | `Column ] 19 | -> ?gap:Css_gen.Length.t 20 | -> ?align_items:Css_gen.item_alignment 21 | -> node_creator 22 | -> node_creator 23 | 24 | val as_hbox 25 | : ?gap:Css_gen.Length.t 26 | -> ?align_items:Css_gen.item_alignment 27 | -> node_creator 28 | -> node_creator 29 | 30 | val as_vbox 31 | : ?gap:Css_gen.Length.t 32 | -> ?align_items:Css_gen.item_alignment 33 | -> node_creator 34 | -> node_creator 35 | 36 | (** Set flex-grow to 1 on the given Node. *) 37 | val grow : Node.t -> Node.t 38 | 39 | (** Set flex-grow and flex-shrink to 1 on the given Node. *) 40 | val grow_and_shrink : Node.t -> Node.t 41 | 42 | (** Make this a child of a flexbox that can scroll (e.g. if its content is bigger than the 43 | size given to it by the parent container, scrollbars will appear) *) 44 | val scrollable : Node.t -> Node.t 45 | 46 | (** Convenience wrapper same as [as_hbox Node.div]. *) 47 | val hbox : ?gap:Css_gen.Length.t -> ?align_items:Css_gen.item_alignment -> node_creator 48 | 49 | (** Convenience wrapper same as [as_vbox Node.div]. *) 50 | val vbox : ?gap:Css_gen.Length.t -> ?align_items:Css_gen.item_alignment -> node_creator 51 | 52 | (** a blank div element *) 53 | val spacer 54 | : ?attrs:Attr.t list 55 | -> ?min_width:Css_gen.Length.t 56 | -> ?min_height:Css_gen.Length.t 57 | -> unit 58 | -> Node.t 59 | 60 | (** box (default direction=`Column), width = 100vh, height = 100vh *) 61 | val body 62 | : ?direction:[ `Row | `Column ] 63 | -> ?gap:Css_gen.Length.t 64 | -> ?align_items:Css_gen.item_alignment 65 | -> node_creator 66 | 67 | (** Display nodes (layouted as a box) in a smaller window on top of everything else (with 68 | everything else grayed out and inaccessible via the mouse). Note that this renders a 69 | border but no control elements (not even a button to close the window) *) 70 | val modal 71 | : ?direction:[ `Row | `Column ] 72 | -> ?gap:Css_gen.Length.t 73 | -> ?align_items:Css_gen.item_alignment 74 | -> node_creator 75 | -------------------------------------------------------------------------------- /lib/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt-Esch. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | To regenerate the bundle, run 2 | 3 | ``` 4 | npm install 5 | npm run bundle 6 | ``` 7 | 8 | inside a bubblewrap 9 | 10 | last imported at revision 947ecf92b67d25bb693a0f625fa8e90c099887d5 11 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bundle": "browserify --standalone VirtualDom virtualdom.js -o virtualdom.compiled.js && prettier virtualdom.compiled.js > virtualdom.compiled.pretty.js" 4 | }, 5 | "dependencies": { 6 | "browser-split": "0.0.1", 7 | "browserify": "^13.1.0", 8 | "error": "^4.3.0", 9 | "ev-store": "^7.0.0", 10 | "global": "^4.3.0", 11 | "is-object": "^1.0.1", 12 | "next-tick": "^0.2.2", 13 | "prettier": "^2.8.4", 14 | "x-is-array": "0.1.0", 15 | "x-is-string": "0.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/vendor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v2.0.0 4 | 5 | Provides fundamental fixes and tests for reordering of keyed nodes. 6 | 7 | Everyone is encouraged to upgrade to v2. The main caveat of this upgrade 8 | is that it changes the patch format to encode all of the move data, 9 | instead of previously providing the mapping and expecting patch to work 10 | out which moves are required. While this might limit options for 11 | reordering, on the grounds of performance and debugging it made more 12 | sense. 13 | 14 | This is considered a breaking change for that reason. However, for those 15 | of you who only consume `create`, `diff`, `patch` and `h`, this will 16 | not affect your usage, and upgrading is recommended due to the bugs this 17 | new version fixes. 18 | 19 | ## v1.3.0 20 | 21 | - Add optimization to AttributHook to prevent resetting attributes where 22 | new hook instances are used but their values remain the same. 23 | 24 | - Extend the interface of unhook to take the next value from diff. 25 | 26 | - Fix bug where hook is called on unhook-only hooks. 27 | 28 | - Code refactor: diffProps broken out into it's own file 29 | 30 | ## v1.2.0 31 | 32 | - Correctly sets SVG attributes that are namespaced using a (fixed) 33 | attribute hook. 34 | 35 | - Add CSS animation notes from github issue (css-animations.md) 36 | 37 | - A hook with an `unhook` method and no `hook` method is now considered a 38 | valid hook. 39 | 40 | - Fixes issue where unhook was not called when a hook property is replaced 41 | with a new value 42 | 43 | - Fixes dist script update 44 | 45 | - Update README to note that an instance of `dom-delegator` is required to 46 | use the `ev-*` properties. 47 | 48 | ## v1.1.0 - Element reordering 49 | 50 | - Updates the way in which elements are reordered to increase performance 51 | in common use cases. 52 | 53 | - Adds additional SVG display attributes. 54 | 55 | # v1.0.0 - Sensible versioning begins 56 | 57 | # v0.0.24 - Fix destroy ordering 58 | 59 | - Fixes a bug where widgets cannot be replaced by vnodes due to a bug in the 60 | order of destroy patches. 61 | 62 | # v0.0.23 - Release notes begin 63 | -------------------------------------------------------------------------------- /lib/vendor/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt-Esch. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/vendor/create-element.js: -------------------------------------------------------------------------------- 1 | var createElement = require("./vdom/create-element.js") 2 | 3 | module.exports = createElement 4 | -------------------------------------------------------------------------------- /lib/vendor/diff.js: -------------------------------------------------------------------------------- 1 | var diff = require("./vtree/diff.js") 2 | 3 | module.exports = diff 4 | -------------------------------------------------------------------------------- /lib/vendor/docs.jsig: -------------------------------------------------------------------------------- 1 | -- A VHook is an object with a hook and unhook method. 2 | -- A VHook can be used to define your own apply property logic. 3 | -- 4 | -- When a hook is found in the VProperties object in either 5 | -- `patch()` or `createElement()`, instead of setting 6 | -- or unsetting the property we will invoke `hook()` or 7 | -- `unhook()` respectively so you can define what setting 8 | -- and unsetting a property means. 9 | type VHook : { 10 | hook: (node: DOMElement, propertyName: String) => void, 11 | unhook: (node: DOMElement, propertyName: String) => void 12 | } 13 | 14 | type VPropertyName : String 15 | type VPropertyValue : String | Boolean | Number 16 | 17 | -- A VProperties is a data structure that represents the 18 | -- set of properties that can be attached to a virtual node 19 | -- 20 | -- Properties can be many things. The simplest case is a string 21 | -- property name and string or bool or number property value 22 | -- 23 | -- You can also have hooks in your properties. You give a 24 | -- string hook name and a hook value. The hook will then 25 | -- be invoked as part of `patch()` and `createElement()` 26 | -- which allows you to set custom properties. 27 | -- 28 | -- Next up, attributes and style are treated slightly 29 | -- differently. These keys are expected to have objects 30 | -- of string keys and string values and will have the 31 | -- correct `patch()` and `createElement()` logic for 32 | -- setting attributes and styles. 33 | -- 34 | -- Finally properties can also have nested arbitrary objects 35 | -- of some key name to some value that is an object of 36 | -- string to bool or number or string. 37 | type VProperties : 38 | Object & 39 | Object & 40 | Object<"attributes", 41 | Object> & 42 | Object<"style", 43 | Object> & 44 | Object 48 | 49 | -- A VNode is a data structure that represents a virtual node 50 | -- in a virtual tree. 51 | -- 52 | -- A virtual node consists of a tagName, a set of properties 53 | -- and a list of children. It also has meta data attached, 54 | -- namely a key and a namespace. 55 | type VNode : { 56 | tagName: String, 57 | properties: VProperties 58 | children: Array, 59 | key: String | undefined, 60 | namespace: String | null 61 | } 62 | 63 | -- A VText is a data structure representing a text node in 64 | -- a virtual tree. 65 | type VText : { 66 | text: String, 67 | type: "VirtualText" 68 | } 69 | 70 | -- A Widget is a custom data structure for representing an 71 | -- object in a virtual tree. 72 | -- 73 | -- A Widget allows you to fully specify the semantics of 74 | -- intitialization of the object, updating of the object 75 | -- and destruction of the object. 76 | -- 77 | -- A Widget should generally be used for custom, performance 78 | -- critical code. 79 | type Widget : { 80 | type: "Widget", 81 | init: () => DOMElement, 82 | update: (previous: Widget, domNode: DOMElement) => void, 83 | destroy: (node: DOMElement) => void 84 | } 85 | 86 | -- A Thunk is a custom data structure for representing an 87 | -- unevaluated node in a virtual tree. 88 | -- 89 | -- A Thunk allows you to overwrite the semantics of `diff()` 90 | -- by implementing your own render method that takes the 91 | -- previous node in the previous virtual tree. 92 | -- 93 | -- Inside `render()` you can check whether the previous node 94 | -- is conceptually the same as the current node and return 95 | -- `previous.vnode` instead of re-computing and recreating 96 | -- an equivelant vnode. 97 | -- 98 | -- This allows you to implement caching in the virtual tree 99 | -- and has significant performance improvements 100 | type Thunk : { 101 | type: "Thunk", 102 | vnode: VTree, 103 | render: (previous: VTree | null) => VTree 104 | } 105 | 106 | -- A VTree is the union of all the types of nodes that can 107 | -- exist in a virtual tree. 108 | type VTree : VText | VNode | Widget | Thunk 109 | 110 | -- A VPatch represents a patch object that is returned from 111 | -- `diff()`. This patch object can be applied to an 112 | -- existing DOM element tree. 113 | type VPatch := { 114 | type: Number, 115 | vNode: VNode, 116 | patch: Any, 117 | type: 'VirtualPatch' 118 | } 119 | 120 | virtual-dom/create-element : (vnode: VTree, opts: { 121 | document?: DOMDocument, 122 | warn?: Boolean 123 | }) => DOMElement | DOMTextNode 124 | 125 | virtual-dom/diff : (left: VTree, right: VTree) => Array 126 | 127 | virtual-dom/h : ( 128 | tagSelector: String, 129 | properties?: { 130 | key: String, 131 | namespace: String 132 | } & VProperties, 133 | children?: Array | Vtree | textContent: String 134 | ) => VNode 135 | 136 | virtual-dom/patch : ( 137 | rootNode: DOMElement, 138 | patches: Array 139 | ) => newRootNode: DOMElement 140 | -------------------------------------------------------------------------------- /lib/vendor/docs/README.md: -------------------------------------------------------------------------------- 1 | # virtual-dom documentation 2 | This documentation is aimed at people who would like to work with virtual-dom directly, or gain a deeper understanding of how their virtual-dom based framework works. If you would rather be working at a higher level, you may find the [mercury framework](https://github.com/Raynos/mercury) a better place to start. 3 | 4 | ## Overview 5 | 6 | virtual-dom consists of four main parts: 7 | 8 | [vtree](https://github.com/Matt-Esch/virtual-dom/tree/master/vtree) - responsible for diffing two virtual representations DOM nodes 9 | [vdom](https://github.com/Matt-Esch/virtual-dom/tree/master/vdom) - responsible for taking the [patch](https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/patch.js) genereated by [vtree/diff](https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js) and using it to modify the rendered DOM 10 | [vnode](https://github.com/Matt-Esch/virtual-dom/tree/master/vnode) - virtual representation of dom elements 11 | [virtual-hyperscript](https://github.com/Matt-Esch/virtual-dom/tree/master/virtual-hyperscript) - an interface for generating VNodes from simple data structures 12 | 13 | Newcomers should start by reading the VNode and VText documentation, as virtual nodes are central to the operation of virtual-dom. Hooks, Thunks, and Widgets are more advanced features, and you will find both documentation of their interfaces and several examples on their respective pages. 14 | 15 | ## Contents 16 | 17 | [VNode](vnode.md) - A representation of a DOM element 18 | 19 | [VText](vtext.md) - A representation of a text node 20 | 21 | [Hooks](hooks.md) - The mechanism for executing functions after a new node has been created 22 | 23 | [Thunk](thunk.md) - The mechanism for taking control of diffing a specific DOM sub-tree 24 | 25 | [Widget](widget.md) - The mechanism for taking control of node patching: DOM Element creation, updating, and removal. 26 | 27 | [CSS animations](css-animations.md) 28 | -------------------------------------------------------------------------------- /lib/vendor/docs/css-animations.md: -------------------------------------------------------------------------------- 1 | # CSS animations 2 | Based on a discusison in [question](https://github.com/Matt-Esch/virtual-dom/issues/104#issuecomment-68611995). 3 | 4 | You should be activating the CSS transitions using hooks and nextTick. Here is a basic example of inserting an element through transition: 5 | ```javascript 6 | Item.render = function(state) { 7 | return h('li', { 8 | 'class' : new ItemInsertHook('item'), 9 | }, [ 10 | h('div.text', state.text), 11 | h('button', { 12 | 'ev-click' : mercury.event(...) 13 | }, 'Remove or something...'), 14 | ]); 15 | } 16 | 17 | function ItemInsertHook(value) { 18 | this.value = value; 19 | } 20 | 21 | ItemInsertHook.prototype.hook = function(elem, propName) { 22 | 23 | // Here we want to see if this is a newly created dom element 24 | // or an element that was inserted before and is revisited. 25 | // The way to check for that in this case is see if the element is 26 | // attached to the dom. 27 | // Newly created element will not be attached to the dom when hook is executed. 28 | 29 | if (!document.body.contains(elem)) { 30 | elem.setAttribute(propName, this.value + ' inserting'); 31 | 32 | nextTick(function () { 33 | elem.setAttribute(propName, this.value + ''); 34 | }.bind(this)) 35 | } 36 | } 37 | 38 | //Elswhere at the top level of application: 39 | function renderItemsList(state) { 40 | return h('ul#item-list', [ 41 | state.items.map(function(item) {return Item.render(item);}) 42 | ]); 43 | } 44 | ``` 45 | 46 | And css: 47 | ```css 48 | li.item.inserting { opacity : 0.01; } 49 | li.item { transition: opacity 0.2s ease-in-out; } 50 | li.item { opacity : 0.99; } 51 | ``` 52 | 53 | See full example on requirebin: http://requirebin.com/?gist=250e6e59aa40d5ff0fcc 54 | 55 | In a more complex case it may be necessary to encode animation state in the model. You should know exactly which nodes you wish to animate based on your data, and you should use that data to add a transition hook based on next tick. 56 | 57 | You don't have to do animations with JS, prefer CSS transitions, but you do need to model your expectations properly in your data model and apply transitions to the nodes using that data. Generic transitions that rely on the way in which the DOM is mutated isn't going to work consistently. 58 | 59 | For example, if you want an inserted transition, you might add a wasInserted boolean flag to your model. 60 | 61 | On rendering that item, if wasInserted is true, you add an animation hook which, on next tick, adds a css class like .inserted. You code your CSS transitions against this class. 62 | 63 | On next tick your hook will add the class and the transition will happen. Further to that you will want to clear the wasInserted flag, probably also on next tick. 64 | 65 | There are tons of these flag situations which should not trigger re-render. I think mercury needs to add something like this as a primitive type but that's outside the scope of virtual-dom. 66 | 67 | But as you can see, the animation works because you have recorded the state and necessity for the transition, and did not rely simply on the insertion of the node to trigger that animation. 68 | -------------------------------------------------------------------------------- /lib/vendor/docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | These are frequently asked questions. If you have any questions 4 | not on this list, then **please** [open an issue and ask][new-issue]. 5 | 6 | [new-issue]: https://github.com/Matt-Esch/virtual-dom/issues/new 7 | 8 | ## How do I do custom rendering 9 | 10 | If you want to embed a custom piece of rendering machinery in 11 | the virtual DOM you can use widgets. 12 | 13 | A widget is a object with an `init()` and `update()` method and a `type` attribute with the "Widget" value. 14 | 15 | ```js 16 | function GoogleMapWidget(initialPosition) { 17 | this.type = 'Widget' 18 | this.position = initialPosition 19 | } 20 | 21 | GoogleMapWidget.prototype.init = function () { 22 | var elem = document.createElement('div') 23 | this.map = GoogleMap(elem) 24 | this.map.setPosition(this.position) 25 | return elem 26 | } 27 | 28 | GoogleMapWidget.prototype.update = function (prev, elem) { 29 | this.map = this.map || prev.map 30 | this.map.setPosition(this.position) 31 | } 32 | 33 | h('div', [ 34 | new GoogleMapWidget({ x: 0, y: 0 }) 35 | ]) 36 | ``` 37 | 38 | The rules for a widget is that the first time it's seen we call 39 | `init()`, we expect `init()` to return a DOM element. 40 | 41 | The DOM element you return is yours to keep & mutate, virtual 42 | DOM will not touch it or its children. However you should never 43 | touch `elem.parentNode` as that does not belong to the widget 44 | 45 | The second method is `update()` if we see a widget and we have 46 | the same widget in the previous tree we call `update(prev, elem)` 47 | instead. `update()` is a good place to copy over any stateful 48 | things from the `prev` widget instance and then to update the 49 | state with the current properties by accessing them with `this` 50 | 51 | For another example of a widget see the 52 | [canvas demo](examples/canvas.js) 53 | 54 | ## How do I update custom properties 55 | 56 | If you want to update a custom property on a DOM element, like 57 | calling `setAttribute()` or calling `focus()` then you can 58 | use a hook 59 | 60 | ```js 61 | function AttributeHook(value) { 62 | this.value = value 63 | } 64 | 65 | AttributeHook.prototype.hook = function (elem, prop) { 66 | elem.setAttribute(prop, this.value) 67 | } 68 | 69 | h('div', { 70 | class: new AttributeHook('some-class-name') 71 | }) 72 | ``` 73 | 74 | For another example of a hook see 75 | [TodoMVC focus hook](https://github.com/Raynos/mercury/blob/master/examples/lib/focus-hook.js) 76 | 77 | ## How do I get life cycle hooks for VNodes 78 | 79 | `VNode` only exposes one life cycle mechanism. which is the hook 80 | mechanism. 81 | 82 | ### Hooking into VNode creation 83 | 84 | If you want to do some custom DOM logic immediately once a VNode 85 | is created you can add a hook, I normally add them to 86 | `ev-foo` properties. 87 | 88 | ```js 89 | function MyHook(args) { 90 | this.args = args 91 | } 92 | 93 | MyHook.prototype.hook = function (elem, propName) { 94 | /* do DOM stuff */ 95 | } 96 | 97 | h('div', { 98 | 'ev-myHook': new MyHook(args) 99 | }) 100 | ``` 101 | 102 | ### Hooking into VNode after it's in the DOM 103 | 104 | If you want to a hook to fire after the DOM element has been 105 | appended into the DOM you will have to delay the hook manually 106 | 107 | ```js 108 | function MyHook(args) { 109 | this.args = args 110 | } 111 | 112 | MyHook.prototype.hook = function (elem, propName) { 113 | setImmediate(function () { 114 | // DOM element will be in the real DOM by now 115 | // do DOM stuff 116 | }) 117 | } 118 | 119 | h('div', { 120 | 'ev-myHook': new MyHook(args) 121 | }) 122 | ``` 123 | 124 | We only have one type of hook as maintaining both life cycles 125 | separately is very complex when it can simply be done at 126 | the user level with a `setImmediate` 127 | 128 | We have the hook fire immediately by default because sometimes 129 | you need to run DOM logic BEFORE the element is in the DOM. 130 | 131 | Firing the hook when the element is in the DOM makes it 132 | impossible to fire it when it's not in the DOM. 133 | 134 | -------------------------------------------------------------------------------- /lib/vendor/docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | Hooks are functions that execute after turning a VNode into an Element. They are set by passing a VNode any property key with an object that has a function called hook that has not been directly assigned. The simplest way to ensure that a function isn't directly assigned is for it to be a prototype on an object. 3 | 4 | Will Work 5 | ```javascript 6 | var Hook = function(){} 7 | Hook.prototype.hook = function(node, propertyName, previousValue) { 8 | console.log("Hello, World") 9 | } 10 | createElement(h('div', { "my-hook": new Hook() })) 11 | ``` 12 | 13 | Won't Work 14 | ```javascript 15 | var hook = { hook: function(node, propertyName, previousvalue) { console.log("Hello, World") } } 16 | createElement(h('div', { "my-hook": hook })) 17 | ``` 18 | 19 | ## Arguments 20 | Your hook function will be given the following arguments 21 | 22 | **node** 23 | The Element generated from the VNode. 24 | 25 | ```javascript 26 | var Hook = function(){} 27 | Hook.prototype.hook = function(node, propertyName, previousValue) { 28 | console.log("type: ", node.constructor.name) 29 | } 30 | createElement(h('div', { "my-hook": new Hook() })) 31 | // logs "type: HTMLDivElement" 32 | ``` 33 | 34 | **propertyName** 35 | String, key of the property this hook was assigned from. 36 | 37 | ```javascript 38 | var Hook = function(){} 39 | Hook.prototype.hook = function(node, propertyName, previousValue) { 40 | console.log("name: " + propertyName) 41 | } 42 | createElement(h('div', { "my-hook": new Hook() })) 43 | // logs "name: my-hook" 44 | ``` 45 | 46 | **previousValue** *(optional)* 47 | If this node is having just its properties changed during a patch, it will receive the value that was previously assigned to the key. Otherwise, this argument will be undefined. 48 | 49 | ## Other Examples 50 | [virtual-hyperscript](https://github.com/Matt-Esch/virtual-dom/tree/master/virtual-hyperscript) uses hooks for several things, including setting up events and returning focus to input elements after a render. You can view these hooks in the [virtual-hyperscript/hooks](https://github.com/Matt-Esch/virtual-dom/tree/master/virtual-hyperscript/hooks) folder. 51 | -------------------------------------------------------------------------------- /lib/vendor/docs/vtext.md: -------------------------------------------------------------------------------- 1 | # VText 2 | VText is a representation of a Text node. 3 | 4 | In virtual-dom, a VText is turned into an actual text node with the `createElement` function. You can read the code at [vdom/create-element](https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/create-element.js) 5 | 6 | `createElement` turns a VText into a Text node using [document#createTextNode](https://developer.mozilla.org/en-US/docs/Web/API/document.createTextNode). 7 | 8 | ## Full Example 9 | ```javascript 10 | var createElement = require("virtual-dom").create 11 | var VNode = require("virtual-dom/vnode/vnode") 12 | var VText = require("virtual-dom/vnode/vtext") 13 | var myText = new VText("Hello, World") 14 | 15 | // Pass our VText as a child of our VNode 16 | var myNode = new VNode("div", { id: "my-node" }, [myText]) 17 | 18 | var myElem = createElement(myNode) 19 | document.body.appendChild(myElem) 20 | // Will result in a dom string that looks like
Hello, World
21 | ``` 22 | 23 | ## Arguments 24 | **text** 25 | The string you would like the text node to contain. 26 | 27 | ## HTML Injection 28 | `document#createTextNode` will defend against HTML injection. You could use the innerHTML property, but it will most likely break virtual dom. 29 | 30 | ```javascript 31 | escapedText = new VText('Example') 32 | escapedNode = new VNode('div', null, [escapedText]) 33 | // Will enter the dom as
<span>Example</span>
34 | 35 | unescapedNode = new VNode('div', { innerHTML: "Example" }) 36 | // Will enter the dom as
Example
37 | // You should probably never do this 38 | ``` 39 | -------------------------------------------------------------------------------- /lib/vendor/docs/widget.md: -------------------------------------------------------------------------------- 1 | # Widget 2 | Widgets are used to take control of the patching process, allowing the user to create stateful components, control sub-tree rendering, and hook into element removal. 3 | 4 | ## Widget Interface 5 | **type** 6 | Must be the string "Widget" 7 | 8 | **init** 9 | The function called when the widget is being created. Should return a DOM Element. 10 | 11 | **update** 12 | The function called when the widget is being updated. 13 | 14 | **destroy** 15 | The function called when the widget is being removed from the dom. 16 | 17 | ```javascript 18 | // Boilerplate widget 19 | var Widget = function (){} 20 | Widget.prototype.type = "Widget" 21 | Widget.prototype.init = function(){} 22 | Widget.prototype.update = function(previous, domNode){} 23 | Widget.prototype.destroy = function(domNode){} 24 | ``` 25 | 26 | ### Update Arguments 27 | The arguments passed to `Widget#update` 28 | 29 | **previous** 30 | The previous Widget 31 | 32 | **domNode** 33 | The previous DOM Element associated with this widget 34 | 35 | ### Destroy Argument 36 | The argument passed to `Widget#destroy` 37 | 38 | **domNode** 39 | The HTMLElement associated with the widget that will be removed 40 | 41 | ## Full Example 42 | This example demonstrates one way to pass local component state and use `init`, `update`, and `destroy` to create a widget that counts each time it tries to update, only showing the odd numbers. 43 | 44 | ```javascript 45 | var diff = require("virtual-dom").diff 46 | var patch = require("virtual-dom").patch 47 | var h = require("virtual-dom").h 48 | var createElement = require("virtual-dom").create 49 | 50 | var OddCounterWidget = function() {} 51 | OddCounterWidget.prototype.type = "Widget" 52 | OddCounterWidget.prototype.count = 1 53 | OddCounterWidget.prototype.init = function() { 54 | // With widgets, you can use any method you would like to generate the DOM Elements. 55 | // We could get the same result using: 56 | // return createElement(h("div", "Count is: " + this.count)) 57 | var divElem = document.createElement("div") 58 | var textElem = document.createTextNode("Count is: " + this.count) 59 | divElem.appendChild(textElem) 60 | return divElem 61 | } 62 | 63 | OddCounterWidget.prototype.update = function(previous, domNode) { 64 | this.count = previous.count + 1 65 | // Only re-render if the current count is odd 66 | if (this.count % 2) { 67 | // Returning a new element from widget#update 68 | // will replace the previous node 69 | return this.init() 70 | } 71 | return null 72 | } 73 | 74 | OddCounterWidget.prototype.destroy = function(domNode) { 75 | // While you can do any cleanup you would like here, 76 | // we don't really have to do anything in this case. 77 | // Instead, we'll log the current count 78 | console.log(this.count) 79 | } 80 | 81 | var myCounter = new OddCounterWidget() 82 | var currentNode = myCounter 83 | var rootNode = createElement(currentNode) 84 | 85 | // A simple function to diff your widgets, and patch the dom 86 | var update = function(nextNode) { 87 | var patches = diff(currentNode, nextNode) 88 | rootNode = patch(rootNode, patches) 89 | currentNode = nextNode 90 | } 91 | 92 | document.body.appendChild(rootNode) 93 | setInterval(function(){ 94 | update(new OddCounterWidget()) 95 | }, 1000) 96 | ``` 97 | -------------------------------------------------------------------------------- /lib/vendor/h.js: -------------------------------------------------------------------------------- 1 | var h = require("./virtual-hyperscript/index.js") 2 | 3 | module.exports = h 4 | -------------------------------------------------------------------------------- /lib/vendor/index.js: -------------------------------------------------------------------------------- 1 | var diff = require("./diff.js") 2 | var patch = require("./patch.js") 3 | var h = require("./h.js") 4 | var create = require("./create-element.js") 5 | var VNode = require('./vnode/vnode.js') 6 | var VText = require('./vnode/vtext.js') 7 | 8 | module.exports = { 9 | diff: diff, 10 | patch: patch, 11 | h: h, 12 | create: create, 13 | VNode: VNode, 14 | VText: VText 15 | } 16 | -------------------------------------------------------------------------------- /lib/vendor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-dom", 3 | "version": "2.1.1", 4 | "description": "A batched diff-based DOM rendering strategy", 5 | "keywords": [ 6 | "virtual", 7 | "dom", 8 | "vdom", 9 | "vtree", 10 | "diff", 11 | "patch", 12 | "browser" 13 | ], 14 | "author": "Matt-Esch ", 15 | "repository": "git://github.com/Matt-Esch/virtual-dom.git", 16 | "main": "index", 17 | "homepage": "https://github.com/Matt-Esch/virtual-dom", 18 | "contributors": [ 19 | { 20 | "name": "Matt-Esch" 21 | } 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/Matt-Esch/virtual-dom/issues", 25 | "email": "matt@mattesch.info" 26 | }, 27 | "dependencies": { 28 | "browser-split": "0.0.1", 29 | "error": "^4.3.0", 30 | "ev-store": "^7.0.0", 31 | "global": "^4.3.0", 32 | "is-object": "^1.0.1", 33 | "next-tick": "^0.2.2", 34 | "x-is-array": "0.1.0", 35 | "x-is-string": "0.1.0" 36 | }, 37 | "devDependencies": { 38 | "browserify": "^9.0.7", 39 | "istanbul": "^0.3.13", 40 | "min-document": "^2.14.0", 41 | "opn": "^1.0.1", 42 | "run-browser": "^2.0.2", 43 | "tap-dot": "^1.0.0", 44 | "tap-spec": "^3.0.0", 45 | "tape": "^4.0.0", 46 | "zuul": "^2.1.1" 47 | }, 48 | "license": "MIT", 49 | "scripts": { 50 | "test": "node ./test/index.js | tap-spec", 51 | "dot": "node ./test/index.js | tap-dot", 52 | "start": "node ./index.js", 53 | "cover": "istanbul cover --report html --print detail ./test/index.js", 54 | "view-cover": "istanbul report html && opn ./coverage/index.html", 55 | "browser": "run-browser test/index.js", 56 | "phantom": "run-browser test/index.js -b | tap-spec", 57 | "dist": "browserify --standalone virtual-dom index.js > dist/virtual-dom.js", 58 | "travis-test": "npm run phantom && npm run cover && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)", 59 | "release": "npm run release-patch", 60 | "release-patch": "git checkout master && npm version patch && git push origin master --tags && npm publish", 61 | "release-minor": "git checkout master && npm version minor && git push origin master --tags && npm publish", 62 | "release-major": "git checkout master && npm version major && git push origin master --tags && npm publish" 63 | }, 64 | "testling": { 65 | "files": "test/*.js", 66 | "browsers": [ 67 | "ie/8..latest", 68 | "firefox/17..latest", 69 | "firefox/nightly", 70 | "chrome/22..latest", 71 | "chrome/canary", 72 | "opera/12..latest", 73 | "opera/next", 74 | "safari/5.1..latest", 75 | "ipad/6.0..latest", 76 | "iphone/6.0..latest", 77 | "android-browser/4.2..latest" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/vendor/patch.js: -------------------------------------------------------------------------------- 1 | var patch = require("./vdom/patch.js") 2 | 3 | module.exports = patch 4 | -------------------------------------------------------------------------------- /lib/vendor/vdom/README.md: -------------------------------------------------------------------------------- 1 | # vdom 2 | 3 | A DOM render and patch algorithm for vtree 4 | 5 | ## Motivation 6 | 7 | Given a `vtree` structure representing a DOM structure, we would like to either 8 | render the structure to a DOM node using `vdom/create-element` or we would like 9 | to update the DOM using the results of `vtree/diff` by patching the DOM with 10 | `vdom/patch` 11 | 12 | ## Example 13 | 14 | ```js 15 | var h = require("virtual-dom/h") 16 | var diff = require("virtual-dom/diff") 17 | 18 | var createElement = require("virtual-dom/create-element") 19 | var patch = require("virtual-dom/patch") 20 | 21 | var leftNode = h("div") 22 | var rightNode = h("text") 23 | 24 | // Render the left node to a DOM node 25 | var rootNode = createElement(leftNode) 26 | document.body.appendChild(rootNode) 27 | 28 | // Update the DOM with the results of a diff 29 | var patches = diff(leftNode, rightNode) 30 | patch(rootNode, patches) 31 | ``` 32 | 33 | ## Installation 34 | 35 | `npm install virtual-dom` 36 | 37 | ## Contributors 38 | 39 | - Matt Esch 40 | 41 | ## MIT Licenced 42 | -------------------------------------------------------------------------------- /lib/vendor/vdom/apply-properties.js: -------------------------------------------------------------------------------- 1 | var isObject = require("is-object") 2 | var isHook = require("../vnode/is-vhook.js") 3 | 4 | module.exports = applyProperties 5 | 6 | function applyProperties(node, props, previous) { 7 | for (var propName in props) { 8 | var propValue = props[propName] 9 | 10 | if (propValue === undefined) { 11 | removeProperty(node, propName, propValue, previous); 12 | } else if (isHook(propValue)) { 13 | removeProperty(node, propName, propValue, previous) 14 | if (propValue.hook) { 15 | propValue.hook(node, 16 | propName, 17 | previous ? previous[propName] : undefined) 18 | } 19 | } else { 20 | if (isObject(propValue)) { 21 | patchObject(node, props, previous, propName, propValue); 22 | } else { 23 | node[propName] = propValue 24 | } 25 | } 26 | } 27 | } 28 | 29 | function removeProperty(node, propName, propValue, previous) { 30 | if (previous) { 31 | var previousValue = previous[propName] 32 | 33 | if (!isHook(previousValue)) { 34 | if (propName === "attributes") { 35 | for (var attrName in previousValue) { 36 | node.removeAttribute(attrName) 37 | } 38 | } else if (propName === "style") { 39 | for (var i in previousValue) { 40 | node.style[i] = "" 41 | } 42 | } else if (typeof previousValue === "string") { 43 | node[propName] = "" 44 | } else { 45 | node[propName] = null 46 | } 47 | } else if (previousValue.unhook) { 48 | previousValue.unhook(node, propName, propValue) 49 | } 50 | } 51 | } 52 | 53 | function patchObject(node, props, previous, propName, propValue) { 54 | var previousValue = previous ? previous[propName] : undefined 55 | 56 | // Set attributes 57 | if (propName === "attributes") { 58 | for (var attrName in propValue) { 59 | var attrValue = propValue[attrName] 60 | 61 | if (attrValue === undefined) { 62 | node.removeAttribute(attrName) 63 | } else { 64 | node.setAttribute(attrName, attrValue) 65 | } 66 | } 67 | 68 | return 69 | } 70 | 71 | if(previousValue && isObject(previousValue) && 72 | getPrototype(previousValue) !== getPrototype(propValue)) { 73 | node[propName] = propValue 74 | return 75 | } 76 | 77 | if (!isObject(node[propName])) { 78 | node[propName] = {} 79 | } 80 | 81 | var replacer = propName === "style" ? "" : undefined 82 | 83 | for (var k in propValue) { 84 | var value = propValue[k] 85 | node[propName][k] = (value === undefined) ? replacer : value 86 | } 87 | } 88 | 89 | function getPrototype(value) { 90 | if (Object.getPrototypeOf) { 91 | return Object.getPrototypeOf(value) 92 | } else if (value.__proto__) { 93 | return value.__proto__ 94 | } else if (value.constructor) { 95 | return value.constructor.prototype 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/vendor/vdom/create-element.js: -------------------------------------------------------------------------------- 1 | var document = require("global/document") 2 | 3 | var applyProperties = require("./apply-properties") 4 | 5 | var isVNode = require("../vnode/is-vnode.js") 6 | var isVText = require("../vnode/is-vtext.js") 7 | var isWidget = require("../vnode/is-widget.js") 8 | var handleThunk = require("../vnode/handle-thunk.js") 9 | 10 | module.exports = createElement 11 | 12 | function createElement(vnode, opts) { 13 | var doc = opts ? opts.document || document : document 14 | var warn = opts ? opts.warn : null 15 | 16 | vnode = handleThunk(vnode).a 17 | 18 | if (isWidget(vnode)) { 19 | return vnode.init() 20 | } else if (isVText(vnode)) { 21 | return doc.createTextNode(vnode.text) 22 | } else if (!isVNode(vnode)) { 23 | if (warn) { 24 | warn("Item is not a valid virtual dom node", vnode) 25 | } 26 | return null 27 | } 28 | 29 | var node = (vnode.namespace === null) ? 30 | doc.createElement(vnode.tagName) : 31 | doc.createElementNS(vnode.namespace, vnode.tagName) 32 | 33 | var props = vnode.properties 34 | applyProperties(node, props) 35 | 36 | var children = vnode.children 37 | 38 | for (var i = 0; i < children.length; i++) { 39 | var childNode = createElement(children[i], opts) 40 | if (childNode) { 41 | node.appendChild(childNode) 42 | } 43 | } 44 | 45 | return node 46 | } 47 | -------------------------------------------------------------------------------- /lib/vendor/vdom/dom-index.js: -------------------------------------------------------------------------------- 1 | // Maps a virtual DOM tree onto a real DOM tree in an efficient manner. 2 | // We don't want to read all of the DOM nodes in the tree so we use 3 | // the in-order tree indexing to eliminate recursion down certain branches. 4 | // We only recurse into a DOM node if we know that it contains a child of 5 | // interest. 6 | 7 | var noChild = {} 8 | 9 | module.exports = domIndex 10 | 11 | function domIndex(rootNode, tree, indices, nodes) { 12 | if (!indices || indices.length === 0) { 13 | return {} 14 | } else { 15 | indices.sort(ascending) 16 | return recurse(rootNode, tree, indices, nodes, 0) 17 | } 18 | } 19 | 20 | function recurse(rootNode, tree, indices, nodes, rootIndex) { 21 | nodes = nodes || {} 22 | 23 | 24 | if (rootNode) { 25 | if (indexInRange(indices, rootIndex, rootIndex)) { 26 | nodes[rootIndex] = rootNode 27 | } 28 | 29 | var vChildren = tree.children 30 | 31 | if (vChildren) { 32 | 33 | var childNodes = rootNode.childNodes 34 | 35 | for (var i = 0; i < tree.children.length; i++) { 36 | rootIndex += 1 37 | 38 | var vChild = vChildren[i] || noChild 39 | var nextIndex = rootIndex + (vChild.count || 0) 40 | 41 | // skip recursion down the tree if there are no nodes down here 42 | if (indexInRange(indices, rootIndex, nextIndex)) { 43 | recurse(childNodes[i], vChild, indices, nodes, rootIndex) 44 | } 45 | 46 | rootIndex = nextIndex 47 | } 48 | } 49 | } 50 | 51 | return nodes 52 | } 53 | 54 | // Binary search for an index in the interval [left, right] 55 | function indexInRange(indices, left, right) { 56 | if (indices.length === 0) { 57 | return false 58 | } 59 | 60 | var minIndex = 0 61 | var maxIndex = indices.length - 1 62 | var currentIndex 63 | var currentItem 64 | 65 | while (minIndex <= maxIndex) { 66 | currentIndex = ((maxIndex + minIndex) / 2) >> 0 67 | currentItem = indices[currentIndex] 68 | 69 | if (minIndex === maxIndex) { 70 | return currentItem >= left && currentItem <= right 71 | } else if (currentItem < left) { 72 | minIndex = currentIndex + 1 73 | } else if (currentItem > right) { 74 | maxIndex = currentIndex - 1 75 | } else { 76 | return true 77 | } 78 | } 79 | 80 | return false; 81 | } 82 | 83 | function ascending(a, b) { 84 | return a > b ? 1 : -1 85 | } 86 | -------------------------------------------------------------------------------- /lib/vendor/vdom/patch-op.js: -------------------------------------------------------------------------------- 1 | var applyProperties = require("./apply-properties") 2 | 3 | var isWidget = require("../vnode/is-widget.js") 4 | var VPatch = require("../vnode/vpatch.js") 5 | 6 | var updateWidget = require("./update-widget") 7 | 8 | module.exports = applyPatch 9 | 10 | function applyPatch(vpatch, domNode, renderOptions) { 11 | var type = vpatch.type 12 | var vNode = vpatch.vNode 13 | var patch = vpatch.patch 14 | 15 | switch (type) { 16 | case VPatch.REMOVE: 17 | return removeNode(domNode, vNode) 18 | case VPatch.INSERT: 19 | return insertNode(domNode, patch, renderOptions) 20 | case VPatch.VTEXT: 21 | return stringPatch(domNode, vNode, patch, renderOptions) 22 | case VPatch.WIDGET: 23 | return widgetPatch(domNode, vNode, patch, renderOptions) 24 | case VPatch.VNODE: 25 | return vNodePatch(domNode, vNode, patch, renderOptions) 26 | case VPatch.ORDER: 27 | reorderChildren(domNode, patch) 28 | return domNode 29 | case VPatch.PROPS: 30 | applyProperties(domNode, patch, vNode.properties) 31 | return domNode 32 | case VPatch.THUNK: 33 | return replaceRoot(domNode, 34 | renderOptions.patch(domNode, patch, renderOptions)) 35 | default: 36 | return domNode 37 | } 38 | } 39 | 40 | function removeNode(domNode, vNode) { 41 | var parentNode = domNode.parentNode 42 | 43 | if (parentNode) { 44 | parentNode.removeChild(domNode) 45 | } 46 | 47 | destroyWidget(domNode, vNode); 48 | 49 | return null 50 | } 51 | 52 | function insertNode(parentNode, vNode, renderOptions) { 53 | var newNode = renderOptions.render(vNode, renderOptions) 54 | 55 | if (parentNode) { 56 | parentNode.appendChild(newNode) 57 | } 58 | 59 | return parentNode 60 | } 61 | 62 | function stringPatch(domNode, leftVNode, vText, renderOptions) { 63 | var newNode 64 | 65 | if (domNode.nodeType === 3) { 66 | domNode.nodeValue = vText.text; 67 | newNode = domNode 68 | } else { 69 | var parentNode = domNode.parentNode 70 | newNode = renderOptions.render(vText, renderOptions) 71 | 72 | if (parentNode && newNode !== domNode) { 73 | parentNode.replaceChild(newNode, domNode) 74 | } 75 | } 76 | 77 | return newNode 78 | } 79 | 80 | function widgetPatch(domNode, leftVNode, widget, renderOptions) { 81 | var updating = updateWidget(leftVNode, widget) 82 | var newNode 83 | 84 | if (updating) { 85 | newNode = widget.update(leftVNode, domNode) || domNode 86 | } else { 87 | newNode = renderOptions.render(widget, renderOptions) 88 | } 89 | 90 | var parentNode = domNode.parentNode 91 | 92 | if (parentNode && newNode !== domNode) { 93 | parentNode.replaceChild(newNode, domNode) 94 | } 95 | 96 | if (!updating) { 97 | destroyWidget(domNode, leftVNode) 98 | } 99 | 100 | return newNode 101 | } 102 | 103 | function vNodePatch(domNode, leftVNode, vNode, renderOptions) { 104 | var parentNode = domNode.parentNode 105 | var newNode = renderOptions.render(vNode, renderOptions) 106 | 107 | if (parentNode && newNode !== domNode) { 108 | parentNode.replaceChild(newNode, domNode) 109 | } 110 | 111 | return newNode 112 | } 113 | 114 | function destroyWidget(domNode, w) { 115 | if (typeof w.destroy === "function" && isWidget(w)) { 116 | w.destroy(domNode) 117 | } 118 | } 119 | 120 | function reorderChildren(domNode, moves) { 121 | var childNodes = domNode.childNodes 122 | var keyMap = {} 123 | var node 124 | var remove 125 | var insert 126 | 127 | for (var i = 0; i < moves.removes.length; i++) { 128 | remove = moves.removes[i] 129 | node = childNodes[remove.from] 130 | if (remove.key) { 131 | keyMap[remove.key] = node 132 | } 133 | domNode.removeChild(node) 134 | } 135 | 136 | var length = childNodes.length 137 | for (var j = 0; j < moves.inserts.length; j++) { 138 | insert = moves.inserts[j] 139 | node = keyMap[insert.key] 140 | // this is the weirdest bug i've ever seen in webkit 141 | domNode.insertBefore(node, insert.to >= length++ ? null : childNodes[insert.to]) 142 | } 143 | } 144 | 145 | function replaceRoot(oldRoot, newRoot) { 146 | if (oldRoot && newRoot && oldRoot !== newRoot && oldRoot.parentNode) { 147 | oldRoot.parentNode.replaceChild(newRoot, oldRoot) 148 | } 149 | 150 | return newRoot; 151 | } 152 | -------------------------------------------------------------------------------- /lib/vendor/vdom/patch.js: -------------------------------------------------------------------------------- 1 | var document = require("global/document") 2 | var isArray = require("x-is-array") 3 | 4 | var render = require("./create-element") 5 | var domIndex = require("./dom-index") 6 | var patchOp = require("./patch-op") 7 | module.exports = patch 8 | 9 | function patch(rootNode, patches, renderOptions) { 10 | renderOptions = renderOptions || {} 11 | renderOptions.patch = renderOptions.patch && renderOptions.patch !== patch 12 | ? renderOptions.patch 13 | : patchRecursive 14 | renderOptions.render = renderOptions.render || render 15 | 16 | return renderOptions.patch(rootNode, patches, renderOptions) 17 | } 18 | 19 | function patchRecursive(rootNode, patches, renderOptions) { 20 | var indices = patchIndices(patches) 21 | 22 | if (indices.length === 0) { 23 | return rootNode 24 | } 25 | 26 | var index = domIndex(rootNode, patches.a, indices) 27 | var ownerDocument = rootNode.ownerDocument 28 | 29 | if (!renderOptions.document && ownerDocument !== document) { 30 | renderOptions.document = ownerDocument 31 | } 32 | 33 | for (var i = 0; i < indices.length; i++) { 34 | var nodeIndex = indices[i] 35 | rootNode = applyPatch(rootNode, 36 | index[nodeIndex], 37 | patches[nodeIndex], 38 | renderOptions) 39 | } 40 | 41 | return rootNode 42 | } 43 | 44 | function applyPatch(rootNode, domNode, patchList, renderOptions) { 45 | if (!domNode) { 46 | return rootNode 47 | } 48 | 49 | var newNode 50 | 51 | if (isArray(patchList)) { 52 | for (var i = 0; i < patchList.length; i++) { 53 | newNode = patchOp(patchList[i], domNode, renderOptions) 54 | 55 | if (domNode === rootNode) { 56 | rootNode = newNode 57 | } 58 | } 59 | } else { 60 | newNode = patchOp(patchList, domNode, renderOptions) 61 | 62 | if (domNode === rootNode) { 63 | rootNode = newNode 64 | } 65 | } 66 | 67 | return rootNode 68 | } 69 | 70 | function patchIndices(patches) { 71 | var indices = [] 72 | 73 | for (var key in patches) { 74 | if (key !== "a") { 75 | indices.push(Number(key)) 76 | } 77 | } 78 | 79 | return indices 80 | } 81 | -------------------------------------------------------------------------------- /lib/vendor/vdom/update-widget.js: -------------------------------------------------------------------------------- 1 | var isWidget = require("../vnode/is-widget.js") 2 | 3 | module.exports = updateWidget 4 | 5 | function updateWidget(a, b) { 6 | if (isWidget(a) && isWidget(b)) { 7 | if ("name" in a && "name" in b) { 8 | return a.id === b.id 9 | } else { 10 | return a.init === b.init 11 | } 12 | } 13 | 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/README.md: -------------------------------------------------------------------------------- 1 | # virtual-hyperscript 2 | 3 | A DSL for creating virtual trees 4 | 5 | ## Example 6 | 7 | ```js 8 | var h = require('virtual-dom/h') 9 | 10 | var tree = h('div.foo#some-id', [ 11 | h('span', 'some text'), 12 | h('input', { type: 'text', value: 'foo' }) 13 | ]) 14 | ``` 15 | 16 | ## Docs 17 | 18 | See [hyperscript](https://github.com/dominictarr/hyperscript) which has the 19 | same interface. 20 | 21 | Except `virtual-hyperscript` returns a virtual DOM tree instead of a DOM 22 | element. 23 | 24 | ### `h(selector, properties, children)` 25 | 26 | `h()` takes a selector, an optional properties object and an 27 | optional array of children or a child that is a string. 28 | 29 | If you pass it a selector like `span.foo.bar#some-id` it will 30 | parse the selector and change the `id` and `className` 31 | properties of the `properties` object. 32 | 33 | If you pass it an array of `children` it will have child 34 | nodes, normally you want to create children with `h()`. 35 | 36 | If you pass it a string it will create an array containing 37 | a single child node that is a text element. 38 | 39 | ### Special properties in `h()` 40 | 41 | #### `key` 42 | 43 | If you call `h` with `h('div', { key: someKey })` it will 44 | set a key on the return `VNode`. This `key` is not a normal 45 | DOM property but is a virtual-dom optimization hint. 46 | 47 | It basically tells virtual-dom to re-order DOM nodes instead of 48 | mutating them. 49 | 50 | #### `namespace` 51 | 52 | If you call `h` with `h('div', { namespace: "http://www.w3.org/2000/svg" })` 53 | it will set the namespace on the returned `VNode`. This 54 | `namespace` is not a normal DOM property, instead it will 55 | cause `vdom` to create a DOM element with a namespace. 56 | 57 | #### `ev-*` 58 | 59 | **Note:** You must create an instance of `dom-delegator` for `ev-*` to work. 60 | 61 | If you call `h` with `h('div', { ev-click: function (ev) { } })` it 62 | will store the event handler on the dom element. It will not 63 | set a property `'ev-foo'` on the DOM element. 64 | 65 | This means that `dom-delegator` will recognise the event handler 66 | on that element and correctly call your handler when a click 67 | event happens. 68 | 69 | ## Installation 70 | 71 | `npm install virtual-dom` 72 | 73 | ## Contributors 74 | 75 | - Raynos 76 | - Matt Esch 77 | 78 | ## MIT Licenced 79 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/hooks/attribute-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = AttributeHook; 4 | 5 | function AttributeHook(namespace, value) { 6 | if (!(this instanceof AttributeHook)) { 7 | return new AttributeHook(namespace, value); 8 | } 9 | 10 | this.namespace = namespace; 11 | this.value = value; 12 | } 13 | 14 | AttributeHook.prototype.hook = function (node, prop, prev) { 15 | if (prev && prev.type === 'AttributeHook' && 16 | prev.value === this.value && 17 | prev.namespace === this.namespace) { 18 | return; 19 | } 20 | 21 | node.setAttributeNS(this.namespace, prop, this.value); 22 | }; 23 | 24 | AttributeHook.prototype.unhook = function (node, prop, next) { 25 | if (next && next.type === 'AttributeHook' && 26 | next.namespace === this.namespace) { 27 | return; 28 | } 29 | 30 | var colonPosition = prop.indexOf(':'); 31 | var localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop; 32 | node.removeAttributeNS(this.namespace, localName); 33 | }; 34 | 35 | AttributeHook.prototype.type = 'AttributeHook'; 36 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/hooks/ev-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EvStore = require('ev-store'); 4 | 5 | module.exports = EvHook; 6 | 7 | function EvHook(value) { 8 | if (!(this instanceof EvHook)) { 9 | return new EvHook(value); 10 | } 11 | 12 | this.value = value; 13 | } 14 | 15 | EvHook.prototype.hook = function (node, propertyName) { 16 | var es = EvStore(node); 17 | var propName = propertyName.substr(3); 18 | 19 | es[propName] = this.value; 20 | }; 21 | 22 | EvHook.prototype.unhook = function(node, propertyName) { 23 | var es = EvStore(node); 24 | var propName = propertyName.substr(3); 25 | 26 | es[propName] = undefined; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/hooks/focus-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require("global/document"); 4 | var nextTick = require("next-tick"); 5 | 6 | module.exports = MutableFocusHook; 7 | 8 | function MutableFocusHook() { 9 | if (!(this instanceof MutableFocusHook)) { 10 | return new MutableFocusHook(); 11 | } 12 | } 13 | 14 | MutableFocusHook.prototype.hook = function (node) { 15 | nextTick(function () { 16 | if (document.activeElement !== node) { 17 | node.focus(); 18 | } 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/hooks/soft-set-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = SoftSetHook; 4 | 5 | function SoftSetHook(value) { 6 | if (!(this instanceof SoftSetHook)) { 7 | return new SoftSetHook(value); 8 | } 9 | 10 | this.value = value; 11 | } 12 | 13 | SoftSetHook.prototype.hook = function (node, propertyName) { 14 | if (node[propertyName] !== this.value) { 15 | node[propertyName] = this.value; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isArray = require('x-is-array'); 4 | 5 | var VNode = require('../vnode/vnode.js'); 6 | var VText = require('../vnode/vtext.js'); 7 | var isVNode = require('../vnode/is-vnode'); 8 | var isVText = require('../vnode/is-vtext'); 9 | var isWidget = require('../vnode/is-widget'); 10 | var isHook = require('../vnode/is-vhook'); 11 | var isVThunk = require('../vnode/is-thunk'); 12 | 13 | var parseTag = require('./parse-tag.js'); 14 | var softSetHook = require('./hooks/soft-set-hook.js'); 15 | var evHook = require('./hooks/ev-hook.js'); 16 | 17 | module.exports = h; 18 | 19 | function h(tagName, properties, children) { 20 | var childNodes = []; 21 | var tag, props, key, namespace; 22 | 23 | if (!children && isChildren(properties)) { 24 | children = properties; 25 | props = {}; 26 | } 27 | 28 | props = props || properties || {}; 29 | tag = parseTag(tagName, props); 30 | 31 | // support keys 32 | if (props.hasOwnProperty('key')) { 33 | key = props.key; 34 | props.key = undefined; 35 | } 36 | 37 | // support namespace 38 | if (props.hasOwnProperty('namespace')) { 39 | namespace = props.namespace; 40 | props.namespace = undefined; 41 | } 42 | 43 | // fix cursor bug 44 | if (tag === 'INPUT' && 45 | !namespace && 46 | props.hasOwnProperty('value') && 47 | props.value !== undefined && 48 | !isHook(props.value) 49 | ) { 50 | props.value = softSetHook(props.value); 51 | } 52 | 53 | transformProperties(props); 54 | 55 | if (children !== undefined && children !== null) { 56 | addChild(children, childNodes, tag, props); 57 | } 58 | 59 | 60 | return new VNode(tag, props, childNodes, key, namespace); 61 | } 62 | 63 | function addChild(c, childNodes, tag, props) { 64 | if (typeof c === 'string') { 65 | childNodes.push(new VText(c)); 66 | } else if (typeof c === 'number') { 67 | childNodes.push(new VText(String(c))); 68 | } else if (isChild(c)) { 69 | childNodes.push(c); 70 | } else if (isArray(c)) { 71 | for (var i = 0; i < c.length; i++) { 72 | addChild(c[i], childNodes, tag, props); 73 | } 74 | } else if (c === null || c === undefined) { 75 | return; 76 | } else { 77 | throw UnexpectedVirtualElement({ 78 | foreignObject: c, 79 | parentVnode: { 80 | tagName: tag, 81 | properties: props 82 | } 83 | }); 84 | } 85 | } 86 | 87 | function transformProperties(props) { 88 | for (var propName in props) { 89 | if (props.hasOwnProperty(propName)) { 90 | var value = props[propName]; 91 | 92 | if (isHook(value)) { 93 | continue; 94 | } 95 | 96 | if (propName.substr(0, 3) === 'ev-') { 97 | // add ev-foo support 98 | props[propName] = evHook(value); 99 | } 100 | } 101 | } 102 | } 103 | 104 | function isChild(x) { 105 | return isVNode(x) || isVText(x) || isWidget(x) || isVThunk(x); 106 | } 107 | 108 | function isChildren(x) { 109 | return typeof x === 'string' || isArray(x) || isChild(x); 110 | } 111 | 112 | function UnexpectedVirtualElement(data) { 113 | var err = new Error(); 114 | 115 | err.type = 'virtual-hyperscript.unexpected.virtual-element'; 116 | err.message = 'Unexpected virtual child passed to h().\n' + 117 | 'Expected a VNode / Vthunk / VWidget / string but:\n' + 118 | 'got:\n' + 119 | errorString(data.foreignObject) + 120 | '.\n' + 121 | 'The parent vnode is:\n' + 122 | errorString(data.parentVnode) 123 | '\n' + 124 | 'Suggested fix: change your `h(..., [ ... ])` callsite.'; 125 | err.foreignObject = data.foreignObject; 126 | err.parentVnode = data.parentVnode; 127 | 128 | return err; 129 | } 130 | 131 | function UnsupportedValueType(data) { 132 | var err = new Error(); 133 | 134 | err.type = 'virtual-hyperscript.unsupported.value-type'; 135 | err.message = 'Unexpected value type for input passed to h().\n' + 136 | 'Expected a ' + 137 | errorString(data.expected) + 138 | ' but got:\n' + 139 | errorString(data.received) + 140 | '.\n' + 141 | 'The vnode is:\n' + 142 | errorString(data.Vnode) 143 | '\n' + 144 | 'Suggested fix: Cast the value passed to h() to a string using String(value).'; 145 | err.Vnode = data.Vnode; 146 | 147 | return err; 148 | } 149 | 150 | function errorString(obj) { 151 | try { 152 | return JSON.stringify(obj, null, ' '); 153 | } catch (e) { 154 | return String(obj); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/parse-tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var split = require('browser-split'); 4 | 5 | var classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/; 6 | var notClassId = /^\.|#/; 7 | 8 | module.exports = parseTag; 9 | 10 | function parseTag(tag, props) { 11 | if (!tag) { 12 | return 'DIV'; 13 | } 14 | 15 | var noId = !(props.hasOwnProperty('id')); 16 | 17 | var tagParts = split(tag, classIdSplit); 18 | var tagName = null; 19 | 20 | if (notClassId.test(tagParts[1])) { 21 | tagName = 'DIV'; 22 | } 23 | 24 | var classes, part, type, i; 25 | 26 | for (i = 0; i < tagParts.length; i++) { 27 | part = tagParts[i]; 28 | 29 | if (!part) { 30 | continue; 31 | } 32 | 33 | type = part.charAt(0); 34 | 35 | if (!tagName) { 36 | tagName = part; 37 | } else if (type === '.') { 38 | classes = classes || []; 39 | classes.push(part.substring(1, part.length)); 40 | } else if (type === '#' && noId) { 41 | props.id = part.substring(1, part.length); 42 | } 43 | } 44 | 45 | if (classes) { 46 | if (props.className) { 47 | classes.push(props.className); 48 | } 49 | 50 | props.className = classes.join(' '); 51 | } 52 | 53 | return props.namespace ? tagName : tagName.toUpperCase(); 54 | } 55 | -------------------------------------------------------------------------------- /lib/vendor/virtual-hyperscript/svg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isArray = require('x-is-array'); 4 | 5 | var h = require('./index.js'); 6 | 7 | 8 | var SVGAttributeNamespace = require('./svg-attribute-namespace'); 9 | var attributeHook = require('./hooks/attribute-hook'); 10 | 11 | var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 12 | 13 | module.exports = svg; 14 | 15 | function svg(tagName, properties, children) { 16 | if (!children && isChildren(properties)) { 17 | children = properties; 18 | properties = {}; 19 | } 20 | 21 | properties = properties || {}; 22 | 23 | // set namespace for svg 24 | properties.namespace = SVG_NAMESPACE; 25 | 26 | var attributes = properties.attributes || (properties.attributes = {}); 27 | 28 | for (var key in properties) { 29 | if (!properties.hasOwnProperty(key)) { 30 | continue; 31 | } 32 | 33 | var namespace = SVGAttributeNamespace(key); 34 | 35 | if (namespace === undefined) { // not a svg attribute 36 | continue; 37 | } 38 | 39 | var value = properties[key]; 40 | 41 | if (typeof value !== 'string' && 42 | typeof value !== 'number' && 43 | typeof value !== 'boolean' 44 | ) { 45 | continue; 46 | } 47 | 48 | if (namespace !== null) { // namespaced attribute 49 | properties[key] = attributeHook(namespace, value); 50 | continue; 51 | } 52 | 53 | attributes[key] = value 54 | properties[key] = undefined 55 | } 56 | 57 | return h(tagName, properties, children); 58 | } 59 | 60 | function isChildren(x) { 61 | return typeof x === 'string' || isArray(x); 62 | } 63 | -------------------------------------------------------------------------------- /lib/vendor/vnode/handle-thunk.js: -------------------------------------------------------------------------------- 1 | var isVNode = require("./is-vnode") 2 | var isVText = require("./is-vtext") 3 | var isWidget = require("./is-widget") 4 | var isThunk = require("./is-thunk") 5 | 6 | module.exports = handleThunk 7 | 8 | function handleThunk(a, b) { 9 | var renderedA = a 10 | var renderedB = b 11 | 12 | if (isThunk(b)) { 13 | renderedB = renderThunk(b, a) 14 | } 15 | 16 | if (isThunk(a)) { 17 | renderedA = renderThunk(a, null) 18 | } 19 | 20 | return { 21 | a: renderedA, 22 | b: renderedB 23 | } 24 | } 25 | 26 | function renderThunk(thunk, previous) { 27 | var renderedThunk = thunk.vnode 28 | 29 | if (!renderedThunk) { 30 | renderedThunk = thunk.vnode = thunk.render(previous) 31 | } 32 | 33 | if (!(isVNode(renderedThunk) || 34 | isVText(renderedThunk) || 35 | isWidget(renderedThunk))) { 36 | throw new Error("thunk did not return a valid node"); 37 | } 38 | 39 | return renderedThunk 40 | } 41 | -------------------------------------------------------------------------------- /lib/vendor/vnode/is-thunk.js: -------------------------------------------------------------------------------- 1 | module.exports = isThunk 2 | 3 | function isThunk(t) { 4 | return t && t.type === "Thunk" 5 | } 6 | -------------------------------------------------------------------------------- /lib/vendor/vnode/is-vhook.js: -------------------------------------------------------------------------------- 1 | module.exports = isHook 2 | 3 | function isHook(hook) { 4 | return hook && 5 | (typeof hook.hook === "function" && !hook.hasOwnProperty("hook") || 6 | typeof hook.unhook === "function" && !hook.hasOwnProperty("unhook")) 7 | } 8 | -------------------------------------------------------------------------------- /lib/vendor/vnode/is-vnode.js: -------------------------------------------------------------------------------- 1 | var version = require("./version") 2 | 3 | module.exports = isVirtualNode 4 | 5 | function isVirtualNode(x) { 6 | return x && x.type === "VirtualNode" && x.version === version 7 | } 8 | -------------------------------------------------------------------------------- /lib/vendor/vnode/is-vtext.js: -------------------------------------------------------------------------------- 1 | var version = require("./version") 2 | 3 | module.exports = isVirtualText 4 | 5 | function isVirtualText(x) { 6 | return x && x.type === "VirtualText" && x.version === version 7 | } 8 | -------------------------------------------------------------------------------- /lib/vendor/vnode/is-widget.js: -------------------------------------------------------------------------------- 1 | module.exports = isWidget 2 | 3 | function isWidget(w) { 4 | return w && w.type === "Widget" 5 | } 6 | -------------------------------------------------------------------------------- /lib/vendor/vnode/version.js: -------------------------------------------------------------------------------- 1 | module.exports = "2" 2 | -------------------------------------------------------------------------------- /lib/vendor/vnode/vnode.js: -------------------------------------------------------------------------------- 1 | var version = require("./version") 2 | var isVNode = require("./is-vnode") 3 | var isWidget = require("./is-widget") 4 | var isThunk = require("./is-thunk") 5 | var isVHook = require("./is-vhook") 6 | 7 | module.exports = VirtualNode 8 | 9 | var noProperties = {} 10 | var noChildren = [] 11 | 12 | function VirtualNode(tagName, properties, children, key, namespace) { 13 | this.tagName = tagName 14 | this.properties = properties || noProperties 15 | this.children = children || noChildren 16 | this.key = key != null ? String(key) : undefined 17 | this.namespace = (typeof namespace === "string") ? namespace : null 18 | 19 | var count = (children && children.length) || 0 20 | var descendants = 0 21 | var hasWidgets = false 22 | var hasThunks = false 23 | var descendantHooks = false 24 | var hooks 25 | 26 | for (var propName in properties) { 27 | if (properties.hasOwnProperty(propName)) { 28 | var property = properties[propName] 29 | if (isVHook(property) && property.unhook) { 30 | if (!hooks) { 31 | hooks = {} 32 | } 33 | 34 | hooks[propName] = property 35 | } 36 | } 37 | } 38 | 39 | for (var i = 0; i < count; i++) { 40 | var child = children[i] 41 | if (isVNode(child)) { 42 | descendants += child.count || 0 43 | 44 | if (!hasWidgets && child.hasWidgets) { 45 | hasWidgets = true 46 | } 47 | 48 | if (!hasThunks && child.hasThunks) { 49 | hasThunks = true 50 | } 51 | 52 | if (!descendantHooks && (child.hooks || child.descendantHooks)) { 53 | descendantHooks = true 54 | } 55 | } else if (!hasWidgets && isWidget(child)) { 56 | if (typeof child.destroy === "function") { 57 | hasWidgets = true 58 | } 59 | } else if (!hasThunks && isThunk(child)) { 60 | hasThunks = true; 61 | } 62 | } 63 | 64 | this.count = count + descendants 65 | this.hasWidgets = hasWidgets 66 | this.hasThunks = hasThunks 67 | this.hooks = hooks 68 | this.descendantHooks = descendantHooks 69 | } 70 | 71 | VirtualNode.prototype.version = version 72 | VirtualNode.prototype.type = "VirtualNode" 73 | -------------------------------------------------------------------------------- /lib/vendor/vnode/vpatch.js: -------------------------------------------------------------------------------- 1 | var version = require("./version") 2 | 3 | VirtualPatch.NONE = 0 4 | VirtualPatch.VTEXT = 1 5 | VirtualPatch.VNODE = 2 6 | VirtualPatch.WIDGET = 3 7 | VirtualPatch.PROPS = 4 8 | VirtualPatch.ORDER = 5 9 | VirtualPatch.INSERT = 6 10 | VirtualPatch.REMOVE = 7 11 | VirtualPatch.THUNK = 8 12 | 13 | module.exports = VirtualPatch 14 | 15 | function VirtualPatch(type, vNode, patch) { 16 | this.type = Number(type) 17 | this.vNode = vNode 18 | this.patch = patch 19 | } 20 | 21 | VirtualPatch.prototype.version = version 22 | VirtualPatch.prototype.type = "VirtualPatch" 23 | -------------------------------------------------------------------------------- /lib/vendor/vnode/vtext.js: -------------------------------------------------------------------------------- 1 | var version = require("./version") 2 | 3 | module.exports = VirtualText 4 | 5 | function VirtualText(text) { 6 | this.text = String(text) 7 | } 8 | 9 | VirtualText.prototype.version = version 10 | VirtualText.prototype.type = "VirtualText" 11 | -------------------------------------------------------------------------------- /lib/vendor/vtree/README.md: -------------------------------------------------------------------------------- 1 | # vtree 2 | 3 | A realtime tree diffing algorithm 4 | 5 | ## Motivation 6 | 7 | `vtree` currently exists as part of `virtual-dom`. It is used for imitating 8 | diff operations between two `vnode` structures that imitate the structure of 9 | the active DOM node structure in the browser. 10 | 11 | ## Example 12 | 13 | ```js 14 | var h = require("virtual-dom/h") 15 | var diff = require("virtual-dom/diff") 16 | 17 | var leftNode = h("div") 18 | var rightNode = h("text") 19 | 20 | var patches = diff(leftNode, rightNode) 21 | /* 22 | -> { 23 | a: leftNode, 24 | 0: vpatch(rightNode) // a replace operation for the first node 25 | } 26 | */ 27 | ``` 28 | 29 | ## Installation 30 | 31 | `npm install virtual-dom` 32 | 33 | ## Contributors 34 | 35 | - Matt Esch 36 | 37 | ## MIT Licenced 38 | -------------------------------------------------------------------------------- /lib/vendor/vtree/diff-props.js: -------------------------------------------------------------------------------- 1 | var isObject = require("is-object") 2 | var isHook = require("../vnode/is-vhook") 3 | 4 | module.exports = diffProps 5 | 6 | function diffProps(a, b) { 7 | var diff 8 | 9 | for (var aKey in a) { 10 | if (!(aKey in b)) { 11 | diff = diff || {} 12 | diff[aKey] = undefined 13 | } 14 | 15 | var aValue = a[aKey] 16 | var bValue = b[aKey] 17 | 18 | if (aValue === bValue) { 19 | continue 20 | } else if (isObject(aValue) && isObject(bValue)) { 21 | if (getPrototype(bValue) !== getPrototype(aValue)) { 22 | diff = diff || {} 23 | diff[aKey] = bValue 24 | } else if (isHook(bValue)) { 25 | diff = diff || {} 26 | diff[aKey] = bValue 27 | } else { 28 | var objectDiff = diffProps(aValue, bValue) 29 | if (objectDiff) { 30 | diff = diff || {} 31 | diff[aKey] = objectDiff 32 | } 33 | } 34 | } else { 35 | diff = diff || {} 36 | diff[aKey] = bValue 37 | } 38 | } 39 | 40 | for (var bKey in b) { 41 | if (!(bKey in a)) { 42 | diff = diff || {} 43 | diff[bKey] = b[bKey] 44 | } 45 | } 46 | 47 | return diff 48 | } 49 | 50 | function getPrototype(value) { 51 | if (Object.getPrototypeOf) { 52 | return Object.getPrototypeOf(value) 53 | } else if (value.__proto__) { 54 | return value.__proto__ 55 | } else if (value.constructor) { 56 | return value.constructor.prototype 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/virtualdom.js: -------------------------------------------------------------------------------- 1 | var vdom = { 2 | VNode: require('./vendor/vnode/vnode.js'), 3 | VText: require('./vendor/vnode/vtext.js'), 4 | diff: require('./vendor/diff.js'), 5 | patch: require('./vendor/patch.js'), 6 | createElement: require('./vendor/create-element.js'), 7 | svg: require("./vendor/virtual-hyperscript/svg.js"), 8 | }; 9 | 10 | global.VirtualDom = vdom; 11 | module.exports = vdom; 12 | -------------------------------------------------------------------------------- /src/dom_float.ml: -------------------------------------------------------------------------------- 1 | open Js_of_ocaml 2 | 3 | let to_js_string value = (Js.number_of_float value)##toString 4 | let to_js_string_fixed digits value = (Js.number_of_float value)##toFixed digits 5 | let to_js_string_precision digits value = (Js.number_of_float value)##toPrecision digits 6 | let to_js_string_exponential value = (Js.number_of_float value)##toExponential 7 | let to_string value = to_js_string value |> Js.to_string 8 | let to_string_fixed digits value = to_js_string_fixed digits value |> Js.to_string 9 | let to_string_precision digits value = to_js_string_precision digits value |> Js.to_string 10 | let to_string_exponential value = to_js_string_exponential value |> Js.to_string 11 | 12 | let%expect_test _ = 13 | let open Core in 14 | let print f = printf "%s" (to_string f) in 15 | print 1.; 16 | [%expect {| 1 |}]; 17 | print Float.nan; 18 | [%expect {| NaN |}]; 19 | print Float.infinity; 20 | [%expect {| Infinity |}]; 21 | print Float.neg_infinity; 22 | [%expect {| -Infinity |}]; 23 | print 0.00000001; 24 | [%expect {| 1e-8 |}]; 25 | print (-1.); 26 | [%expect {| -1 |}]; 27 | print 1.0000001; 28 | [%expect {| 1.0000001 |}] 29 | ;; 30 | -------------------------------------------------------------------------------- /src/dom_float.mli: -------------------------------------------------------------------------------- 1 | (* Printing floats compatible with what javascript does *) 2 | 3 | open Js_of_ocaml 4 | 5 | (** Calls the [toString] method on the float *) 6 | val to_js_string : float -> Js.js_string Js.t 7 | 8 | (** Calls the [toFixed] method on the float 9 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed *) 10 | val to_js_string_fixed : int -> float -> Js.js_string Js.t 11 | 12 | (** Calls the [toPrecision] method on the float 13 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision *) 14 | val to_js_string_precision : int -> float -> Js.js_string Js.t 15 | 16 | (** Calls the [toExponential] method on the float 17 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential *) 18 | val to_js_string_exponential : float -> Js.js_string Js.t 19 | 20 | val to_string : float -> string 21 | val to_string_fixed : int -> float -> string 22 | val to_string_precision : int -> float -> string 23 | val to_string_exponential : float -> string 24 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom) 3 | (public_name virtual_dom) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (js_of_ocaml 7 | (javascript_files ../lib/virtualdom.compiled.pretty.js ./thunk.js 8 | ./hooks.js)) 9 | (libraries ui_effect js_of_ocaml js_of_ocaml_patches css_gen core sexplib 10 | jsoo_weak_collections) 11 | (wasm_of_ocaml 12 | (javascript_files ../lib/virtualdom.compiled.pretty.js ./thunk.js 13 | ./hooks.js))) 14 | -------------------------------------------------------------------------------- /src/effect.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Js_of_ocaml 3 | include Ui_effect 4 | 5 | (* All visibility handlers see all events, so a simple list is enough. *) 6 | let visibility_handlers : (unit -> unit) list ref = ref [] 7 | 8 | module type Visibility_handler = sig 9 | val handle : unit -> unit 10 | end 11 | 12 | module Define_visibility (VH : Visibility_handler) = struct 13 | let () = visibility_handlers := VH.handle :: !visibility_handlers 14 | end 15 | 16 | module Open_url_target = struct 17 | type t = 18 | | This_tab 19 | | New_tab_or_window 20 | | Iframe_parent_or_this_tab 21 | | Iframe_root_parent_or_this_tab 22 | 23 | let to_target = function 24 | (* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target *) 25 | | This_tab -> "_self" 26 | | New_tab_or_window -> "_blank" 27 | | Iframe_parent_or_this_tab -> "_parent" 28 | | Iframe_root_parent_or_this_tab -> "_top" 29 | ;; 30 | end 31 | 32 | type _ t += 33 | | Viewport_changed 34 | | Stop_propagation 35 | | Stop_immediate_propagation 36 | | Prevent_default 37 | | Open : 38 | { url : string 39 | ; target : Open_url_target.t 40 | } 41 | -> unit t 42 | 43 | let sequence_as_sibling left ~unless_stopped = 44 | let rec contains_stop = function 45 | | Many es -> List.exists es ~f:contains_stop 46 | | Stop_immediate_propagation -> true 47 | | _ -> false 48 | in 49 | if contains_stop left then left else Ui_effect.Many [ left; unless_stopped () ] 50 | ;; 51 | 52 | let open_url ?(in_ = Open_url_target.This_tab) url = Open { url; target = in_ } 53 | 54 | (* We need to keep track of the current dom event here so that 55 | movement between [Vdom.Effect.Expert.handle] and 56 | [Ui_concrete.Effect.Expert.handle] keeps the original 57 | dom event around. *) 58 | let current_dom_event = ref None 59 | 60 | let () = 61 | Hashtbl.add_exn 62 | Expert.handlers 63 | ~key:Stdlib.Obj.Extension_constructor.(id (of_val Viewport_changed)) 64 | ~data:(fun _ -> List.iter !visibility_handlers ~f:(fun f -> f ())) 65 | ;; 66 | 67 | let () = 68 | Hashtbl.add_exn 69 | Expert.handlers 70 | ~key:Stdlib.Obj.Extension_constructor.(id (of_val Stop_propagation)) 71 | ~data:(fun _ -> Option.iter !current_dom_event ~f:Dom_html.stopPropagation) 72 | ;; 73 | 74 | let () = 75 | Hashtbl.add_exn 76 | Expert.handlers 77 | ~key:Stdlib.Obj.Extension_constructor.(id (of_val Prevent_default)) 78 | ~data:(fun _ -> Option.iter !current_dom_event ~f:Dom.preventDefault) 79 | ;; 80 | 81 | let () = 82 | Hashtbl.add_exn 83 | Expert.handlers 84 | ~key:(Stdlib.Obj.Extension_constructor.id [%extension_constructor Open]) 85 | ~data:(fun hidden -> 86 | match hidden with 87 | | T (Open { url; target }, callback) -> 88 | let (_ : Dom_html.window Js.t Js.Opt.t) = 89 | Dom_html.window##open_ 90 | (Js.string url) 91 | (Js.string (Open_url_target.to_target target)) 92 | Js.Opt.empty 93 | in 94 | callback () 95 | | _ -> failwith "Unrecognized variant") 96 | ;; 97 | 98 | module Expert = struct 99 | let handle_non_dom_event_exn = Expert.handle 100 | 101 | let handle dom_event event = 102 | let old = !current_dom_event in 103 | current_dom_event := Some (dom_event :> Dom_html.element Dom.event Js.t); 104 | Expert.handle event; 105 | current_dom_event := old 106 | ;; 107 | end 108 | -------------------------------------------------------------------------------- /src/effect.mli: -------------------------------------------------------------------------------- 1 | open Js_of_ocaml 2 | 3 | include module type of struct 4 | include Ui_effect 5 | end 6 | 7 | module type Visibility_handler = sig 8 | val handle : unit -> unit 9 | end 10 | 11 | module type S = sig 12 | type action 13 | type 'a t 14 | 15 | val inject : action -> unit t 16 | end 17 | 18 | module Open_url_target : sig 19 | (** Target for opening a URL. See 20 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target *) 21 | type t = 22 | | This_tab (** _self *) 23 | | New_tab_or_window (** _blank *) 24 | | Iframe_parent_or_this_tab (** _parent *) 25 | | Iframe_root_parent_or_this_tab (** _top *) 26 | 27 | val to_target : t -> string 28 | end 29 | 30 | type 'a t += 31 | | Viewport_changed 32 | (** [Viewport_changed] events are delivered to all visibility handlers *) 33 | | Stop_propagation 34 | (** [Stop_propagation] prevents the underlying DOM event from propagating up to the 35 | parent elements *) 36 | | Stop_immediate_propagation 37 | (** [Stop_immediate_propagation] causes [sequence_as_sibling] to ignore next 38 | sequenced event. *) 39 | | Prevent_default 40 | (** [Prevent_default] prevents the default browser action from occurring as a result 41 | of this event *) 42 | | Open : 43 | { url : string 44 | ; target : Open_url_target.t 45 | } 46 | -> unit t 47 | (** [Open (url, target)] is [window.open(url, target)]. *) 48 | 49 | (** Sequences two events, but only if the first is neither [Stop_immediate_propagation] 50 | nor a [Many] which contains [Stop_immediate_propagation]. Use this instead of [Many] 51 | if combining events that are associated with the same source; for example, the 52 | motivation for this function is for merging Inputs of hooks. 53 | 54 | The second argument is a function that takes unit not because it is expected to be 55 | impure, but because often it is computed via some arbitrary handler function. Using 56 | hooks as an example, often the input to a hook has type ['a -> Effect.t]. To merge 57 | inputs [f] and [g], you might write 58 | 59 | {[ 60 | fun x -> sequence_as_sibling (f x) (g x) 61 | ]} 62 | 63 | but this might unnecessarily call [g] if [f] returns something with 64 | [Stop_immediate_propagation]. Instead, using this API, you must write 65 | 66 | {[ 67 | fun x -> sequence_as_sibling (f x) (fun () -> g x) 68 | ]} *) 69 | val sequence_as_sibling : unit t -> unless_stopped:(unit -> unit t) -> unit t 70 | 71 | (** [open_url ~in_ url] creates an effect that opens the given URL. *) 72 | val open_url : ?in_:Open_url_target.t -> string -> unit t 73 | 74 | (** For registering a handler for Viewport_changed events. Note that if this functor is 75 | called multiple times, each handler will see all of the events. *) 76 | module Define_visibility (VH : Visibility_handler) : sig end 77 | 78 | module Expert : sig 79 | (** [handle t] looks up the [Handler.handle] function in the table of [Define]d 80 | functions, unwraps the [Effect.t] back into its underlying [Action.t], and applies 81 | the two. This is only intended for internal use by this library, specifically by the 82 | attribute code. *) 83 | val handle : #Dom_html.event Js.t -> unit t -> unit 84 | 85 | (** [handle_non_dom_event_exn] is the same as [handle] except that it raises in any case 86 | that would have required the [#Dom_html.event Js.t]. In particular, this can be to 87 | feed Actions back to the system that are not triggered by events from the DOM and do 88 | not have a corresponding [#Dom_html.event Js.t]. *) 89 | val handle_non_dom_event_exn : unit t -> unit 90 | end 91 | -------------------------------------------------------------------------------- /src/global_listeners.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | (** Hooks to set events listeners on [window]. This is needed as if we only set them on 5 | individual elements we will miss ones that happen outside of the viewport 6 | 7 | https://coderwall.com/p/79hkbw/js-mouse-events-that-work-even-when-mouse-is-moved-outside-the-window *) 8 | 9 | module Phase : sig 10 | (** Should the handler run on the [capture] or [bubbling] phase of event processing? 11 | https://javascript.info/bubbling-and-capturing#capturing 12 | 13 | If ran on capture, it will run before any element-specific listeners. 14 | 15 | If ran on bubbling, it will run after all element-specific listeners, and it won't 16 | run if [stopPropagation] was called during the handlers of the event target or any 17 | of its ancestors. 18 | 19 | When in doubt, default to [Bubbling]. *) 20 | type t = 21 | | Capture 22 | | Bubbling 23 | end 24 | 25 | (** Mouse-event handlers *) 26 | 27 | val mousedown 28 | : phase:Phase.t 29 | -> f:(Dom_html.mouseEvent Js.t -> unit Ui_effect.t) 30 | -> Attr.t 31 | 32 | val mouseup : phase:Phase.t -> f:(Dom_html.mouseEvent Js.t -> unit Ui_effect.t) -> Attr.t 33 | 34 | val mousemove 35 | : phase:Phase.t 36 | -> f:(Dom_html.mouseEvent Js.t -> unit Ui_effect.t) 37 | -> Attr.t 38 | 39 | val click : phase:Phase.t -> f:(Dom_html.mouseEvent Js.t -> unit Ui_effect.t) -> Attr.t 40 | val blur : phase:Phase.t -> f:(Dom_html.focusEvent Js.t -> unit Ui_effect.t) -> Attr.t 41 | 42 | val contextmenu 43 | : phase:Phase.t 44 | -> f:(Dom_html.mouseEvent Js.t -> unit Ui_effect.t) 45 | -> Attr.t 46 | 47 | (** Keyboard-event handlers *) 48 | 49 | val keydown 50 | : phase:Phase.t 51 | -> f:(Dom_html.keyboardEvent Js.t -> unit Ui_effect.t) 52 | -> Attr.t 53 | 54 | val keyup : phase:Phase.t -> f:(Dom_html.keyboardEvent Js.t -> unit Ui_effect.t) -> Attr.t 55 | 56 | (** Other event handlers *) 57 | val visibilitychange 58 | : phase:Phase.t 59 | -> f:(Dom_html.event Js.t -> unit Ui_effect.t) 60 | -> Attr.t 61 | 62 | (* Chrome may not allow Javascript to run after a user requests a tab close, so an 63 | effect passed into `Custom_best_effort may or may not execute to completion *) 64 | val beforeunload 65 | : phase:Phase.t 66 | -> f: 67 | (Dom_html.event Js.t 68 | -> [ `Show_warning | `Do_nothing | `Custom_best_effort of unit Ui_effect.t ] 69 | Ui_effect.t) 70 | -> Attr.t 71 | 72 | module For_testing : sig 73 | type 'a t = 74 | { capture : 'a option 75 | ; bubbling : 'a option 76 | } 77 | 78 | (** In tests, you might just want to run all global listeners. This helper will run the 79 | `combine` and `bubbling` handlers in parallel. If your tests include running other 80 | handlers, you probably don't want this. *) 81 | val combine_capture_and_bubbling : ('a -> unit Ui_effect.t) t -> 'a -> unit Ui_effect.t 82 | 83 | val mousedown_type_id : (Dom_html.mouseEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 84 | val mouseup_type_id : (Dom_html.mouseEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 85 | val mousemove_type_id : (Dom_html.mouseEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 86 | val click_type_id : (Dom_html.mouseEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 87 | 88 | val contextmenu_type_id 89 | : (Dom_html.mouseEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 90 | 91 | val keydown_type_id 92 | : (Dom_html.keyboardEvent Js.t -> unit Ui_effect.t) t Type_equal.Id.t 93 | 94 | val visibilitychange_type_id 95 | : (Dom_html.event Js.t -> unit Ui_effect.t) t Type_equal.Id.t 96 | 97 | val beforeunload_type_id : (Dom_html.event Js.t -> unit Ui_effect.t) t Type_equal.Id.t 98 | end 99 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | // Used by workaround for input element value field 2 | // Based on https://github.com/Matt-Esch/virtual-dom/blob/947ecf92b67d25bb693a0f625fa8e90c099887d5/virtual-hyperscript/hooks/soft-set-hook.js 3 | 4 | globalThis.SoftSetHook = function(value) { 5 | if (!(this instanceof SoftSetHook)) { 6 | return new SoftSetHook(value); 7 | } 8 | 9 | this.value = value; 10 | }; 11 | 12 | globalThis.SoftSetHook.prototype.hook = function (node, propertyName) { 13 | if (node[propertyName] !== this.value) { 14 | node[propertyName] = this.value; 15 | } 16 | }; 17 | 18 | 19 | var GenericHook = function (init, update, destroy, id, extra) { 20 | if (!(this instanceof GenericHook)) { 21 | return new GenericHook(init, update, destroy, id, extra); 22 | } 23 | 24 | this.init = init; 25 | this.update = update; 26 | this.destroy = destroy; 27 | this.id = id; 28 | this.extra = extra; 29 | }; 30 | 31 | var hook_state_key = "vdom_hook_state_key"; 32 | 33 | if (globalThis.Symbol) { 34 | hook_state_key = Symbol(hook_state_key); 35 | } 36 | 37 | GenericHook.write_state = function (node, propName, state) { 38 | if (!node[hook_state_key]) { 39 | node[hook_state_key] = {}; 40 | } 41 | node[hook_state_key][propName] = state; 42 | } 43 | 44 | GenericHook.read_state = function (node, propName) { 45 | return node[hook_state_key][propName]; 46 | } 47 | 48 | GenericHook.remove_state = function (node, propName) { 49 | delete node[hook_state_key][propName]; 50 | } 51 | 52 | GenericHook.canTransition = function (from, to) { 53 | return from instanceof this && to instanceof this && from.id === to.id && to.update; 54 | }; 55 | 56 | GenericHook.prototype.hook = function (node, propName, prev) { 57 | if (GenericHook.canTransition(prev, this)) { 58 | var state = GenericHook.read_state(node, propName); 59 | state = this.update(state, node); 60 | GenericHook.write_state(node, propName, state); 61 | } else { 62 | var state = this.init(node); 63 | GenericHook.write_state(node, propName, state); 64 | } 65 | }; 66 | 67 | GenericHook.prototype.unhook = function (node, propName, next) { 68 | if (GenericHook.canTransition(this, next)) { 69 | // Do nothing, the impending [hook] will handle the call to update. 70 | } else { 71 | var state = GenericHook.read_state(node, propName); 72 | this.destroy(state, node); 73 | GenericHook.remove_state(node, propName); 74 | } 75 | }; 76 | 77 | globalThis.GenericHook = GenericHook; 78 | -------------------------------------------------------------------------------- /src/hooks.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | module type S = Hooks_intf.S 5 | module type Input = Hooks_intf.Input 6 | 7 | let cancel_animation_frame id = Dom_html.window##cancelAnimationFrame id 8 | 9 | let request_animation_frame f = 10 | Dom_html.window##requestAnimationFrame (Js.wrap_callback f) 11 | ;; 12 | 13 | module Extra = struct 14 | type t = 15 | | T : 16 | { type_id : 'a Type_equal.Id.t 17 | ; value : 'a 18 | } 19 | -> t 20 | 21 | let sexp_of_t (T { type_id; value }) = Type_equal.Id.to_sexp type_id value 22 | end 23 | 24 | type t = 25 | | T : 26 | { input : 'input 27 | ; input_id : 'input Type_equal.Id.t 28 | ; combine_inputs : 'input -> 'input -> 'input 29 | ; init : 'input -> Dom_html.element Js.t -> 'input * 'extra * 'state 30 | ; update : 31 | 'input 32 | -> 'input * 'extra * 'state 33 | -> Dom_html.element Js.t 34 | -> 'input * 'extra * 'state 35 | ; destroy : 'input * 'extra * 'state -> Dom_html.element Js.t -> unit 36 | ; id : ('input * 'extra * 'state) Core.Type_equal.Id.t 37 | } 38 | -> t 39 | 40 | let generic_hook = lazy Js.Unsafe.(get global (Js.string "GenericHook")) 41 | 42 | let make_hook ~combine_inputs ~init ~extra:(input, input_id) ~update ~destroy ~id = 43 | T { init; combine_inputs; input; input_id; update; destroy; id } 44 | ;; 45 | 46 | let unsafe_create = make_hook 47 | 48 | let pack (T { init; input; input_id; update; destroy; id; _ }) = 49 | let wrap a = a |> Js.wrap_callback |> Js.Unsafe.inject in 50 | let init = wrap (init input) in 51 | let update = wrap (update input) in 52 | let destroy = wrap destroy in 53 | let generic_hook = Lazy.force generic_hook in 54 | let extra = Extra.T { type_id = input_id; value = input } in 55 | Js.Unsafe.fun_call 56 | generic_hook 57 | [| init; update; destroy; id |> Js.Unsafe.inject; extra |> Js.Unsafe.inject |] 58 | ;; 59 | 60 | let combine (T left) (T right) = 61 | match Type_equal.Id.same_witness left.input_id right.input_id with 62 | | None -> 63 | eprint_s 64 | [%message 65 | "hooks do not have the same type, so they cannot be combined; taking the second \ 66 | of the two"]; 67 | T right 68 | | Some T -> T { right with input = right.combine_inputs left.input right.input } 69 | ;; 70 | 71 | module Make (S : S) = struct 72 | let input_and_state_id = 73 | Type_equal.Id.create ~name:"" (fun (input, _animation_id, state) -> 74 | [%sexp_of: S.Input.t * opaque] (input, state)) 75 | ;; 76 | 77 | let input_id = Type_equal.Id.create ~name:"" S.Input.sexp_of_t 78 | 79 | let init input element = 80 | let state = S.init input element in 81 | let warn_if_disconnected () = 82 | let element : < isConnected : bool Js.t Js.prop > Js.t = Js.Unsafe.coerce element in 83 | if not (element##.isConnected |> Js.to_bool) 84 | then 85 | Console.console##warn_2 86 | (Js.string "[on_mount] hook ran, but element was not connected.") 87 | element 88 | in 89 | let on_destroy = 90 | match S.on_mount with 91 | | `Do_nothing -> fun () -> () 92 | | `Schedule_immediately_after_this_dom_patch_completes on_mount -> 93 | let should_run = ref true in 94 | (* It is unlikely that a hook is destroyed immediately after being created, but it 95 | can happen, if the creation and destruction happen in the [on_mount] of a different 96 | hook. In this case, the element never gets displayed to the user, so we just 97 | don't run its [on_mount]. *) 98 | On_mount.Private_for_this_library_only.schedule (fun () -> 99 | if !should_run 100 | then ( 101 | warn_if_disconnected (); 102 | on_mount input state element)); 103 | fun () -> should_run := false 104 | | `Schedule_animation_frame on_mount -> 105 | let animation_id = 106 | request_animation_frame (fun _ -> 107 | warn_if_disconnected (); 108 | on_mount input state element) 109 | in 110 | fun () -> cancel_animation_frame animation_id 111 | in 112 | input, on_destroy, state 113 | ;; 114 | 115 | let update input (old_input, on_destroy, state) element = 116 | S.update ~old_input ~new_input:input state element; 117 | input, on_destroy, state 118 | ;; 119 | 120 | let destroy (old_input, on_destroy, state) element = 121 | on_destroy (); 122 | S.destroy old_input state element 123 | ;; 124 | 125 | let create input = 126 | make_hook 127 | ~extra:(input, input_id) 128 | ~combine_inputs:S.Input.combine 129 | ~id:input_and_state_id 130 | ~init 131 | ~update 132 | ~destroy 133 | ;; 134 | 135 | module For_testing = struct 136 | let type_id = input_id 137 | end 138 | end 139 | 140 | module For_testing = struct 141 | module Extra = Extra 142 | end 143 | -------------------------------------------------------------------------------- /src/hooks.mli: -------------------------------------------------------------------------------- 1 | include Hooks_intf.Hooks (** @inline *) 2 | -------------------------------------------------------------------------------- /src/hooks_intf.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | module type Input = sig 5 | type t [@@deriving sexp_of] 6 | 7 | (* [combine first second] describes how more than one of the same hook should 8 | be merged. This function will only be used if the hooks are combined 9 | using [Attr.many]'s merge semantics. It is common for [t] to by 10 | a function type like ['a -> unit Ui_effect.t]; in this case, the proper 11 | implementation is probably the following: 12 | 13 | {[ 14 | let combine f g event = 15 | Vdom.Effect.sequence_as_sibling 16 | (f event) 17 | ~unless_stopped:(fun () -> g event) 18 | ]} *) 19 | 20 | val combine : t -> t -> t 21 | end 22 | 23 | module type S = sig 24 | module State : T 25 | module Input : Input 26 | 27 | (** [init] is called the first time that this attribute is attached to a particular 28 | node. It is particularly responsible for producing a value of type [State.t]. The 29 | element that it is being attached to is not necessarily attached to the rest of the 30 | DOM tree. *) 31 | val init : Input.t -> Dom_html.element Js.t -> State.t 32 | 33 | (** [on_mount] specifies what happens when the element is attached to the rest of the 34 | DOM tree. 35 | 36 | If [`Schedule_immediately_after_this_dom_patch_completes] is passed, the provided 37 | function will be called immediately after the Vdom patch that attaches the element. 38 | Any exceptions thrown in the function will crash the app. 39 | 40 | When [`Schedule_animation_frame] is passed, the browser will call the provided 41 | function during the next frame via [window.requestAnimationFrame()]. 42 | 43 | Notably, "moving" a node is usually interpreted as destroying it, and creating a new 44 | one. You should not assume that [on_mount] will only be called once. *) 45 | val on_mount 46 | : [ `Do_nothing 47 | | `Schedule_immediately_after_this_dom_patch_completes of 48 | Input.t -> State.t -> Dom_html.element Js.t -> unit 49 | | `Schedule_animation_frame of Input.t -> State.t -> Dom_html.element Js.t -> unit 50 | ] 51 | 52 | (** [update] is called when a previous attribute of the same kind existed on the vdom 53 | node. You get access to the [Input.t] that the previous node was created with, as 54 | well as the State.t for that hook, which you can mutate if you like. There is no 55 | guarantee that [update] will be called instead of a sequence of [destroy] followed 56 | by [init], so [update] should behave the same as that sequence (except it might be 57 | faster). *) 58 | val update 59 | : old_input:Input.t 60 | -> new_input:Input.t 61 | -> State.t 62 | -> Dom_html.element Js.t 63 | -> unit 64 | 65 | (** [destroy] is called when the previous vdom has this hook, but a newer vdom tree does 66 | not. The last input and state are passed in alongside the element that it used to be 67 | attached to. *) 68 | val destroy : Input.t -> State.t -> Dom_html.element Js.t -> unit 69 | end 70 | 71 | module type Hooks = sig 72 | module type S = S 73 | module type Input = Input 74 | 75 | type t 76 | 77 | val combine : t -> t -> t 78 | val pack : t -> Js.Unsafe.any 79 | 80 | module Make (S : S) : sig 81 | val create : S.Input.t -> t 82 | 83 | module For_testing : sig 84 | (** The type-id provided here can be used to pull out the input value for an 85 | instance of this hook for testing-purposes. *) 86 | 87 | val type_id : S.Input.t Type_equal.Id.t 88 | end 89 | end 90 | 91 | val unsafe_create 92 | : combine_inputs:('input -> 'input -> 'input) 93 | -> init:('input -> Dom_html.element Js.t -> 'input * unit * 'state) 94 | -> extra:'input * 'input Type_equal.Id.t 95 | -> update: 96 | ('input 97 | -> 'input * unit * 'state 98 | -> Dom_html.element Js.t 99 | -> 'input * unit * 'state) 100 | -> destroy:('input * unit * 'state -> Dom_html.element Js.t -> unit) 101 | -> id:('input * unit * 'state) Type_equal.Id.t 102 | -> t 103 | 104 | module For_testing : sig 105 | module Extra : sig 106 | type t = 107 | | T : 108 | { type_id : 'a Type_equal.Id.t 109 | ; value : 'a 110 | } 111 | -> t 112 | 113 | val sexp_of_t : t -> Sexp.t 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /src/js_map.ml: -------------------------------------------------------------------------------- 1 | open! Js_of_ocaml 2 | open! Js 3 | 4 | class type ['a, 'b] map = object ('self) 5 | method set : 'a -> 'b -> unit meth 6 | method get : 'a -> 'b Optdef.t meth 7 | method delete : 'a -> unit meth 8 | end 9 | 10 | type ('a, 'b) t = ('a, 'b) map Js.t 11 | 12 | let map : unit -> ('a, 'b) map Js.t Js.constr = fun () -> Unsafe.global##._Map 13 | 14 | let create () = 15 | let map = map () in 16 | new%js map 17 | ;; 18 | 19 | let set t a b = t##set a b 20 | let get t a = Js.Optdef.to_option (t##get a) 21 | let delete t a = t##delete a 22 | -------------------------------------------------------------------------------- /src/js_map.mli: -------------------------------------------------------------------------------- 1 | open! Js_of_ocaml 2 | 3 | (** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map *) 4 | 5 | type ('a, 'b) t 6 | 7 | val create : unit -> ('a, 'b) t 8 | val set : ('a, 'b) t -> 'a -> 'b -> unit 9 | val get : ('a, 'b) t -> 'a -> 'b option 10 | val delete : ('a, 'b) t -> 'a -> unit 11 | -------------------------------------------------------------------------------- /src/on_mount.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | 3 | let stack = ref None 4 | 5 | module Private_for_this_library_only = struct 6 | let schedule task = 7 | match !stack with 8 | | None -> 9 | print_endline 10 | "You attempted to schedule an [on_mount] task somewhere other than within a \ 11 | [with_on_mount_at_end] call, which is not allowed, so the task will not be \ 12 | scheduled. This is usually caused by running [Vdom.Node.to_dom] or \ 13 | [Vdom.Node.Patch.apply] outside of a widget or hook." 14 | | Some tl -> stack := Some (task :: tl) 15 | ;; 16 | end 17 | 18 | let with_on_mount_at_end f = 19 | let r = 20 | Ref.set_temporarily stack (Some []) ~f:(fun () -> 21 | let r = f () in 22 | let stack_has_tasks () = 23 | match !stack with 24 | | None -> 25 | print_endline 26 | "BUG! Scheduled tasks should never be empty inside of a \ 27 | [with_on_mount_at_end] call."; 28 | false 29 | | Some [] -> false 30 | | _ -> true 31 | in 32 | while stack_has_tasks () do 33 | let to_run = Option.value !stack ~default:[] in 34 | stack := Some []; 35 | List.iter (List.rev to_run) ~f:(fun f -> f ()) 36 | done; 37 | r) 38 | in 39 | r 40 | ;; 41 | 42 | module For_testing = struct 43 | let num_queued_tasks () = 44 | match !stack with 45 | | None -> 0 46 | | Some l -> List.length l 47 | ;; 48 | end 49 | -------------------------------------------------------------------------------- /src/on_mount.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | 3 | module Private_for_this_library_only : sig 4 | val schedule : (unit -> unit) -> unit 5 | end 6 | 7 | val with_on_mount_at_end : (unit -> 'a) -> 'a 8 | 9 | module For_testing : sig 10 | val num_queued_tasks : unit -> int 11 | end 12 | -------------------------------------------------------------------------------- /src/raw.mli: -------------------------------------------------------------------------------- 1 | open Base 2 | open Js_of_ocaml 3 | 4 | module Attrs : sig 5 | type t = private Js.Unsafe.any 6 | 7 | val create : unit -> t 8 | val has_property : t -> Js.js_string Js.t -> bool 9 | val has_attribute : t -> Js.js_string Js.t -> bool 10 | val set_property : t -> Js.js_string Js.t -> Js.Unsafe.any -> unit 11 | val set_attribute : t -> Js.js_string Js.t -> Js.Unsafe.any -> unit 12 | end 13 | 14 | module Node : sig 15 | type t 16 | 17 | val node : Js.js_string Js.t -> Attrs.t -> t Js.js_array Js.t -> string option -> t 18 | val text : string -> t 19 | val svg : Js.js_string Js.t -> Attrs.t -> t Js.js_array Js.t -> string option -> t 20 | val to_dom : t -> Dom_html.element Js.t 21 | end 22 | 23 | module Patch : sig 24 | type t 25 | 26 | val create : previous:Node.t -> current:Node.t -> t 27 | val apply : Dom_html.element Js.t -> t -> Dom_html.element Js.t 28 | val is_empty : t -> bool 29 | end 30 | 31 | module Widget : sig 32 | type t = Node.t 33 | 34 | val create 35 | : ?vdom_for_testing:Node.t Lazy.t 36 | -> ?destroy:('s -> (#Dom_html.element as 'a) Js.t -> unit) 37 | -> ?update:('s -> 'a Js.t -> 's * 'a Js.t) 38 | -> id:('s * 'a Js.t) Type_equal.Id.t 39 | -> init:(unit -> 's * 'a Js.t) 40 | -> unit 41 | -> t 42 | end 43 | -------------------------------------------------------------------------------- /src/thunk.js: -------------------------------------------------------------------------------- 1 | function VdomThunk(fn, args, key) { 2 | if (!(this instanceof VdomThunk)) { 3 | return new VdomThunk(fn, args, key); 4 | } 5 | 6 | if (key) { 7 | this.key = key; 8 | } 9 | 10 | this.fn = fn; 11 | this.args = args; 12 | }; 13 | 14 | globalThis.VdomThunk = VdomThunk; 15 | 16 | VdomThunk.prototype.type = 'Thunk'; 17 | VdomThunk.prototype.render = function (prev) { 18 | if (prev && this.args === prev.args && this.fn === prev.fn) { 19 | return prev.vnode; 20 | } 21 | 22 | return this.fn(this.args); 23 | }; 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/thunk.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | type t = Raw.Node.t 5 | 6 | let vdom_thunk = lazy Js.Unsafe.(get global (Js.string "VdomThunk")) 7 | 8 | let create ~key arg ~f = 9 | let key = 10 | match key with 11 | | Some key -> Js.Optdef.return (Js.string key) 12 | | None -> Js.Optdef.empty 13 | in 14 | let key = Js.Unsafe.inject key in 15 | let f = Js.wrap_callback (fun a -> f a) |> Js.Unsafe.inject in 16 | let arg = Js.Unsafe.inject arg in 17 | (Obj.magic : _ -> Raw.Node.t) 18 | (Js.Unsafe.fun_call (Lazy.force vdom_thunk) [| f; arg; key |]) 19 | ;; 20 | -------------------------------------------------------------------------------- /src/thunk.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | 3 | type t = Raw.Node.t 4 | 5 | val create : key:string option -> 'a -> f:('a -> Raw.Node.t) -> Raw.Node.t 6 | -------------------------------------------------------------------------------- /src/vdom.ml: -------------------------------------------------------------------------------- 1 | module Attr = struct 2 | include Attr 3 | module Hooks = Hooks 4 | module Global_listeners = Global_listeners 5 | end 6 | 7 | module Attrs = Attr.Multi 8 | module Effect = Effect 9 | 10 | module Node = struct 11 | include Node 12 | module Map_children = Vdom_node_with_map_children 13 | end 14 | 15 | module Html_syntax = struct 16 | module Html_syntax = struct 17 | module Attr : sig 18 | include 19 | module type of Attr 20 | with type t := Attr.t 21 | and module Multi := Attr.Multi 22 | and module Always_focus_hook := Attr.Always_focus_hook 23 | and module Single_focus_hook := Attr.Single_focus_hook 24 | and module No_op_hook := Attr.No_op_hook 25 | and module Expert := Attr.Expert 26 | 27 | module Primitives : sig 28 | val create : ?here:Stdlib.Lexing.position -> string -> string -> Attr.t 29 | val empty : Attr.t 30 | val many : Attr.t list -> Attr.t 31 | end 32 | end = struct 33 | include Attr 34 | 35 | module Primitives = struct 36 | let create = create 37 | let empty = empty 38 | let many = many 39 | end 40 | end 41 | 42 | module Node : sig 43 | include 44 | module type of Node 45 | with type t := Node.t 46 | and module Element := Node.Element 47 | and module Widget := Node.Widget 48 | and module Aliases := Node.Aliases 49 | and module Patch := Node.Patch 50 | and module Expert := Node.Expert 51 | 52 | module Primitives : sig 53 | val text : string -> Node.t 54 | val none : Node.t 55 | val fragment : Node.t list -> Node.t 56 | end 57 | end = struct 58 | include Node 59 | 60 | module Primitives = struct 61 | let text = text 62 | let none = none 63 | let fragment = fragment 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /src/vdom_node_with_map_children.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | (** When given a map of vdom nodes, this function will wrap them in an element whose tag 5 | is determined by the first argument, and efficiently diff them against new nodes in 6 | the future that were created by this function. *) 7 | val make : ?is_svg:bool -> tag:string -> ?attr:Attr.t -> (_, Node.t, _) Map.t -> Node.t 8 | 9 | type ('a, 'b) node_creator := ?attrs:Attr.t list -> ('a, Node.t, 'b) Map.t -> Node.t 10 | 11 | val div : _ node_creator 12 | val span : _ node_creator 13 | val ul : _ node_creator 14 | val ol : _ node_creator 15 | -------------------------------------------------------------------------------- /src/virtual_dom.ml: -------------------------------------------------------------------------------- 1 | module Vdom = Vdom 2 | module Dom_float = Dom_float 3 | module Js_map = Js_map 4 | 5 | module Top_level_effects = struct 6 | let () = 7 | (* use the native-javascript implementation of float -> string with a fixed number of 8 | numbers after the decimal place. *) 9 | Css_gen.Private.float_to_string_with_fixed := Dom_float.to_string_fixed 10 | ;; 11 | end 12 | 13 | module For_testing = struct 14 | let num_queued_on_mount_tasks = On_mount.For_testing.num_queued_tasks 15 | end 16 | -------------------------------------------------------------------------------- /svg/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom_svg) 3 | (public_name virtual_dom.svg) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries virtual_dom css_gen core)) 7 | -------------------------------------------------------------------------------- /svg/src/node.ml: -------------------------------------------------------------------------------- 1 | open Virtual_dom 2 | 3 | let create = Vdom.Node.create_svg 4 | let svg = create "svg" 5 | let a = create "a" 6 | let circle = create "circle" 7 | let ellipse = create "ellipse" 8 | let defs = create "defs" 9 | let g = create "g" 10 | let image = create "image" 11 | let line = create "line" 12 | let linear_gradient = create "linearGradient" 13 | let marker = create "marker" 14 | let mask = create "mask" 15 | let polyline = create "polyline" 16 | let path = create "path" 17 | let polygon = create "polygon" 18 | let radial_gradient = create "radialGradient" 19 | let rect = create "rect" 20 | let stop = create "stop" 21 | let style = create "style" 22 | let symbol = create "symbol" 23 | let text = create "text" 24 | let text_path = create "textPath" 25 | let title = create "title" 26 | let tspan = create "tspan" 27 | let use = create "use" 28 | -------------------------------------------------------------------------------- /svg/src/node.mli: -------------------------------------------------------------------------------- 1 | open Virtual_dom.Vdom 2 | 3 | type node_creator := ?key:string -> ?attrs:Attr.t list -> Node.t list -> Node.t 4 | 5 | val svg : node_creator 6 | val a : node_creator 7 | val circle : node_creator 8 | val defs : node_creator 9 | val ellipse : node_creator 10 | val g : node_creator 11 | val image : node_creator 12 | val line : node_creator 13 | val linear_gradient : node_creator 14 | val marker : node_creator 15 | val mask : node_creator 16 | val polyline : node_creator 17 | val path : node_creator 18 | val polygon : node_creator 19 | val radial_gradient : node_creator 20 | val rect : node_creator 21 | val stop : node_creator 22 | val style : node_creator 23 | val symbol : node_creator 24 | val text : node_creator 25 | val text_path : node_creator 26 | val title : node_creator 27 | val tspan : node_creator 28 | val use : node_creator 29 | -------------------------------------------------------------------------------- /svg/src/virtual_dom_svg.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | module Node = Node 3 | module Attr = Attr 4 | 5 | module Html_syntax = struct 6 | module Html_syntax = struct 7 | module Node = struct 8 | type t = Virtual_dom.Vdom.Node.t 9 | 10 | include Node 11 | module Primitives = Virtual_dom.Vdom.Html_syntax.Html_syntax.Node.Primitives 12 | end 13 | 14 | module Attr = struct 15 | type t = Virtual_dom.Vdom.Attr.t 16 | 17 | include Attr 18 | module Primitives = Virtual_dom.Vdom.Html_syntax.Html_syntax.Attr.Primitives 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /svg/src/virtual_dom_svg.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | module Node = Node 3 | module Attr = Attr 4 | 5 | module Html_syntax : sig 6 | module Html_syntax : sig 7 | module Node : sig 8 | type t = Virtual_dom.Vdom.Node.t 9 | 10 | include module type of Node 11 | module Primitives = Virtual_dom.Vdom.Html_syntax.Html_syntax.Node.Primitives 12 | end 13 | 14 | module Attr : sig 15 | type t = Virtual_dom.Vdom.Attr.t 16 | 17 | include module type of Attr 18 | module Primitives = Virtual_dom.Vdom.Html_syntax.Html_syntax.Attr.Primitives 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom_test) 3 | (preprocess 4 | (pps js_of_ocaml-ppx ppx_jane ppx_html)) 5 | (libraries core js_of_ocaml expect_test_helpers_core virtual_dom 6 | virtual_dom_test_helpers css_gen 7 | expect_test_helpers_core.expect_test_helpers_base 8 | patdiff.expect_test_patdiff ui_effect)) 9 | -------------------------------------------------------------------------------- /test/helpers/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom_test_helpers) 3 | (public_name virtual_dom.vdom_test_helpers) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane)) 6 | (libraries core js_of_ocaml lambdasoup virtual_dom ui_effect vdom_keyboard 7 | uri)) 8 | -------------------------------------------------------------------------------- /test/helpers/handler.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | type t = Js.Unsafe.any 5 | 6 | let of_any_exn t ~name = 7 | if not (Js.equals (Js.typeof t) (Js.string "function")) 8 | then raise_s [%message "handler for" name "is not a function"]; 9 | t 10 | ;; 11 | 12 | let sexp_of_t _ = Sexp.Atom "" 13 | 14 | let throwing_proxy = 15 | let throwing_function missing_field_path = 16 | let missing_field_path = Js.to_string missing_field_path in 17 | raise_s 18 | [%message 19 | (missing_field_path : string) 20 | "The field was read on a fake DOM event. You probably called \ 21 | [Handler.trigger] in a test, and the handler accessed a field of the event \ 22 | that was not provided to [~extra_fields]."] 23 | in 24 | let create_proxy : Js.Unsafe.any -> Js.Unsafe.any -> Js.Unsafe.any -> Js.Unsafe.any = 25 | (* Given a base-object, a function that raises an ocaml exception, 26 | and an optional "path" to the base-object, return a proxy object 27 | that behaves exactly like a read-only [base], but any accesses into 28 | that object which aren't present instead call [raise_missing_field_path] 29 | instead of returning [undefined]. The objects returned via successful 30 | indexes into [base] are also wrapped in a proxy of the same design. 31 | 32 | Because these proxies yield more proxies, the [path] parameter keeps track 33 | of the field names that were accessed in order to get to the current proxy. 34 | This enhances the error message that is thrown back to OCaml. *) 35 | Js.Unsafe.pure_js_expr 36 | {js| 37 | (function create_proxy (base, raise_missing_field_path, path) { 38 | path = path || []; 39 | function get(obj, field) { 40 | if (field in obj) { 41 | var field_value = obj[field]; 42 | if (typeof field_value === 'object') { 43 | return create_proxy(obj[field], raise_missing_field_path, path.concat([field])); 44 | } else { 45 | return field_value; 46 | } 47 | } else { 48 | var missing_field_path = path.concat([field]).join('.'); 49 | raise_missing_field_path(missing_field_path); 50 | } 51 | }; 52 | function thrower(_obj, field) { 53 | var missing_field_path = path.concat([field]).join('.'); 54 | raise_missing_field_path(missing_field_path); 55 | }; 56 | return new Proxy(base, { 57 | get: get, 58 | set: thrower, 59 | deleteProperty: thrower, 60 | enumerate: thrower, 61 | ownKeys: thrower, 62 | has: thrower, 63 | defineProperty: thrower, 64 | getOwnPropertyDescriptor: thrower}); 65 | })|js} 66 | in 67 | fun base -> 68 | let base = base |> List.to_array |> Js.Unsafe.obj |> Js.Unsafe.inject in 69 | let throwing_function = throwing_function |> Js.wrap_callback |> Js.Unsafe.inject in 70 | let path = Js.Unsafe.inject Js.null in 71 | Js.Unsafe.fun_call create_proxy [| base; throwing_function; path |] 72 | ;; 73 | 74 | let trigger ?(extra_fields = []) t = 75 | Js.Unsafe.fun_call t [| throwing_proxy extra_fields |] 76 | ;; 77 | -------------------------------------------------------------------------------- /test/helpers/handler.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | (** The type representing an event handler e.g. onclick, onkeydown, etc... These event 5 | handlers can be triggered, but take care, accessing any of the fields on the event 6 | object in the callback will cause an exception to be raised. *) 7 | type t [@@deriving sexp_of] 8 | 9 | (** Converts a [js_any] into a handler. Throws an exception if the value isn't a 10 | javascript function. *) 11 | val of_any_exn : Js.Unsafe.any -> name:string -> t 12 | 13 | (** Triggers the provided event handler. 14 | 15 | Use the [extra_fields] parameter in order to put extra fields on the event. This will 16 | let you simulate event handlers that read the event object's properties. *) 17 | val trigger : ?extra_fields:(string * Js.Unsafe.any) list -> t -> unit 18 | -------------------------------------------------------------------------------- /test/helpers/node_helpers.mli: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Js_of_ocaml 3 | 4 | type element = 5 | { tag_name : string 6 | ; attributes : (string * string) list 7 | ; string_properties : (string * string) list 8 | ; bool_properties : (string * bool) list 9 | ; styles : (string * string) list 10 | ; handlers : (string * Handler.t) list 11 | ; hooks : (string * Virtual_dom.Vdom.Attr.Hooks.For_testing.Extra.t) list 12 | ; key : string option 13 | ; children : t list 14 | } 15 | [@@deriving sexp_of] 16 | 17 | (** Roughly analogous to {!Vdom.Node.t}, but more easily inspectable and represented as a 18 | pure OCaml type. *) 19 | and t = 20 | | Text of string 21 | | Element of element 22 | | Widget 23 | [@@deriving sexp_of] 24 | 25 | val map : t -> f:(t -> [ `Continue | `Replace_with of t ]) -> t 26 | val is_tag : tag:string -> t -> bool 27 | val has_class : cls:string -> t -> bool 28 | val select : t -> selector:string -> t list 29 | val select_first : t -> selector:string -> t option 30 | val select_first_exn : t -> selector:string -> t 31 | 32 | val to_string_html 33 | : ?filter_printed_attributes:(key:string -> data:string -> bool) 34 | -> ?censor_paths:bool 35 | -> ?censor_hash:bool 36 | -> ?path_censoring_message:string 37 | -> ?hash_censoring_message:string 38 | -> t 39 | -> string 40 | 41 | val inner_text : t -> string 42 | val unsafe_convert_exn : Virtual_dom.Vdom.Node.t -> t 43 | 44 | val trigger 45 | : ?extra_fields:(string * Js.Unsafe.any) list 46 | -> t 47 | -> event_name:string 48 | -> unit 49 | 50 | (** When a hook-based attribute build from an event-returning function, this function will 51 | find the hook, extract the value, call that function with [arg], and schedule the 52 | resulting function. *) 53 | val trigger_hook 54 | : t 55 | -> type_id:'t Type_equal.Id.t 56 | -> name:string 57 | -> f:('t -> 'a -> unit Virtual_dom.Vdom.Effect.t) 58 | -> arg:'a 59 | -> unit 60 | 61 | (** Given an element, this function attempts to retrieve a hook with the name [name], and 62 | the type-id from the hooks [For_testing] module. 63 | 64 | Raises if called on a Widget or Text node, or if a hook matching [name] was found, but 65 | the type id does not match. *) 66 | val get_hook_value_opt : t -> type_id:'a Type_equal.Id.t -> name:string -> 'a option 67 | 68 | (** Like [get_hook_value_opt], but also raises if the hook was not found. *) 69 | val get_hook_value : t -> type_id:'a Type_equal.Id.t -> name:string -> 'a 70 | 71 | module User_actions : sig 72 | (** Convenience functions for {!trigger}, closely modeling user interactions. *) 73 | 74 | val click_on 75 | : ?extra_event_fields:(string * Js.Unsafe.any) list 76 | -> ?shift_key_down:bool 77 | -> ?ctrl_key_down:bool 78 | -> ?alt_key_down:bool 79 | -> ?meta_key_down:bool 80 | -> t 81 | -> unit 82 | 83 | val submit_form : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 84 | val focus : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 85 | 86 | val blur 87 | : ?extra_event_fields:(string * Js.Unsafe.any) list 88 | -> ?related_target:t 89 | -> t 90 | -> unit 91 | 92 | val input_text 93 | : ?extra_event_fields:(string * Js.Unsafe.any) list 94 | -> t 95 | -> text:string 96 | -> unit 97 | 98 | val input_files 99 | : ?extra_event_fields:(string * Js.Unsafe.any) list 100 | -> t 101 | -> files:File.file Js.t list 102 | -> unit 103 | 104 | val keydown 105 | : ?extra_event_fields:(string * Js.Unsafe.any) list 106 | -> ?shift_key_down:bool 107 | -> ?ctrl_key_down:bool 108 | -> ?alt_key_down:bool 109 | -> ?meta_key_down:bool 110 | -> t 111 | -> key:Dom_html.Keyboard_code.t 112 | -> unit 113 | 114 | val set_checkbox 115 | : ?extra_event_fields:(string * Js.Unsafe.any) list 116 | -> ?shift_key_down:bool 117 | -> ?ctrl_key_down:bool 118 | -> ?alt_key_down:bool 119 | -> ?meta_key_down:bool 120 | -> t 121 | -> checked:bool 122 | -> unit 123 | 124 | val change 125 | : ?extra_event_fields:(string * Js.Unsafe.any) list 126 | -> t 127 | -> value:string 128 | -> unit 129 | 130 | val drag : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 131 | val enter : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 132 | val leave : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 133 | val over : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 134 | val drop : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 135 | val end_ : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 136 | val mousemove : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 137 | val mouseenter : ?extra_event_fields:(string * Js.Unsafe.any) list -> t -> unit 138 | 139 | val wheel 140 | : ?extra_event_fields:(string * Js.Unsafe.any) list 141 | -> t 142 | -> delta_y:float 143 | -> unit 144 | end 145 | 146 | module Linter : sig 147 | module Rule : sig 148 | type t = 149 | | Undetectable_clickable_element 150 | | Invalid_tabindex 151 | | Event_handler_html_attribute 152 | | Duplicate_ids 153 | | Whitespace_in_id 154 | | Siblings_have_same_vdom_key 155 | | Unsafe_target_blank 156 | | Clickable_role_but_no_tabindex 157 | | Button_without_valid_type 158 | end 159 | 160 | module Severity : sig 161 | (** Some errors, if not addressed, can unexpectedly crash your app at runtime. For 162 | instance, if two sibling vdom nodes have the same key, vdom will throw an 163 | unrecoverable exception. *) 164 | type t = 165 | | Only_report_app_crashing_errors 166 | | Report_all_errors 167 | end 168 | 169 | val run 170 | : ?expected_failures:Rule.t list 171 | -> ?min_severity:Severity.t 172 | -> t 173 | -> string option 174 | 175 | val print_report 176 | : ?expected_failures:Rule.t list 177 | -> ?min_severity:Severity.t 178 | -> ?on_ok:(unit -> unit) 179 | -> t 180 | -> unit 181 | end 182 | -------------------------------------------------------------------------------- /test/helpers/virtual_dom_test_helpers.ml: -------------------------------------------------------------------------------- 1 | module Handler = Handler 2 | module Node_helpers = Node_helpers 3 | -------------------------------------------------------------------------------- /test/import.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | include Virtual_dom.Vdom 3 | include Virtual_dom_test_helpers 4 | -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open Import 3 | open! Virtual_dom.Vdom 4 | 5 | (* This test doesn't do anything apart from testing that we can actually run tests for 6 | virtual_dom *) 7 | let%test _ = 8 | let previous = Node.div [] in 9 | let current = Node.div [] in 10 | let _patch = Node.Patch.create ~previous ~current in 11 | true 12 | ;; 13 | 14 | let show node = 15 | node |> Node_helpers.unsafe_convert_exn |> [%sexp_of: Node_helpers.t] |> print_s 16 | ;; 17 | 18 | let%expect_test "Node.of_opt" = 19 | show (Some (Node.div []) |> Node.of_opt); 20 | [%expect {| (Element ((tag_name div))) |}]; 21 | show (None |> Node.of_opt); 22 | [%expect {| (Element ((tag_name Vdom.Node.none-widget))) |}] 23 | ;; 24 | 25 | let%expect_test "Node.dfn" = 26 | show (Node.dfn [ Node.text "hi" ]); 27 | [%expect {| (Element ((tag_name dfn) (children ((Text hi))))) |}] 28 | ;; 29 | -------------------------------------------------------------------------------- /test/test.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_attr_multi.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Import 3 | 4 | let show node = 5 | let t = node |> Node_helpers.unsafe_convert_exn in 6 | t |> [%sexp_of: Node_helpers.t] |> print_s; 7 | print_endline "----------------------"; 8 | t |> Node_helpers.to_string_html |> print_endline 9 | ;; 10 | 11 | let%expect_test "combining classes and styles" = 12 | show 13 | (Node.div 14 | ~attrs: 15 | [ Attr.many_without_merge 16 | (Attr.Multi.merge_classes_and_styles 17 | [ Attr.class_ "abc" 18 | ; Attr.class_ "123" 19 | ; Attr.style (Css_gen.margin ~top:(`Px 10) ()) 20 | ; Attr.style (Css_gen.margin ~bottom:(`Px 20) ()) 21 | ]) 22 | ] 23 | []); 24 | [%expect 25 | {| 26 | (Element 27 | ((tag_name div) (attributes ((class "123 abc"))) 28 | (styles ((margin-top 10px) (margin-bottom 20px))))) 29 | ---------------------- 30 |
31 | |}] 32 | ;; 33 | 34 | let%expect_test "add class" = 35 | show 36 | (Node.div 37 | ~attrs: 38 | [ Attr.many_without_merge 39 | (Attr.Multi.add_class [ Attr.autofocus true; Attr.class_ "def" ] "abc") 40 | ] 41 | []); 42 | [%expect 43 | {| 44 | (Element ((tag_name div) (attributes ((autofocus "") (class "abc def"))))) 45 | ---------------------- 46 |
47 | |}] 48 | ;; 49 | 50 | let%expect_test "vdom.attr.lazy_" = 51 | show 52 | (Node.div 53 | ~attrs: 54 | [ Attr.many_without_merge 55 | (Attr.Multi.add_class 56 | [ Attr.autofocus true 57 | ; Attr.class_ "def" 58 | ; Attr.lazy_ 59 | (lazy 60 | (print_endline "forced!"; 61 | Attr.class_ "lazily")) 62 | ] 63 | "abc") 64 | ] 65 | []); 66 | [%expect 67 | {| 68 | forced! 69 | (Element 70 | ((tag_name div) (attributes ((autofocus "") (class "abc def lazily"))))) 71 | ---------------------- 72 |
73 | |}] 74 | ;; 75 | -------------------------------------------------------------------------------- /test/test_attr_multi.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_linter.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_patch.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Import 3 | 4 | let%test "empty patch succeeds" = 5 | let previous = Node.div [] in 6 | let current = Node.div [] in 7 | let patch = Node.Patch.create ~previous ~current in 8 | Node.Patch.is_empty patch 9 | ;; 10 | 11 | let%test "non-empty patch fails" = 12 | let previous = Node.div [ Node.text "Hello" ] in 13 | let current = Node.div [ Node.text "World" ] in 14 | let patch = Node.Patch.create ~previous ~current in 15 | not (Node.Patch.is_empty patch) 16 | ;; 17 | 18 | let%expect_test {|regression: elements with the same event handler produces a patch |} = 19 | let attrs = [ Attr.on_click (fun _ -> Effect.Ignore) ] in 20 | let previous = Node.div ~attrs [] in 21 | let current = Node.div ~attrs [] in 22 | let patch = Node.Patch.create ~previous ~current in 23 | print_s [%message "" ~patch_is_empty:(Node.Patch.is_empty patch : bool)]; 24 | [%expect {| (patch_is_empty true) |}] 25 | ;; 26 | -------------------------------------------------------------------------------- /test/test_patch.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_selector.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Import 3 | 4 | let show selector node = 5 | node 6 | |> Node_helpers.unsafe_convert_exn 7 | |> Node_helpers.select ~selector 8 | |> [%sexp_of: Node_helpers.t list] 9 | |> print_s 10 | ;; 11 | 12 | let%expect_test "select empty div with * selector" = 13 | show "*" (Node.div []); 14 | [%expect {| ((Element ((tag_name div)))) |}] 15 | ;; 16 | 17 | let%expect_test "wrong id selector" = 18 | show "#wrong" (Node.div ~attrs:[ Attr.id "correct" ] []); 19 | [%expect {| () |}] 20 | ;; 21 | 22 | let%expect_test "correct id selector" = 23 | show "#correct" (Node.div ~attrs:[ Attr.id "correct" ] []); 24 | [%expect {| ((Element ((tag_name div) (attributes ((id correct)))))) |}] 25 | ;; 26 | 27 | let%expect_test "multiple classes selector" = 28 | show ".a.b" (Node.div ~attrs:[ Attr.classes [ "a"; "b" ] ] []); 29 | [%expect {| ((Element ((tag_name div) (attributes ((class "a b")))))) |}] 30 | ;; 31 | 32 | let%expect_test "select finds multiple items" = 33 | show 34 | ".a" 35 | (Node.div 36 | [ Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "1" ]) ] [] 37 | ; Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "2" ]) ] [] 38 | ]); 39 | [%expect 40 | {| 41 | ((Element ((tag_name span) (attributes ((id 1) (class a))))) 42 | (Element ((tag_name span) (attributes ((id 2) (class a)))))) 43 | |}] 44 | ;; 45 | 46 | let%expect_test "select nth-child" = 47 | let t = 48 | Node.div 49 | [ Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "1" ]) ] [] 50 | ; Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "2" ]) ] [] 51 | ] 52 | in 53 | show "span:nth-child(1)" t; 54 | [%expect {| ((Element ((tag_name span) (attributes ((id 1) (class a)))))) |}]; 55 | show "span:nth-child(2)" t; 56 | [%expect {| ((Element ((tag_name span) (attributes ((id 2) (class a)))))) |}] 57 | ;; 58 | 59 | let%expect_test "select on node-name" = 60 | let t = 61 | Node.div 62 | [ Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "1" ]) ] [] 63 | ; Node.span ~attrs:[ Attr.(many_without_merge [ class_ "a"; id "2" ]) ] [] 64 | ] 65 | in 66 | show "span" t; 67 | [%expect 68 | {| 69 | ((Element ((tag_name span) (attributes ((id 1) (class a))))) 70 | (Element ((tag_name span) (attributes ((id 2) (class a)))))) 71 | |}] 72 | ;; 73 | 74 | module Person = struct 75 | type t = 76 | { age : int 77 | ; name : string 78 | } 79 | [@@deriving sexp_of] 80 | 81 | let combine _ right = right 82 | end 83 | 84 | module H = Attr.Hooks.Make (struct 85 | module State = Unit 86 | module Input = Person 87 | 88 | let init _input _element = () 89 | let on_mount = `Do_nothing 90 | let update ~old_input:_ ~new_input:_ _state _element = () 91 | let destroy _input _state _element = () 92 | end) 93 | 94 | let%expect_test "print element with a hook" = 95 | show 96 | "*" 97 | (Node.div 98 | ~attrs: 99 | [ Attr.create_hook "unique-name" (H.create { Person.age = 20; name = "person" }) 100 | ] 101 | []); 102 | [%expect 103 | {| ((Element ((tag_name div) (hooks ((unique-name ((age 20) (name person)))))))) |}] 104 | ;; 105 | 106 | let%expect_test "get value out of a hook in a test" = 107 | Node.div 108 | ~attrs: 109 | [ Attr.create_hook "unique-name" (H.create { Person.age = 20; name = "person" }) ] 110 | [] 111 | |> Node_helpers.unsafe_convert_exn 112 | |> Node_helpers.get_hook_value ~type_id:H.For_testing.type_id ~name:"unique-name" 113 | |> Person.sexp_of_t 114 | |> print_s; 115 | [%expect {| ((age 20) (name person)) |}] 116 | ;; 117 | 118 | let%expect_test "try to find hook that doesn't exist" = 119 | Expect_test_helpers_core.require_does_raise (fun () -> 120 | let (_ : _) = 121 | Node.div [] 122 | |> Node_helpers.unsafe_convert_exn 123 | |> Node_helpers.get_hook_value ~type_id:H.For_testing.type_id ~name:"unique-name" 124 | in 125 | ()); 126 | [%expect {| (Failure "get_hook_value: no hook found with name unique-name") |}] 127 | ;; 128 | 129 | let%expect_test "try to find hook on a text node" = 130 | Expect_test_helpers_core.require_does_raise (fun () -> 131 | let (_ : _) = 132 | Node.text "" 133 | |> Node_helpers.unsafe_convert_exn 134 | |> Node_helpers.get_hook_value ~type_id:H.For_testing.type_id ~name:"unique-name" 135 | in 136 | ()); 137 | [%expect {| (Failure "get_hook_value: expected Element, found Text") |}] 138 | ;; 139 | 140 | let%expect_test "try to find hook with a bad type_id" = 141 | Expect_test_helpers_core.require_does_raise (fun () -> 142 | let (_ : _) = 143 | Node.div 144 | ~attrs: 145 | [ Attr.create_hook "unique-name" (H.create { Person.age = 20; name = "person" }) 146 | ] 147 | [] 148 | |> Node_helpers.unsafe_convert_exn 149 | |> Node_helpers.get_hook_value 150 | ~type_id:(Type_equal.Id.create ~name:"" sexp_of_opaque) 151 | ~name:"unique-name" 152 | in 153 | ()); 154 | [%expect 155 | {| 156 | (Failure 157 | "get_hook_value: a hook for unique-name was found, but the type-ids were not the same; are you using the same type-id that you got from the For_testing module from your hook creator?") 158 | |}] 159 | ;; 160 | -------------------------------------------------------------------------------- /test/test_selector.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_to_string.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_unmerged_warning_mode.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Import 3 | 4 | let show node = 5 | let t = node |> Node_helpers.unsafe_convert_exn in 6 | t |> Node_helpers.to_string_html |> print_endline 7 | ;; 8 | 9 | module Expect_test_config = struct 10 | include Expect_test_config 11 | 12 | let sanitize s = Expect_test_helpers_core.hide_positions_in_string (sanitize s) 13 | 14 | let run f = 15 | Virtual_dom.Vdom.Attr.Expert.set_explicitly_print_locations true; 16 | f (); 17 | Virtual_dom.Vdom.Attr.Expert.set_explicitly_print_locations false 18 | ;; 19 | end 20 | 21 | let%expect_test "stop warning after message quota" = 22 | Attr.Unmerged_warning_mode.For_testing.reset_warning_count (); 23 | Attr.Unmerged_warning_mode.current := Stop_after_quota 1; 24 | show 25 | (Node.div 26 | ~attrs: 27 | [ Attr.(many_without_merge [ class_ "a"; class_ "b"; many [ class_ "c" ] ]) ] 28 | []); 29 | [%expect 30 | {| 31 | ("WARNING: not combining classes" (first (a)) (second (b)) 32 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 33 | ("WARNING: reached warning message quota; no more messages will be printed" 34 | (quota 1)) 35 |
36 | |}] 37 | ;; 38 | 39 | let%expect_test "No_warnings prints no warnings" = 40 | Attr.Unmerged_warning_mode.For_testing.reset_warning_count (); 41 | Attr.Unmerged_warning_mode.current := No_warnings; 42 | show 43 | (Node.div 44 | ~attrs: 45 | [ Attr.(many_without_merge [ class_ "a"; class_ "b"; many [ class_ "c" ] ]) ] 46 | []); 47 | [%expect {|
|}] 48 | ;; 49 | 50 | let%expect_test "All_warnings prints warnings" = 51 | Attr.Unmerged_warning_mode.For_testing.reset_warning_count (); 52 | Attr.Unmerged_warning_mode.current := All_warnings; 53 | show 54 | (Node.div 55 | ~attrs: 56 | [ Attr.(many_without_merge [ class_ "a"; class_ "b"; many [ class_ "c" ] ]) ] 57 | []); 58 | [%expect 59 | {| 60 | ("WARNING: not combining classes" (first (a)) (second (b)) 61 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 62 | ("WARNING: not combining classes" (first (b)) (second (c)) 63 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 64 |
65 | |}] 66 | ;; 67 | 68 | let%expect_test "mode transitions are predictable" = 69 | let node () = 70 | Node.div 71 | ~attrs:[ Attr.(many_without_merge [ class_ "a"; class_ "b"; many [ class_ "c" ] ]) ] 72 | [] 73 | in 74 | Attr.Unmerged_warning_mode.For_testing.reset_warning_count (); 75 | Attr.Unmerged_warning_mode.current := No_warnings; 76 | show (node ()); 77 | [%expect {|
|}]; 78 | Attr.Unmerged_warning_mode.current := Stop_after_quota 3; 79 | show (node ()); 80 | [%expect 81 | {| 82 | ("WARNING: not combining classes" (first (a)) (second (b)) 83 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 84 | ("WARNING: reached warning message quota; no more messages will be printed" 85 | (quota 3)) 86 |
87 | |}]; 88 | Attr.Unmerged_warning_mode.current := Stop_after_quota 5; 89 | show (node ()); 90 | [%expect 91 | {| 92 | ("WARNING: not combining classes" (first (a)) (second (b)) 93 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 94 | ("WARNING: reached warning message quota; no more messages will be printed" 95 | (quota 5)) 96 |
97 | |}]; 98 | Attr.Unmerged_warning_mode.current := All_warnings; 99 | show (node ()); 100 | [%expect 101 | {| 102 | ("WARNING: not combining classes" (first (a)) (second (b)) 103 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 104 | ("WARNING: not combining classes" (first (b)) (second (c)) 105 | (here lib/virtual_dom/test/test_unmerged_warning_mode.ml:LINE:COL)) 106 |
107 | |}] 108 | ;; 109 | -------------------------------------------------------------------------------- /test/test_unmerged_warning_mode.mli: -------------------------------------------------------------------------------- 1 | (*_ This signature is deliberately empty. *) 2 | -------------------------------------------------------------------------------- /test/test_vdom_attr_lazy.ml: -------------------------------------------------------------------------------- 1 | open! Core 2 | open! Import 3 | open Virtual_dom 4 | 5 | let show node = 6 | let t = node |> Node_helpers.unsafe_convert_exn in 7 | t |> Node_helpers.to_string_html |> print_endline 8 | ;; 9 | 10 | let%expect_test "vdom.attr.lazy_" = 11 | let lazy_attr = 12 | Vdom.Attr.lazy_ 13 | (lazy 14 | (print_endline "forced!"; 15 | Vdom.Attr.class_ "my-class")) 16 | in 17 | (* not forced yet! *) 18 | [%expect {| |}]; 19 | let element = Node.div ~attrs:[ lazy_attr ] [] in 20 | (* still not forced... *) 21 | [%expect {| |}]; 22 | show element; 23 | (* now they're forced *) 24 | [%expect 25 | {| 26 | forced! 27 |
28 | |}] 29 | ;; 30 | 31 | let%expect_test "multiple vdom.attr.lazy_" = 32 | let lazy_attr_1 = 33 | Vdom.Attr.lazy_ 34 | (lazy 35 | (print_endline "forced 1!"; 36 | Vdom.Attr.class_ "my-class-1")) 37 | in 38 | let lazy_attr_2 = 39 | Vdom.Attr.lazy_ 40 | (lazy 41 | (print_endline "forced 2!"; 42 | Vdom.Attr.class_ "my-class-2")) 43 | in 44 | (* not forced yet! *) 45 | [%expect {| |}]; 46 | let element = Node.div ~attrs:[ lazy_attr_1; lazy_attr_2 ] [] in 47 | (* still not forced... *) 48 | [%expect {| |}]; 49 | show element; 50 | (* now they're forced *) 51 | [%expect 52 | {| 53 | forced 1! 54 | forced 2! 55 |
56 | |}] 57 | ;; 58 | -------------------------------------------------------------------------------- /test/test_vdom_attr_lazy.mli: -------------------------------------------------------------------------------- 1 | (** empty *) 2 | -------------------------------------------------------------------------------- /test/trigger.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janestreet/virtual_dom/9b221deb066ff104fb1a37b33985c0964f426bf6/test/trigger.mli -------------------------------------------------------------------------------- /test/virtual_dom_test.ml: -------------------------------------------------------------------------------- 1 | module Import = Import 2 | module Test = Test 3 | module Test_attr_multi = Test_attr_multi 4 | module Test_linter = Test_linter 5 | module Test_patch = Test_patch 6 | module Test_selector = Test_selector 7 | module Test_to_string = Test_to_string 8 | module Test_unmerged_warning_mode = Test_unmerged_warning_mode 9 | module Test_vdom_attr_lazy = Test_vdom_attr_lazy 10 | module Trigger = Trigger 11 | -------------------------------------------------------------------------------- /tyxml/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name virtual_dom_tyxml) 3 | (public_name virtual_dom.tyxml) 4 | (preprocess 5 | (pps js_of_ocaml-ppx ppx_jane gen_js_api.ppx)) 6 | (libraries virtual_dom ui_effect tyxml.functor js_of_ocaml css_gen base)) 7 | -------------------------------------------------------------------------------- /tyxml/gen_js_api.ml: -------------------------------------------------------------------------------- 1 | module Ojs = Ojs 2 | module Ojs_exn = Ojs_exn 3 | -------------------------------------------------------------------------------- /tyxml/tyxml_f.ml: -------------------------------------------------------------------------------- 1 | module Html_f = Html_f 2 | module Html_sigs = Html_sigs 3 | module Html_types = Html_types 4 | module Svg_f = Svg_f 5 | module Svg_sigs = Svg_sigs 6 | module Svg_types = Svg_types 7 | module Tyxml_f = Tyxml_f 8 | module Xml_iter = Xml_iter 9 | module Xml_print = Xml_print 10 | module Xml_sigs = Xml_sigs 11 | module Xml_stream = Xml_stream 12 | module Xml_wrap = Xml_wrap 13 | -------------------------------------------------------------------------------- /tyxml/virtual_dom_tyxml.ml: -------------------------------------------------------------------------------- 1 | open Js_of_ocaml 2 | open Virtual_dom 3 | open Tyxml_f 4 | open Vdom 5 | 6 | module type XML = 7 | Xml_sigs.T 8 | with type uri = string 9 | and type event_handler = Dom_html.event Js.t -> unit Effect.t 10 | and type mouse_event_handler = Dom_html.mouseEvent Js.t -> unit Effect.t 11 | and type touch_event_handler = Dom_html.touchEvent Js.t -> unit Effect.t 12 | and type keyboard_event_handler = Dom_html.keyboardEvent Js.t -> unit Effect.t 13 | and type elt = Vdom.Node.t 14 | 15 | module Xml = struct 16 | module W = Xml_wrap.NoWrap 17 | 18 | type 'a wrap = 'a 19 | type 'a list_wrap = 'a list 20 | type uri = string 21 | 22 | let uri_of_string s = s 23 | let string_of_uri s = s 24 | 25 | type aname = string 26 | type event_handler = Dom_html.event Js.t -> unit Effect.t 27 | type mouse_event_handler = Dom_html.mouseEvent Js.t -> unit Effect.t 28 | type touch_event_handler = Dom_html.touchEvent Js.t -> unit Effect.t 29 | type keyboard_event_handler = Dom_html.keyboardEvent Js.t -> unit Effect.t 30 | type attrib = Attr.t 31 | 32 | let attr name value = 33 | match name with 34 | | "value" | "checked" | "selected" -> 35 | Attr.property name (Js.Unsafe.inject (Js.string value)) 36 | | name -> Attr.create name value 37 | ;; 38 | 39 | let attr_ev name cvt_to_vdom_event = 40 | let f e = 41 | Effect.Expert.handle e (cvt_to_vdom_event e); 42 | Js._true 43 | in 44 | Attr.property name (Js.Unsafe.inject (Dom.handler f)) 45 | ;; 46 | 47 | let float_attrib name value : attrib = attr name (string_of_float value) 48 | let int_attrib name value = attr name (string_of_int value) 49 | let string_attrib name value = attr name value 50 | let space_sep_attrib name values = attr name (String.concat " " values) 51 | let comma_sep_attrib name values = attr name (String.concat "," values) 52 | let event_handler_attrib name (value : event_handler) = attr_ev name value 53 | let mouse_event_handler_attrib name (value : mouse_event_handler) = attr_ev name value 54 | let touch_event_handler_attrib name (value : touch_event_handler) = attr_ev name value 55 | 56 | let keyboard_event_handler_attrib name (value : keyboard_event_handler) = 57 | attr_ev name value 58 | ;; 59 | 60 | let uri_attrib name value = attr name value 61 | let uris_attrib name values = attr name (String.concat " " values) 62 | 63 | (** Element *) 64 | 65 | type elt = Vdom.Node.t 66 | type ename = string 67 | 68 | let make_a x = x 69 | let empty () = assert false 70 | let comment _c = assert false 71 | let pcdata s = Vdom.Node.text s 72 | let encodedpcdata s = Vdom.Node.text s 73 | 74 | let entity e = 75 | let entity = Dom_html.decode_html_entities (Js.string ("&" ^ e ^ ";")) in 76 | Vdom.Node.text (Js.to_string entity) 77 | ;; 78 | 79 | let leaf ?(a = []) name = 80 | Vdom.Node.create name ~attrs:[ Vdom.Attr.many_without_merge (make_a a) ] [] 81 | ;; 82 | 83 | let node ?(a = []) name children = 84 | Vdom.Node.create name ~attrs:[ Vdom.Attr.many_without_merge (make_a a) ] children 85 | ;; 86 | 87 | let cdata s = pcdata s 88 | let cdata_script s = cdata s 89 | let cdata_style s = cdata s 90 | end 91 | 92 | module Xml_Svg = struct 93 | include Xml 94 | 95 | let leaf ?(a = []) name = 96 | Vdom.Node.create_svg name ~attrs:[ Vdom.Attr.many_without_merge (make_a a) ] [] 97 | ;; 98 | 99 | let node ?(a = []) name children = 100 | Vdom.Node.create_svg name ~attrs:[ Vdom.Attr.many_without_merge (make_a a) ] children 101 | ;; 102 | end 103 | 104 | module Svg = Svg_f.Make (Xml_Svg) 105 | module Html = Html_f.Make (Xml) (Svg) 106 | -------------------------------------------------------------------------------- /tyxml/virtual_dom_tyxml.mli: -------------------------------------------------------------------------------- 1 | open Virtual_dom 2 | open Tyxml_f 3 | open Js_of_ocaml 4 | open Vdom 5 | 6 | module type XML = 7 | Xml_sigs.T 8 | with type uri = string 9 | and type event_handler = Dom_html.event Js.t -> unit Effect.t 10 | and type mouse_event_handler = Dom_html.mouseEvent Js.t -> unit Effect.t 11 | and type touch_event_handler = Dom_html.touchEvent Js.t -> unit Effect.t 12 | and type keyboard_event_handler = Dom_html.keyboardEvent Js.t -> unit Effect.t 13 | and type elt = Vdom.Node.t 14 | 15 | module Xml : XML with module W = Xml_wrap.NoWrap 16 | module Svg : Svg_sigs.Make(Xml).T with module Xml.W = Xml_wrap.NoWrap 17 | module Html : Html_sigs.Make(Xml)(Svg).T with module Xml.W = Xml_wrap.NoWrap 18 | -------------------------------------------------------------------------------- /ui_effect/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name ui_effect) 3 | (public_name virtual_dom.ui_effect) 4 | (libraries base stdio) 5 | (preprocess 6 | (pps ppx_jane))) 7 | -------------------------------------------------------------------------------- /ui_effect/ui_effect.mli: -------------------------------------------------------------------------------- 1 | include Ui_effect_intf.Effect 2 | -------------------------------------------------------------------------------- /ui_effect_of_deferred/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name ui_effect_of_deferred) 3 | (public_name virtual_dom.ui_effect_of_deferred) 4 | (libraries ui_effect async_kernel) 5 | (preprocess 6 | (pps ppx_jane))) 7 | -------------------------------------------------------------------------------- /ui_effect_of_deferred/ui_effect_of_deferred.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Async_kernel 3 | 4 | module Deferred_fun_arg = struct 5 | module Action = struct 6 | type 'r t = T : 'a * ('a -> 'r Deferred.t) -> 'r t 7 | end 8 | 9 | let handle (Action.T (a, f)) ~on_response = 10 | don't_wait_for 11 | (let%map.Deferred result = f a in 12 | on_response result) 13 | ;; 14 | end 15 | 16 | module Deferred_fun = Ui_effect.Define1 (Deferred_fun_arg) 17 | 18 | let of_deferred_fun f a = Deferred_fun.inject (T (a, f)) 19 | let of_deferred_thunk f = of_deferred_fun f () 20 | -------------------------------------------------------------------------------- /ui_effect_of_deferred/ui_effect_of_deferred.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Async_kernel 3 | 4 | (** [of_deferred_fun] is a way to convert from a deferred-returning function to an 5 | effect-returning function. This function is commonly used to wrap RPC calls. *) 6 | val of_deferred_fun : ('query -> 'response Deferred.t) -> 'query -> 'response Ui_effect.t 7 | 8 | (** Like [of_deferred_fun] but with a pre-applied unit query. Side-effects in the function 9 | will be run every time that the resulting effect is scheduled *) 10 | val of_deferred_thunk : (unit -> 'response Deferred.t) -> 'response Ui_effect.t 11 | -------------------------------------------------------------------------------- /virtual_dom.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "Jane Street developers" 3 | authors: ["Jane Street Group, LLC"] 4 | homepage: "https://github.com/janestreet/virtual_dom" 5 | bug-reports: "https://github.com/janestreet/virtual_dom/issues" 6 | dev-repo: "git+https://github.com/janestreet/virtual_dom.git" 7 | doc: "https://ocaml.janestreet.com/ocaml-core/latest/doc/virtual_dom/index.html" 8 | license: "MIT" 9 | build: [ 10 | ["dune" "build" "-p" name "-j" jobs] 11 | ] 12 | depends: [ 13 | "ocaml" {>= "5.1.0"} 14 | "async_kernel" 15 | "base" 16 | "core" 17 | "core_kernel" 18 | "js_of_ocaml_patches" 19 | "ppx_jane" 20 | "sexplib" 21 | "stdio" 22 | "base64" {>= "3.4.0"} 23 | "dune" {>= "3.17.0"} 24 | "gen_js_api" {>= "1.0.8"} 25 | "js_of_ocaml" {>= "6.0.0"} 26 | "js_of_ocaml-ppx" {>= "6.0.0"} 27 | "lambdasoup" {>= "0.6.3"} 28 | "tyxml" {>= "4.3.0"} 29 | "uri" {>= "3.0.0"} 30 | ] 31 | available: arch != "arm32" & arch != "x86_32" 32 | synopsis: "OCaml bindings for the virtual-dom library" 33 | description: " 34 | The library itself may be found at 35 | https://github.com/Matt-Esch/virtual-dom. 36 | " 37 | --------------------------------------------------------------------------------