| 1 | (* The TodoMVC page, rendered from shared/reference.html. The no-JS controls |
| 2 | carry the class "nojs"; app.css hides them once app.js has loaded. *) |
| 3 | |
| 4 | type filter = All | Active | Completed |
| 5 | |
| 6 | let filter_of_string = function "active" -> Active | "completed" -> Completed | _ -> All |
| 7 | let filter_name = function All -> "all" | Active -> "active" | Completed -> "completed" |
| 8 | let filter_path = function All -> "/" | Active -> "/active" | Completed -> "/completed" |
| 9 | |
| 10 | let shows filter (t : Store.todo) = |
| 11 | match filter with All -> true | Active -> not t.completed | Completed -> t.completed |
| 12 | |
| 13 | let escape s = |
| 14 | let b = Buffer.create (String.length s + 16) in |
| 15 | String.iter |
| 16 | (function |
| 17 | | '&' -> Buffer.add_string b "&" |
| 18 | | '<' -> Buffer.add_string b "<" |
| 19 | | '>' -> Buffer.add_string b ">" |
| 20 | | '"' -> Buffer.add_string b """ |
| 21 | | '\'' -> Buffer.add_string b "'" |
| 22 | | c -> Buffer.add_char b c) |
| 23 | s; |
| 24 | Buffer.contents b |
| 25 | |
| 26 | (* A hidden form that a control elsewhere submits with form="<id>". *) |
| 27 | let action_form b ~id ~action filter = |
| 28 | Printf.bprintf b |
| 29 | {|<form id="%s" method="post" action="%s" hidden><input type="hidden" name="filter" value="%s"></form>|} |
| 30 | id action (filter_name filter) |
| 31 | |
| 32 | let item b filter ~editing (t : Store.todo) = |
| 33 | let title = escape t.title in |
| 34 | let classes = List.filter_map Fun.id |
| 35 | [ (if t.completed then Some "completed" else None); (if editing then Some "editing" else None) ] in |
| 36 | Printf.bprintf b {|<li data-id="%d"%s><div class="view">|} t.id |
| 37 | (if classes = [] then "" else Printf.sprintf {| class="%s"|} (String.concat " " classes)); |
| 38 | Printf.bprintf b {|<input class="toggle" type="checkbox" form="toggle-%d"%s><label>%s</label>|} |
| 39 | t.id (if t.completed then " checked" else "") title; |
| 40 | Printf.bprintf b {|<button class="destroy" form="destroy-%d" aria-label="Delete"></button>|} t.id; |
| 41 | Printf.bprintf b |
| 42 | {|<button class="nojs toggle-button" data-action="toggle" form="toggle-%d">%s</button>|} |
| 43 | t.id (if t.completed then "Mark as active" else "Mark as completed"); |
| 44 | Printf.bprintf b {|<a class="nojs edit-link" data-action="edit" href="%s?edit=%d">Edit</a></div>|} |
| 45 | (filter_path filter) t.id; |
| 46 | Printf.bprintf b |
| 47 | {|<form class="edit-form" method="post" action="/todos/%d/edit"><input type="hidden" name="filter" value="%s"><input class="edit" name="title" value="%s" autocomplete="off"%s></form>|} |
| 48 | t.id (filter_name filter) title (if editing then " autofocus" else ""); |
| 49 | action_form b ~id:(Printf.sprintf "toggle-%d" t.id) ~action:(Printf.sprintf "/todos/%d/toggle" t.id) filter; |
| 50 | action_form b ~id:(Printf.sprintf "destroy-%d" t.id) ~action:(Printf.sprintf "/todos/%d/destroy" t.id) filter; |
| 51 | Buffer.add_string b "</li>" |
| 52 | |
| 53 | (* section.todoapp: the part app.js swaps after each action. *) |
| 54 | let todoapp b ~filter ?editing (todos : Store.todo list) = |
| 55 | let left = List.length (List.filter (fun (t : Store.todo) -> not t.completed) todos) in |
| 56 | let done_ = List.length todos - left in |
| 57 | Printf.bprintf b |
| 58 | {|<section class="todoapp"><header class="header"><h1>todos</h1><form method="post" action="/todos"><input type="hidden" name="filter" value="%s"><input class="new-todo" name="title" placeholder="What needs to be done?" autocomplete="off"%s></form></header>|} |
| 59 | (filter_name filter) (if editing = None then " autofocus" else ""); |
| 60 | if todos <> [] then begin |
| 61 | Buffer.add_string b {|<section class="main">|}; |
| 62 | action_form b ~id:"toggle-all-form" ~action:"/todos/toggle-all" filter; |
| 63 | Printf.bprintf b |
| 64 | {|<input id="toggle-all" class="toggle-all" type="checkbox" form="toggle-all-form"%s><label for="toggle-all">Mark all as complete</label>|} |
| 65 | (if left = 0 then " checked" else ""); |
| 66 | Buffer.add_string b |
| 67 | {|<button class="nojs toggle-all-button" data-action="toggle-all" form="toggle-all-form">Mark all as complete</button><ul class="todo-list">|}; |
| 68 | List.iter |
| 69 | (fun (t : Store.todo) -> if shows filter t then item b filter ~editing:(editing = Some t.id) t) |
| 70 | todos; |
| 71 | Buffer.add_string b {|</ul></section><footer class="footer">|}; |
| 72 | Printf.bprintf b {|<span class="todo-count"><strong>%d</strong> %s left</span><ul class="filters">|} |
| 73 | left (if left = 1 then "item" else "items"); |
| 74 | List.iter |
| 75 | (fun (f, label) -> |
| 76 | Printf.bprintf b {|<li><a%s href="%s">%s</a></li>|} |
| 77 | (if f = filter then {| class="selected"|} else "") (filter_path f) label) |
| 78 | [ (All, "All"); (Active, "Active"); (Completed, "Completed") ]; |
| 79 | Buffer.add_string b "</ul>"; |
| 80 | if done_ > 0 then begin |
| 81 | action_form b ~id:"clear-completed-form" ~action:"/todos/clear-completed" filter; |
| 82 | Buffer.add_string b |
| 83 | {|<button class="clear-completed" form="clear-completed-form">Clear completed</button>|} |
| 84 | end; |
| 85 | Buffer.add_string b "</footer>" |
| 86 | end; |
| 87 | Buffer.add_string b "</section>" |
| 88 | |
| 89 | let fragment ~filter todos = |
| 90 | let b = Buffer.create 4096 in |
| 91 | todoapp b ~filter todos; |
| 92 | Buffer.contents b |
| 93 | |
| 94 | let page ~version ~filter ?editing todos = |
| 95 | let b = Buffer.create 8192 in |
| 96 | Printf.bprintf b |
| 97 | {|<!doctype html> |
| 98 | <html lang="en"> |
| 99 | <head> |
| 100 | <meta charset="utf-8"> |
| 101 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 102 | <title>TodoMVC</title> |
| 103 | <script>document.documentElement.classList.add("js")</script> |
| 104 | <link rel="stylesheet" href="/static/base.css?v=%s"> |
| 105 | <link rel="stylesheet" href="/static/index.css?v=%s"> |
| 106 | <link rel="stylesheet" href="/static/app.css?v=%s"> |
| 107 | <script type="module" src="/static/app.js?v=%s"></script> |
| 108 | </head> |
| 109 | <body> |
| 110 | |} |
| 111 | version version version version; |
| 112 | todoapp b ~filter ?editing todos; |
| 113 | Buffer.add_string b |
| 114 | {| |
| 115 | <footer class="info"> |
| 116 | <p>Double-click to edit a todo</p> |
| 117 | <p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · OCaml</p> |
| 118 | <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> |
| 119 | </footer> |
| 120 | </body> |
| 121 | </html> |
| 122 | |}; |
| 123 | Buffer.contents b |
| 124 | |