//! HTML, built with maud, which escapes every interpolated value.
//!
//! The markup follows `shared/reference.html`. The extra controls a page
//! needs without JavaScript (`.nojs`) are hidden unless `` shows
//! them, laid over the parts of the reference design they stand in for.
use maud::{DOCTYPE, Markup, PreEscaped, html};
use crate::db::Todo;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Filter {
All,
Active,
Completed,
}
impl Filter {
/// The `filter` form field. Anything unrecognised means all.
pub fn from_field(s: Option<&str>) -> Filter {
match s {
Some("active") => Filter::Active,
Some("completed") => Filter::Completed,
_ => Filter::All,
}
}
pub fn field(self) -> &'static str {
match self {
Filter::All => "all",
Filter::Active => "active",
Filter::Completed => "completed",
}
}
pub fn path(self) -> &'static str {
match self {
Filter::All => "/",
Filter::Active => "/active",
Filter::Completed => "/completed",
}
}
fn shows(self, todo: &Todo) -> bool {
match self {
Filter::All => true,
Filter::Active => !todo.completed,
Filter::Completed => todo.completed,
}
}
}
pub struct Page<'a> {
pub todos: &'a [Todo],
pub filter: Filter,
pub editing: Option,
pub language: &'a str,
}
const NOJS_CSS: &str = "\
.nojs{display:none}\
.main>form,.footer>form{display:contents}";
const NOSCRIPT_CSS: &str = "\
.nojs{display:block}\
.main .toggle-all-submit{position:absolute;top:-65px;left:0;width:45px;height:65px;font-size:0;cursor:pointer}\
.todo-list li .toggle-submit{position:absolute;top:0;bottom:0;left:0;width:40px;font-size:0;cursor:pointer}\
.todo-list li .edit-link{position:absolute;top:0;bottom:0;left:60px;right:60px;font-size:0}";
pub fn page(p: &Page) -> Markup {
html! {
(DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1";
title { "TodoMVC" }
link rel="stylesheet" href="/static/base.css";
link rel="stylesheet" href="/static/index.css";
style { (PreEscaped(NOJS_CSS)) }
noscript { style { (PreEscaped(NOSCRIPT_CSS)) } }
script type="module" src="/static/app.js" {}
}
body {
section.todoapp {
header.header {
h1 { "todos" }
form method="post" action="/todos" {
input type="hidden" name="filter" value=(p.filter.field());
input.new-todo name="title" placeholder="What needs to be done?"
autocomplete="off" autofocus[p.editing.is_none()];
}
}
(list(p))
}
footer.info {
p { "Double-click to edit a todo" }
p {
"Created by " a href="https://bakeoff.bfj.oncarlos.com" { "the CARLOS bakeoff" }
" ยท " (p.language)
}
p { "Part of " a href="http://todomvc.com" { "TodoMVC" } }
}
}
}
}
}
/// `section.main` and `footer.footer`: everything that changes after an
/// action. JavaScript swaps this fragment in place.
pub fn list(p: &Page) -> Markup {
let total = p.todos.len();
let active = p.todos.iter().filter(|t| !t.completed).count();
let completed = total - active;
let filter = p.filter.field();
html! {
section.main hidden[total == 0] {
input #toggle-all.toggle-all type="checkbox" checked[total > 0 && active == 0];
label for="toggle-all" { "Mark all as complete" }
form method="post" action="/todos/toggle-all" {
input type="hidden" name="filter" value=(filter);
button.nojs.toggle-all-submit data-action="toggle-all" { "Mark all as complete" }
}
ul.todo-list {
@for todo in p.todos.iter().filter(|t| p.filter.shows(t)) {
(item(todo, p.filter, p.editing == Some(todo.id)))
}
}
}
footer.footer hidden[total == 0] {
span.todo-count {
strong { (active) } (if active == 1 { " item left" } else { " items left" })
}
ul.filters {
(filter_link(Filter::All, "All", p.filter)) " "
(filter_link(Filter::Active, "Active", p.filter)) " "
(filter_link(Filter::Completed, "Completed", p.filter))
}
form method="post" action="/todos/clear-completed" {
input type="hidden" name="filter" value=(filter);
button.clear-completed hidden[completed == 0] { "Clear completed" }
}
}
}
}
fn filter_link(f: Filter, text: &str, current: Filter) -> Markup {
html! { li { a class=[(f == current).then_some("selected")] href=(f.path()) { (text) } } }
}
fn item(todo: &Todo, filter: Filter, editing: bool) -> Markup {
let id = todo.id;
let field = filter.field();
let class = match (todo.completed, editing) {
(false, false) => None,
(true, false) => Some("completed"),
(false, true) => Some("editing"),
(true, true) => Some("completed editing"),
};
html! {
li class=[class] data-id=(id) {
div.view {
input.toggle type="checkbox" checked[todo.completed];
label { (todo.title) }
form method="post" action={ "/todos/" (id) "/toggle" } {
input type="hidden" name="filter" value=(field);
button.nojs.toggle-submit data-action="toggle" {
(if todo.completed { "Mark as active" } else { "Mark as complete" })
}
}
a.nojs.edit-link data-action="edit" href={ (filter.path()) "?edit=" (id) } { "Edit" }
form method="post" action={ "/todos/" (id) "/destroy" } {
input type="hidden" name="filter" value=(field);
button.destroy aria-label="Delete" {}
}
}
form method="post" action={ "/todos/" (id) "/edit" } {
input type="hidden" name="filter" value=(field);
input.edit name="title" value=(todo.title) autocomplete="off" autofocus[editing];
}
}
}
}