├── CHANGES.md ├── src ├── jsoo_todomvc.mli ├── new_todo.mli ├── storage.mli ├── dune ├── package.json ├── footer.mli ├── todo.mli ├── storage.ml ├── index.html ├── yarn.lock ├── new_todo.ml ├── std.ml ├── footer.ml ├── todo.ml └── jsoo_todomvc.ml ├── .gitignore ├── Makefile ├── .ocamlformat ├── README.md ├── docs ├── index.html ├── base.css └── index.css ├── dune-project ├── jsoo_todomvc.opam └── LICENSE /CHANGES.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jsoo_todomvc.mli: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/new_todo.mli: -------------------------------------------------------------------------------- 1 | open Std 2 | 3 | val render : 4 | dispatch:([> `Add of Todo.t] -> unit) -> [> Html_types.header] Html.elt 5 | -------------------------------------------------------------------------------- /src/storage.mli: -------------------------------------------------------------------------------- 1 | open Std 2 | 3 | type t 4 | 5 | val create : unit -> (t, [> `Not_supported of string]) result 6 | val get : t -> Js.js_string Js.t option 7 | val put : t -> Js.js_string Js.t -> unit 8 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name jsoo_todomvc) 3 | (modes js) 4 | (libraries uuidm js_of_ocaml js_of_ocaml-tyxml react reactiveData 5 | ocplib-json-typed ocplib-json-typed-browser) 6 | (preprocess 7 | (pps js_of_ocaml-ppx))) 8 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "todomvc-app-css": "^2.3.0", 9 | "todomvc-common": "^1.0.5" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/footer.mli: -------------------------------------------------------------------------------- 1 | open Std 2 | 3 | type t 4 | 5 | val create : unit -> t 6 | val filter_s : t -> filter React.S.t 7 | 8 | val render : 9 | t 10 | -> totals React.S.t 11 | -> dispatch:([> `Clear_completed] -> unit) 12 | -> [> Html_types.footer] Html.elt 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.annot 2 | *.cmo 3 | *.cma 4 | *.cmi 5 | *.a 6 | *.o 7 | *.cmx 8 | *.cmxs 9 | *.cmxa 10 | 11 | # ocamlbuild working directory 12 | _build/ 13 | 14 | # ocamlbuild targets 15 | *.byte 16 | *.native 17 | 18 | # oasis generated files 19 | setup.data 20 | setup.log 21 | 22 | # Merlin configuring file for Vim and Emacs 23 | .merlin 24 | 25 | # Dune generated files 26 | *.install 27 | 28 | # Local OPAM switch 29 | _opam/ 30 | 31 | # node_modules 32 | node_modules/ 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | b build: 2 | dune b 3 | 4 | p prod : 5 | dune b --profile release 6 | 7 | f fmt : 8 | dune build @fmt --auto-promote 9 | 10 | c clean : 11 | dune clean 12 | 13 | install: 14 | opam install . --deps-only --working-dir --with-test 15 | 16 | dev-switch : 17 | opam switch create . ocaml-base-compiler.4.10.0 --deps-only --with-test --working-dir 18 | 19 | open : 20 | xdg-open _build/default/src/index.html 21 | 22 | .PHONY: b build p prod f fmt c clean install dev-switch open 23 | -------------------------------------------------------------------------------- /src/todo.mli: -------------------------------------------------------------------------------- 1 | open Std 2 | 3 | type t 4 | 5 | val create : ?complete:bool -> ?id:Uuidm.t -> string -> t 6 | val complete : t -> bool 7 | val active : t -> bool 8 | val id : t -> Uuidm.t 9 | val set_complete : t -> complete:bool -> t 10 | val json_encoding : (string * bool * string) Json_encoding.encoding 11 | val to_json_value : t -> string * bool * string 12 | 13 | val render : 14 | t 15 | -> dispatch:([> `Update of t | `Destroy of t] -> unit) 16 | -> filter_s:filter React.S.t 17 | -> [> Html_types.li] Html.elt 18 | -------------------------------------------------------------------------------- /src/storage.ml: -------------------------------------------------------------------------------- 1 | open Std 2 | open Result.O 3 | 4 | type t = Dom_html.storage Js.t 5 | 6 | let key = Js.string "jsoo-todomvc" 7 | let get t = t##getItem key |> Js.Opt.to_option 8 | let put t s = t##setItem key s 9 | 10 | let create () = 11 | let* storage = 12 | Js.Optdef.case 13 | Dom_html.window##.localStorage 14 | (fun () -> 15 | Result.error 16 | (`Not_supported 17 | "HTML5 localStorage functionality not supported by this browser")) 18 | Result.ok 19 | in 20 | Ok storage 21 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.15.0 2 | profile = compact 3 | parse-docstrings = true 4 | break-infix = fit-or-vertical 5 | break-fun-decl = fit-or-vertical 6 | align-cases = true 7 | align-constructors-decl = true 8 | align-variants-decl = true 9 | field-space = loose 10 | sequence-blank-line = preserve-one 11 | break-collection-expressions = fit-or-vertical 12 | exp-grouping = preserve 13 | wrap-fun-args = false 14 | type-decl = sparse 15 | let-and = sparse 16 | dock-collection-brackets = false 17 | doc-comments = after-when-possible 18 | break-cases = all 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todo app in js_of_ocaml ⚡ 2 | 3 | Todo MVC implemented in js_of_ocaml 4 | 5 | [Running Demo](https://lemaetech.github.io/jsoo_todomvc/) 6 | 7 | ``` 8 | git clone git@github.com:bikallem/jsoo_todomvc.git 9 | cd jsoo_todomvc 10 | make install 11 | make build 12 | make open 13 | ``` 14 | 15 | ## js_of_ocaml resources 16 | 17 | 1. https://dune.readthedocs.io/en/latest/dune-files.html#js-of-ocaml - configure `js_of_ocaml` compiler in dune 18 | 19 | 2. https://dune.readthedocs.io/en/latest/jsoo.html - getting started with `js_of_ocaml` projects with dune. 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSOO TODO app 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSOO TODO app 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | todomvc-app-css@^2.3.0: 6 | version "2.3.0" 7 | resolved "https://registry.yarnpkg.com/todomvc-app-css/-/todomvc-app-css-2.3.0.tgz#cc27f1686ab4c418eef4e790289cc124baca2546" 8 | integrity sha512-RG8hxqoVn8Be3wxyuyHfOSAXiY1Z0N+PYQOe/jxzy3wpU1Obfwd1RF1i/fz/fR+MrYL+Q+BdrUt8SsXdtR7Oow== 9 | 10 | todomvc-common@^1.0.5: 11 | version "1.0.5" 12 | resolved "https://registry.yarnpkg.com/todomvc-common/-/todomvc-common-1.0.5.tgz#8c3e799ac9f1fc1573e0c204f984510826914730" 13 | integrity sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ== 14 | -------------------------------------------------------------------------------- /src/new_todo.ml: -------------------------------------------------------------------------------- 1 | open Std 2 | open Dom_html 3 | open Html 4 | 5 | let handle_key_down dispatch evt = 6 | let open Opt.O in 7 | if evt##.keyCode = enter_keycode then 8 | (let* target = evt##.target in 9 | let+ input = CoerceTo.input target in 10 | let todo = Js.to_string input##.value |> Todo.create in 11 | (todo, fun () -> input##.value := Js.string "")) 12 | |> fun o -> 13 | Opt.iter o (fun (todo, reset) -> 14 | `Add todo |> dispatch ; 15 | reset ()) 16 | else () ; 17 | true 18 | 19 | let render ~dispatch = 20 | header 21 | ~a:[a_class ["header"]] 22 | [ h1 [txt "todos"] 23 | ; input 24 | ~a: 25 | [ a_class ["new-todo"] 26 | ; a_placeholder "What needs to be done?" 27 | ; a_autofocus () 28 | ; a_onkeydown @@ handle_key_down dispatch ] 29 | () ] 30 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.5) 2 | 3 | (name jsoo_todomvc) 4 | 5 | (version 1.0.0) 6 | 7 | (generate_opam_files true) 8 | 9 | (source 10 | (github bikallem/jsoo_todomvc)) 11 | 12 | (license MPL-2.0) 13 | 14 | (authors "Bikal Lem") 15 | 16 | (maintainers "Bikal Lem") 17 | 18 | (package 19 | (name jsoo_todomvc) 20 | (synopsis "TODO MVC app in js_of_ocaml") 21 | (description "Uses js_of_ocaml provided libraries only.") 22 | (depends 23 | (ocaml 24 | (>= 4.10.0)) 25 | (dune 26 | (and 27 | :build 28 | (>= 2.5))) 29 | (uuidm 30 | (>= 0.9.7)) 31 | (merlin 32 | (and 33 | :dev 34 | (>= 3.3.4))) 35 | (ocamlformat 36 | (and 37 | :dev 38 | (>= 0.14.1))) 39 | (utop 40 | (and 41 | :dev 42 | (>= 2.5.0))) 43 | (opam-lock 44 | (and 45 | :dev 46 | (>= 0.2))) 47 | (ocplib-json-typed 48 | (>= 0.7.1)) 49 | (ocplib-json-typed-browser 50 | (>= 0.7.1)) 51 | (js_of_ocaml 52 | (>= 3.6.0)) 53 | (js_of_ocaml-ppx 54 | (>= 3.6.0)) 55 | (js_of_ocaml-lwt 56 | (>= 3.6.0)) 57 | (js_of_ocaml-tyxml 58 | (>= 3.6.0)))) 59 | -------------------------------------------------------------------------------- /jsoo_todomvc.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "1.0.0" 4 | synopsis: "TODO MVC app in js_of_ocaml" 5 | description: "Uses js_of_ocaml provided libraries only." 6 | maintainer: ["Bikal Lem"] 7 | authors: ["Bikal Lem"] 8 | license: "MPL-2.0" 9 | homepage: "https://github.com/bikallem/jsoo_todomvc" 10 | bug-reports: "https://github.com/bikallem/jsoo_todomvc/issues" 11 | depends: [ 12 | "ocaml" {>= "4.10.0"} 13 | "dune" {build & >= "2.5"} 14 | "uuidm" {>= "0.9.7"} 15 | "merlin" {dev & >= "3.3.4"} 16 | "ocamlformat" {dev & >= "0.14.1"} 17 | "utop" {dev & >= "2.5.0"} 18 | "opam-lock" {dev & >= "0.2"} 19 | "ocplib-json-typed" {>= "0.7.1"} 20 | "ocplib-json-typed-browser" {>= "0.7.1"} 21 | "js_of_ocaml" {>= "3.6.0"} 22 | "js_of_ocaml-ppx" {>= "3.6.0"} 23 | "js_of_ocaml-lwt" {>= "3.6.0"} 24 | "js_of_ocaml-tyxml" {>= "3.6.0"} 25 | ] 26 | build: [ 27 | ["dune" "subst"] {pinned} 28 | [ 29 | "dune" 30 | "build" 31 | "-p" 32 | name 33 | "-j" 34 | jobs 35 | "@install" 36 | "@runtest" {with-test} 37 | "@doc" {with-doc} 38 | ] 39 | ] 40 | dev-repo: "git+https://github.com/bikallem/jsoo_todomvc.git" 41 | -------------------------------------------------------------------------------- /src/std.ml: -------------------------------------------------------------------------------- 1 | include Js_of_ocaml 2 | 3 | module Opt = struct 4 | include Js_of_ocaml.Js.Opt 5 | 6 | module O = struct 7 | let ( let* ) = bind let ( let+ ) = map let ( >>= ) = bind let ( >>| ) = map 8 | end 9 | end 10 | 11 | module Option = struct 12 | include Option 13 | 14 | let get ~default o = 15 | match o with 16 | | Some a -> a 17 | | None -> default 18 | 19 | module O = struct 20 | let ( let* ) = bind 21 | let ( let+ ) o f = map f o 22 | let ( >>= ) = ( let* ) 23 | let ( >>| ) = ( let+ ) 24 | end 25 | end 26 | 27 | module Result = struct 28 | include Stdlib.Result 29 | 30 | module O = struct 31 | let ( let* ) = bind 32 | let ( let+ ) o f = map f o 33 | let ( >>= ) = ( let* ) 34 | let ( >>| ) = ( let+ ) 35 | end 36 | end 37 | 38 | module List = struct 39 | include Stdlib.List 40 | 41 | let fold_right f b l = List.fold_right f l b 42 | end 43 | 44 | module RList = ReactiveData.RList 45 | module Html = Js_of_ocaml_tyxml.Tyxml_js.Html 46 | module Dom_html = Js_of_ocaml.Dom_html 47 | module R = Js_of_ocaml_tyxml.Tyxml_js.R 48 | module To_dom = Js_of_ocaml_tyxml.Tyxml_js.To_dom 49 | module Log = Js_of_ocaml.Firebug 50 | 51 | type totals = 52 | { total : int 53 | ; completed : int 54 | ; remaining : int } 55 | 56 | let[@inline] ( >> ) f g x = g (f x) 57 | let enter_keycode = 13 58 | let esc_keycode = 27 59 | 60 | type filter = 61 | [ `All 62 | | `Active 63 | | `Completed ] 64 | -------------------------------------------------------------------------------- /src/footer.ml: -------------------------------------------------------------------------------- 1 | open Std 2 | open Html 3 | 4 | type t = 5 | { filter_s : filter React.S.t (** filter change signal. *) 6 | ; change_filter : filter -> unit (** change current filter. *) } 7 | 8 | let filter_s t = t.filter_s 9 | 10 | let current_filter () = 11 | let frag = Url.Current.get_fragment () in 12 | let frag = if String.equal frag "" then "/" else frag in 13 | String.split_on_char '/' frag 14 | |> List.filter (String.equal "" >> not) 15 | |> List.map String.lowercase_ascii 16 | |> function 17 | | "active" :: _ -> `Active 18 | | "completed" :: _ -> `Completed 19 | | [] 20 | |_ -> 21 | `All 22 | 23 | let configure_onfilterchange change_filter = 24 | let handle_hashchange (_ : #Dom_html.hashChangeEvent Js.t) = 25 | current_filter () |> change_filter ; 26 | Js._true in 27 | Dom_html.window##.onhashchange := Dom_html.handler @@ handle_hashchange 28 | 29 | let create () = 30 | let filter_s, change_filter = React.S.create @@ current_filter () in 31 | configure_onfilterchange change_filter ; 32 | {filter_s; change_filter} 33 | 34 | let filter_link lbl url filter t = 35 | a 36 | ~a: 37 | [ a_href url 38 | ; R.filter_attrib 39 | (a_class ["selected"]) 40 | (React.S.map (( = ) filter) t.filter_s) ] 41 | [txt lbl] 42 | 43 | let render t total_s ~dispatch = 44 | let items_left_txt = 45 | React.S.map 46 | (fun {remaining; _} -> 47 | Printf.sprintf 48 | "%i %s left" 49 | remaining 50 | (if remaining <= 1 then "item" else "items")) 51 | total_s in 52 | footer 53 | ~a:[a_class ["footer"]] 54 | [ span ~a:[a_class ["todo-count"]] [R.Html.txt items_left_txt] 55 | ; ul 56 | ~a:[a_class ["filters"]] 57 | [ li [filter_link "All" "#/" `All t] 58 | ; li [filter_link "Active" "#/active" `Active t] 59 | ; li [filter_link "Completed" "#/completed" `Completed t] ] 60 | ; button 61 | ~a: 62 | [ a_class ["clear-completed"] 63 | ; R.filter_attrib 64 | (a_style "display:none") 65 | (React.S.map (fun {completed; _} -> completed <= 0) total_s) 66 | ; a_onclick (fun _ -> 67 | dispatch @@ `Clear_completed ; 68 | true) ] 69 | [txt "Clear completed"] ] 70 | -------------------------------------------------------------------------------- /docs/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/todo.ml: -------------------------------------------------------------------------------- 1 | open Std 2 | open Html 3 | open Dom_html 4 | 5 | type t = 6 | { description : string 7 | ; complete : bool 8 | ; id : Uuidm.t 9 | ; editing_s : bool React.signal 10 | ; set_editing : bool -> unit 11 | ; complete_s : bool React.signal 12 | ; set_complete : bool -> unit } 13 | 14 | let create ?(complete = false) ?(id = Uuidm.create `V4) description = 15 | let editing_s, set_editing = React.S.create false in 16 | let complete_s, set_complete = React.S.create complete in 17 | {description; complete; id; editing_s; set_editing; complete_s; set_complete} 18 | 19 | let json_encoding = 20 | Json_encoding.( 21 | obj3 (req "description" string) (req "complete" bool) (req "id" string)) 22 | 23 | let to_json_value t = (t.description, t.complete, Uuidm.to_string t.id) 24 | let complete t = t.complete 25 | let active t = not t.complete 26 | let id t = t.id 27 | let set_complete t ~complete = {t with complete} 28 | 29 | let handle_dblclick t todo_input _ = 30 | t.set_editing true ; 31 | let dom_inp = To_dom.of_input todo_input in 32 | dom_inp##focus ; true 33 | 34 | let todo_input t dispatch = 35 | let open Opt.O in 36 | let reset_input input_elem = 37 | input_elem 38 | >>= CoerceTo.input 39 | |> fun o -> 40 | Opt.iter o (fun input -> input##.value := Js.string t.description) in 41 | let handle_onblur evt = 42 | t.set_editing false ; 43 | reset_input evt##.target ; 44 | true in 45 | let handle_key_down evt = 46 | if evt##.keyCode = enter_keycode then 47 | ( t.set_editing false ; 48 | let* target = evt##.target in 49 | let+ input = CoerceTo.input target in 50 | Js.to_string input##.value ) 51 | |> fun o -> 52 | Opt.iter o (fun description -> `Update {t with description} |> dispatch) 53 | else if evt##.keyCode = esc_keycode then ( 54 | t.set_editing false ; 55 | reset_input evt##.target ) 56 | else () ; 57 | true in 58 | input 59 | ~a: 60 | [ a_id (Uuidm.to_string t.id) 61 | ; a_input_type `Text 62 | ; a_class ["edit"] 63 | ; a_value t.description 64 | ; a_onblur handle_onblur 65 | ; a_onkeydown handle_key_down ] 66 | () 67 | 68 | let render t ~dispatch ~filter_s = 69 | t.set_complete t.complete ; 70 | let li_cls_attr = 71 | let cls_attr = if complete t then ["completed"] else [] in 72 | R.Html.a_class 73 | @@ React.S.map 74 | (function 75 | | true -> "editing" :: cls_attr 76 | | false -> cls_attr) 77 | t.editing_s in 78 | let complete_toggler = 79 | let handle_onclick (_ : #Dom_html.event Js.t) = 80 | let complete = not t.complete in 81 | `Update {t with complete} |> dispatch ; 82 | t.set_complete complete ; 83 | true in 84 | input 85 | ~a: 86 | [ a_class ["toggle"] 87 | ; a_input_type `Checkbox 88 | ; a_onclick handle_onclick 89 | ; R.filter_attrib (a_checked ()) (React.S.map Fun.id t.complete_s) ] 90 | () in 91 | let todo_input = todo_input t dispatch in 92 | let handle_destroy (_ : #Dom_html.event Js.t) = 93 | dispatch (`Destroy t) ; 94 | true in 95 | li 96 | ~a: 97 | [ li_cls_attr 98 | ; R.filter_attrib 99 | (a_style "display:none") 100 | (React.S.map 101 | (function 102 | | `All -> false 103 | | `Active -> not (active t) 104 | | `Completed -> not (complete t)) 105 | filter_s) ] 106 | [ div 107 | ~a:[a_class ["view"]] 108 | [ complete_toggler 109 | ; label 110 | ~a:[a_ondblclick @@ handle_dblclick t todo_input] 111 | [txt t.description] 112 | ; button ~a:[a_class ["destroy"]; a_onclick handle_destroy] [] ] 113 | ; todo_input ] 114 | -------------------------------------------------------------------------------- /src/jsoo_todomvc.ml: -------------------------------------------------------------------------------- 1 | open Std 2 | open Html 3 | 4 | (*---------------------------------------------------------------------- 5 | * Variable Naming Convention :- 6 | * _s : denotes a signal type React.S.t. Signals are dynamic and the 7 | * value it encodes change according to input. 8 | *----------------------------------------------------------------------*) 9 | module Indextbl = Hashtbl.Make (struct 10 | type t = Uuidm.t 11 | 12 | let equal = Uuidm.equal 13 | let hash (u : Uuidm.t) = Hashtbl.hash u 14 | end) 15 | 16 | type t = 17 | { rl : Todo.t RList.t (** Reactive Todo.t list store. *) 18 | ; rh : Todo.t RList.handle (** Reactive Todo.t list handle to 'rl'. *) 19 | ; total_s : totals React.S.t (** Todo list totals change signal. *) 20 | ; index_tbl : int Indextbl.t 21 | (** Tbl: key - Todo.id; value - index in 'rl'. *) 22 | ; dispatch : action -> unit (** Dispatch action. *) 23 | ; storage : Storage.t option (** Browser localStorage. *) 24 | ; mutable markall_complete : bool (** Markall todos as complete. *) } 25 | 26 | and action = 27 | [ `Add of Todo.t 28 | | `Update of Todo.t 29 | | `Destroy of Todo.t 30 | | `Clear_completed 31 | | `Toggle_all of bool ] 32 | 33 | let update_index rl index_tbl = 34 | let todos = RList.value rl in 35 | List.iteri (fun i todo -> Indextbl.replace index_tbl (Todo.id todo) i) todos 36 | 37 | let json_encoding = Json_encoding.(list Todo.json_encoding) 38 | 39 | let to_json t = 40 | Json_repr_browser.( 41 | Json_encoding.construct 42 | json_encoding 43 | (RList.value t.rl |> List.map Todo.to_json_value) 44 | |> js_stringify) 45 | 46 | let of_json s = 47 | Json_repr_browser.( 48 | parse_js_string s 49 | |> Json_encoding.destruct json_encoding 50 | |> List.fold_right 51 | (fun (description, complete, id) (todos, err_buf) -> 52 | match Uuidm.of_string id with 53 | | Some id -> (Todo.create ~complete ~id description :: todos, err_buf) 54 | | None -> 55 | Buffer.add_string err_buf 56 | @@ Printf.sprintf "Unable to decode id : %s" id ; 57 | (todos, err_buf)) 58 | ([], Buffer.create 5)) 59 | 60 | let update_state t action = 61 | let do_if_index_found todo f = 62 | Todo.id todo |> Indextbl.find_opt t.index_tbl |> Option.iter f in 63 | ( match action with 64 | | `Add todo -> 65 | RList.snoc todo t.rh ; 66 | update_index t.rl t.index_tbl 67 | | `Update todo -> 68 | do_if_index_found todo (fun index -> RList.update todo index t.rh) 69 | | `Destroy todo -> 70 | do_if_index_found todo (fun index -> RList.remove index t.rh) ; 71 | update_index t.rl t.index_tbl 72 | | `Clear_completed -> 73 | RList.value t.rl |> List.filter (Todo.complete >> not) |> RList.set t.rh ; 74 | update_index t.rl t.index_tbl 75 | | `Toggle_all toggle -> 76 | t.markall_complete <- toggle ; 77 | RList.value t.rl 78 | |> List.map (Todo.set_complete ~complete:toggle) 79 | |> RList.set t.rh ; 80 | update_index t.rl t.index_tbl ) ; 81 | Option.iter (fun s -> to_json t |> Storage.put s) t.storage 82 | 83 | let create todos storage = 84 | let calculate_totals rl = 85 | let todos = RList.value rl in 86 | let total = List.length todos in 87 | let remaining = todos |> List.filter (Todo.complete >> not) |> List.length in 88 | let completed = total - remaining in 89 | {total; completed; remaining} in 90 | let rl, rh = RList.create todos in 91 | let index_tbl = Indextbl.create (List.length todos) in 92 | let total_s, set_total = React.S.create @@ calculate_totals rl in 93 | let action_s, dispatch = React.E.create () in 94 | let t = 95 | {rl; rh; index_tbl; total_s; markall_complete = false; dispatch; storage} 96 | in 97 | update_index rl index_tbl ; 98 | 99 | (*--------------------------------------- 100 | Attach reactive mappers/observers. 101 | ---------------------------------------*) 102 | let (_ : unit React.E.t) = React.E.map (update_state t) action_s in 103 | let (_ : unit React.event) = 104 | RList.event rl |> React.E.map (fun _ -> set_total @@ calculate_totals rl) 105 | in 106 | let (_ : unit React.signal) = 107 | React.S.map 108 | (fun {total; completed; _} -> t.markall_complete <- total = completed) 109 | t.total_s in 110 | t 111 | 112 | let main_section ({dispatch; _} as t) = 113 | let footer = Footer.create () in 114 | section 115 | ~a: 116 | [ a_class ["main"] 117 | ; R.filter_attrib 118 | (a_style "display:none") 119 | (React.S.map (fun {total; _} -> total = 0) t.total_s) ] 120 | [ input 121 | ~a: 122 | [ a_id "toggle-all" 123 | ; a_class ["toggle-all"] 124 | ; a_input_type `Checkbox 125 | ; R.filter_attrib 126 | (a_checked ()) 127 | (React.S.map 128 | (fun {total; completed; _} -> total = completed) 129 | t.total_s) 130 | ; a_onclick (fun _ -> 131 | `Toggle_all (not t.markall_complete) |> dispatch ; 132 | true) ] 133 | () 134 | ; label ~a:[a_label_for "toggle-all"] [txt "Mark all as complete"] 135 | ; R.Html.ul ~a:[a_class ["todo-list"]] 136 | @@ RList.map 137 | (Todo.render ~dispatch ~filter_s:(Footer.filter_s footer)) 138 | t.rl 139 | ; Footer.render footer t.total_s ~dispatch ] 140 | 141 | let info_footer = 142 | footer 143 | ~a:[a_class ["info"]] 144 | [ p [txt "Double click to edit a todo"] 145 | ; p 146 | [ txt "Written by " 147 | ; a ~a:[a_href "http://github.com/bikallem/"] [txt "Bikal Lem"] ] 148 | ; p [txt "Part of "; a ~a:[a_href "http://todomvc.com"] [txt "TodoMVC"]] ] 149 | |> To_dom.of_footer 150 | 151 | let main default_todos (_ : #Dom_html.event Js.t) = 152 | let storage = 153 | match Storage.create () with 154 | | Error (`Not_supported msg) -> 155 | Log.console##log (Js.string msg) ; 156 | None 157 | | Ok storage -> Some storage in 158 | let todos = 159 | (let open Option.O in 160 | let* storage = storage in 161 | let+ json = Storage.get storage in 162 | let todos, err_buf = of_json json in 163 | if Buffer.length err_buf > 0 then 164 | Log.console##log (Js.string @@ Buffer.contents err_buf) ; 165 | todos) 166 | |> Option.get ~default:default_todos in 167 | let ({dispatch; _} as t) = create todos storage in 168 | let todo_app = 169 | section ~a:[a_class ["todoapp"]] [New_todo.render ~dispatch; main_section t] 170 | |> To_dom.of_section in 171 | [todo_app; info_footer] 172 | |> List.iter (fun elem -> 173 | Dom.appendChild (Dom_html.getElementById "app") elem) ; 174 | Js.bool true 175 | 176 | let () = 177 | let todos = 178 | [(true, "Buy a unicorn"); (false, "Eat haagen daz ice-cream, yummy!")] 179 | |> List.map (fun (complete, description) -> 180 | Todo.create ~complete description) in 181 | Dom_html.window##.onload := Dom_html.handler @@ main todos 182 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #111111; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: rgba(0, 0, 0, 0.4); 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: rgba(0, 0, 0, 0.4); 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: rgba(0, 0, 0, 0.4); 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -140px; 73 | width: 100%; 74 | font-size: 80px; 75 | font-weight: 200; 76 | text-align: center; 77 | color: #b83f45; 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | color: inherit; 93 | padding: 6px; 94 | border: 1px solid #999; 95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 96 | box-sizing: border-box; 97 | -webkit-font-smoothing: antialiased; 98 | -moz-osx-font-smoothing: grayscale; 99 | } 100 | 101 | .new-todo { 102 | padding: 16px 16px 16px 60px; 103 | border: none; 104 | background: rgba(0, 0, 0, 0.003); 105 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 106 | } 107 | 108 | .main { 109 | position: relative; 110 | z-index: 2; 111 | border-top: 1px solid #e6e6e6; 112 | } 113 | 114 | .toggle-all { 115 | width: 1px; 116 | height: 1px; 117 | border: none; /* Mobile Safari */ 118 | opacity: 0; 119 | position: absolute; 120 | right: 100%; 121 | bottom: 100%; 122 | } 123 | 124 | .toggle-all + label { 125 | width: 60px; 126 | height: 34px; 127 | font-size: 0; 128 | position: absolute; 129 | top: -52px; 130 | left: -13px; 131 | -webkit-transform: rotate(90deg); 132 | transform: rotate(90deg); 133 | } 134 | 135 | .toggle-all + label:before { 136 | content: '❯'; 137 | font-size: 22px; 138 | color: #e6e6e6; 139 | padding: 10px 27px 10px 27px; 140 | } 141 | 142 | .toggle-all:checked + label:before { 143 | color: #737373; 144 | } 145 | 146 | .todo-list { 147 | margin: 0; 148 | padding: 0; 149 | list-style: none; 150 | } 151 | 152 | .todo-list li { 153 | position: relative; 154 | font-size: 24px; 155 | border-bottom: 1px solid #ededed; 156 | } 157 | 158 | .todo-list li:last-child { 159 | border-bottom: none; 160 | } 161 | 162 | .todo-list li.editing { 163 | border-bottom: none; 164 | padding: 0; 165 | } 166 | 167 | .todo-list li.editing .edit { 168 | display: block; 169 | width: calc(100% - 43px); 170 | padding: 12px 16px; 171 | margin: 0 0 0 43px; 172 | } 173 | 174 | .todo-list li.editing .view { 175 | display: none; 176 | } 177 | 178 | .todo-list li .toggle { 179 | text-align: center; 180 | width: 40px; 181 | /* auto, since non-WebKit browsers doesn't support input styling */ 182 | height: auto; 183 | position: absolute; 184 | top: 0; 185 | bottom: 0; 186 | margin: auto 0; 187 | border: none; /* Mobile Safari */ 188 | -webkit-appearance: none; 189 | appearance: none; 190 | } 191 | 192 | .todo-list li .toggle { 193 | opacity: 0; 194 | } 195 | 196 | .todo-list li .toggle + label { 197 | /* 198 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 199 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 200 | */ 201 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 202 | background-repeat: no-repeat; 203 | background-position: center left; 204 | } 205 | 206 | .todo-list li .toggle:checked + label { 207 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 208 | } 209 | 210 | .todo-list li label { 211 | word-break: break-all; 212 | padding: 15px 15px 15px 60px; 213 | display: block; 214 | line-height: 1.2; 215 | transition: color 0.4s; 216 | font-weight: 400; 217 | color: #4d4d4d; 218 | } 219 | 220 | .todo-list li.completed label { 221 | color: #cdcdcd; 222 | text-decoration: line-through; 223 | } 224 | 225 | .todo-list li .destroy { 226 | display: none; 227 | position: absolute; 228 | top: 0; 229 | right: 10px; 230 | bottom: 0; 231 | width: 40px; 232 | height: 40px; 233 | margin: auto 0; 234 | font-size: 30px; 235 | color: #cc9a9a; 236 | margin-bottom: 11px; 237 | transition: color 0.2s ease-out; 238 | } 239 | 240 | .todo-list li .destroy:hover { 241 | color: #af5b5e; 242 | } 243 | 244 | .todo-list li .destroy:after { 245 | content: '×'; 246 | } 247 | 248 | .todo-list li:hover .destroy { 249 | display: block; 250 | } 251 | 252 | .todo-list li .edit { 253 | display: none; 254 | } 255 | 256 | .todo-list li.editing:last-child { 257 | margin-bottom: -1px; 258 | } 259 | 260 | .footer { 261 | padding: 10px 15px; 262 | height: 20px; 263 | text-align: center; 264 | font-size: 15px; 265 | border-top: 1px solid #e6e6e6; 266 | } 267 | 268 | .footer:before { 269 | content: ''; 270 | position: absolute; 271 | right: 0; 272 | bottom: 0; 273 | left: 0; 274 | height: 50px; 275 | overflow: hidden; 276 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 277 | 0 8px 0 -3px #f6f6f6, 278 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 279 | 0 16px 0 -6px #f6f6f6, 280 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 281 | } 282 | 283 | .todo-count { 284 | float: left; 285 | text-align: left; 286 | } 287 | 288 | .todo-count strong { 289 | font-weight: 300; 290 | } 291 | 292 | .filters { 293 | margin: 0; 294 | padding: 0; 295 | list-style: none; 296 | position: absolute; 297 | right: 0; 298 | left: 0; 299 | } 300 | 301 | .filters li { 302 | display: inline; 303 | } 304 | 305 | .filters li a { 306 | color: inherit; 307 | margin: 3px; 308 | padding: 3px 7px; 309 | text-decoration: none; 310 | border: 1px solid transparent; 311 | border-radius: 3px; 312 | } 313 | 314 | .filters li a:hover { 315 | border-color: rgba(175, 47, 47, 0.1); 316 | } 317 | 318 | .filters li a.selected { 319 | border-color: rgba(175, 47, 47, 0.2); 320 | } 321 | 322 | .clear-completed, 323 | html .clear-completed:active { 324 | float: right; 325 | position: relative; 326 | line-height: 20px; 327 | text-decoration: none; 328 | cursor: pointer; 329 | } 330 | 331 | .clear-completed:hover { 332 | text-decoration: underline; 333 | } 334 | 335 | .info { 336 | margin: 65px auto 0; 337 | color: #4d4d4d; 338 | font-size: 11px; 339 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 340 | text-align: center; 341 | } 342 | 343 | .info p { 344 | line-height: 1; 345 | } 346 | 347 | .info a { 348 | color: inherit; 349 | text-decoration: none; 350 | font-weight: 400; 351 | } 352 | 353 | .info a:hover { 354 | text-decoration: underline; 355 | } 356 | 357 | /* 358 | Hack to remove background from Mobile Safari. 359 | Can't use it globally since it destroys checkboxes in Firefox 360 | */ 361 | @media screen and (-webkit-min-device-pixel-ratio:0) { 362 | .toggle-all, 363 | .todo-list li .toggle { 364 | background: none; 365 | } 366 | 367 | .todo-list li .toggle { 368 | height: 40px; 369 | } 370 | } 371 | 372 | @media (max-width: 430px) { 373 | .footer { 374 | height: 50px; 375 | } 376 | 377 | .filters { 378 | bottom: 10px; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------