carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1//! HTML, built with maud, which escapes every interpolated value.
2//!
3//! The markup follows `shared/reference.html`. The extra controls a page
4//! needs without JavaScript (`.nojs`) are hidden unless `<noscript>` shows
5//! them, laid over the parts of the reference design they stand in for.
6
7use maud::{DOCTYPE, Markup, PreEscaped, html};
8
9use crate::db::Todo;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Filter {
13 All,
14 Active,
15 Completed,
16}
17
18impl Filter {
19 /// The `filter` form field. Anything unrecognised means all.
20 pub fn from_field(s: Option<&str>) -> Filter {
21 match s {
22 Some("active") => Filter::Active,
23 Some("completed") => Filter::Completed,
24 _ => Filter::All,
25 }
26 }
27
28 pub fn field(self) -> &'static str {
29 match self {
30 Filter::All => "all",
31 Filter::Active => "active",
32 Filter::Completed => "completed",
33 }
34 }
35
36 pub fn path(self) -> &'static str {
37 match self {
38 Filter::All => "/",
39 Filter::Active => "/active",
40 Filter::Completed => "/completed",
41 }
42 }
43
44 fn shows(self, todo: &Todo) -> bool {
45 match self {
46 Filter::All => true,
47 Filter::Active => !todo.completed,
48 Filter::Completed => todo.completed,
49 }
50 }
51}
52
53pub struct Page<'a> {
54 pub todos: &'a [Todo],
55 pub filter: Filter,
56 pub editing: Option<i64>,
57 pub language: &'a str,
58}
59
60const NOJS_CSS: &str = "\
61.nojs{display:none}\
62.main>form,.footer>form{display:contents}";
63
64const NOSCRIPT_CSS: &str = "\
65.nojs{display:block}\
66.main .toggle-all-submit{position:absolute;top:-65px;left:0;width:45px;height:65px;font-size:0;cursor:pointer}\
67.todo-list li .toggle-submit{position:absolute;top:0;bottom:0;left:0;width:40px;font-size:0;cursor:pointer}\
68.todo-list li .edit-link{position:absolute;top:0;bottom:0;left:60px;right:60px;font-size:0}";
69
70pub fn page(p: &Page) -> Markup {
71 html! {
72 (DOCTYPE)
73 html lang="en" {
74 head {
75 meta charset="utf-8";
76 meta name="viewport" content="width=device-width, initial-scale=1";
77 title { "TodoMVC" }
78 link rel="stylesheet" href="/static/base.css";
79 link rel="stylesheet" href="/static/index.css";
80 style { (PreEscaped(NOJS_CSS)) }
81 noscript { style { (PreEscaped(NOSCRIPT_CSS)) } }
82 script type="module" src="/static/app.js" {}
83 }
84 body {
85 section.todoapp {
86 header.header {
87 h1 { "todos" }
88 form method="post" action="/todos" {
89 input type="hidden" name="filter" value=(p.filter.field());
90 input.new-todo name="title" placeholder="What needs to be done?"
91 autocomplete="off" autofocus[p.editing.is_none()];
92 }
93 }
94 (list(p))
95 }
96 footer.info {
97 p { "Double-click to edit a todo" }
98 p {
99 "Created by " a href="https://bakeoff.bfj.oncarlos.com" { "the CARLOS bakeoff" }
100 " · " (p.language)
101 }
102 p { "Part of " a href="http://todomvc.com" { "TodoMVC" } }
103 }
104 }
105 }
106 }
107}
108
109/// `section.main` and `footer.footer`: everything that changes after an
110/// action. JavaScript swaps this fragment in place.
111pub fn list(p: &Page) -> Markup {
112 let total = p.todos.len();
113 let active = p.todos.iter().filter(|t| !t.completed).count();
114 let completed = total - active;
115 let filter = p.filter.field();
116 html! {
117 section.main hidden[total == 0] {
118 input #toggle-all.toggle-all type="checkbox" checked[total > 0 && active == 0];
119 label for="toggle-all" { "Mark all as complete" }
120 form method="post" action="/todos/toggle-all" {
121 input type="hidden" name="filter" value=(filter);
122 button.nojs.toggle-all-submit data-action="toggle-all" { "Mark all as complete" }
123 }
124 ul.todo-list {
125 @for todo in p.todos.iter().filter(|t| p.filter.shows(t)) {
126 (item(todo, p.filter, p.editing == Some(todo.id)))
127 }
128 }
129 }
130 footer.footer hidden[total == 0] {
131 span.todo-count {
132 strong { (active) } (if active == 1 { " item left" } else { " items left" })
133 }
134 ul.filters {
135 (filter_link(Filter::All, "All", p.filter)) " "
136 (filter_link(Filter::Active, "Active", p.filter)) " "
137 (filter_link(Filter::Completed, "Completed", p.filter))
138 }
139 form method="post" action="/todos/clear-completed" {
140 input type="hidden" name="filter" value=(filter);
141 button.clear-completed hidden[completed == 0] { "Clear completed" }
142 }
143 }
144 }
145}
146
147fn filter_link(f: Filter, text: &str, current: Filter) -> Markup {
148 html! { li { a class=[(f == current).then_some("selected")] href=(f.path()) { (text) } } }
149}
150
151fn item(todo: &Todo, filter: Filter, editing: bool) -> Markup {
152 let id = todo.id;
153 let field = filter.field();
154 let class = match (todo.completed, editing) {
155 (false, false) => None,
156 (true, false) => Some("completed"),
157 (false, true) => Some("editing"),
158 (true, true) => Some("completed editing"),
159 };
160 html! {
161 li class=[class] data-id=(id) {
162 div.view {
163 input.toggle type="checkbox" checked[todo.completed];
164 label { (todo.title) }
165 form method="post" action={ "/todos/" (id) "/toggle" } {
166 input type="hidden" name="filter" value=(field);
167 button.nojs.toggle-submit data-action="toggle" {
168 (if todo.completed { "Mark as active" } else { "Mark as complete" })
169 }
170 }
171 a.nojs.edit-link data-action="edit" href={ (filter.path()) "?edit=" (id) } { "Edit" }
172 form method="post" action={ "/todos/" (id) "/destroy" } {
173 input type="hidden" name="filter" value=(field);
174 button.destroy aria-label="Delete" {}
175 }
176 }
177 form method="post" action={ "/todos/" (id) "/edit" } {
178 input type="hidden" name="filter" value=(field);
179 input.edit name="title" value=(todo.title) autocomplete="off" autofocus[editing];
180 }
181 }
182 }
183}
184