diff --git a/examples/test/Cargo.toml b/examples/test/Cargo.toml new file mode 100644 index 0000000..ebec9f1 --- /dev/null +++ b/examples/test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "todomvc" +version = "0.1.0" +authors = ["Pauan "] + +[dependencies] +dominator = { path = "../.." } +signals = { path = "../../signals" } + +[dependencies.stdweb] +version = "0.4.0" +features = ["experimental_features_which_may_break_on_minor_version_bumps"] diff --git a/examples/test/src/main.rs b/examples/test/src/main.rs new file mode 100644 index 0000000..a2bba2f --- /dev/null +++ b/examples/test/src/main.rs @@ -0,0 +1,171 @@ +#[macro_use] +extern crate stdweb; + +#[macro_use] +extern crate dominator; + +#[macro_use] +extern crate signals; + +use std::rc::Rc; +use stdweb::web::{document, HtmlElement}; +use stdweb::web::event::ClickEvent; +use stdweb::web::IParentNode; + +use signals::signal; +use signals::signal_vec; +use signals::signal::Signal; +use signals::signal_vec::SignalVec; +use dominator::traits::*; +use dominator::{Dom, text}; + + +fn main() { + stylesheet!("div", { + style("border", "5px solid black"); + }); + + let foobar = class! { + style("border-right", "10px solid purple"); + }; + + /*let media_query = stylesheet!(format!("@media (max-width: 500px) .{}", foobar), { + style("border-left", "10px solid teal"); + });*/ + + let mut count = 0; + + let (sender_count, receiver_count) = signal::unsync::mutable(count); + + + let mut width: u32 = 10; + + let (sender1, receiver1) = signal::unsync::mutable(width); + let (sender2, receiver2) = signal::unsync::mutable(vec![width]); + let (sender3, receiver3) = signal::unsync::mutable(vec![width]); + let (text_sender, text_receiver) = signal::unsync::mutable(format!("{}", width)); + + let (mut sender_elements, receiver_elements) = signal_vec::unsync::mutable(); + + /*let style_width = receiver1.switch(move |x| { + receiver2.clone().switch(move |y| { + receiver3.clone().map(move |z| { + Some(format!("{}px", x + y[0] + z[0])) + }) + }) + });*/ + + let style_width = map_clone! { + let x: Rc = receiver1, + let y: Rc> = receiver2, + let z: Rc> = receiver3 => + Some(format!("{}px", *x + y[0] + z[0])) + }; + + + let mut elements_index = 0; + + let mut increment = move || { + elements_index += 1; + elements_index + }; + + sender_elements.push((increment(), 1)); + sender_elements.push((increment(), 2)); + sender_elements.push((increment(), 3)); + sender_elements.push((increment(), 4)); + sender_elements.push((increment(), 5)); + sender_elements.push((increment(), 6)); + sender_elements.push((increment(), 7)); + + + dominator::append_dom(&document().query_selector("body").unwrap().unwrap(), + html!("div", { + style("border", "10px solid blue"); + children(&mut [ + text("Testing testing!!!"), + + text(text_receiver.dynamic()), + + text(receiver_count.map(|x| format!(" - {}", x)).dynamic()), + + html!("div", { + style("width", style_width.dynamic()); + style("height", "50px"); + style("background-color", "green"); + event(move |event: ClickEvent| { + count += 1; + width += 5; + + console!(log, &event); + + sender1.set(width).unwrap(); + sender2.set(vec![width]).unwrap(); + sender3.set(vec![width]).unwrap(); + text_sender.set(format!("{}", width)).unwrap(); + sender_count.set(count).unwrap(); + sender_elements.push((increment(), 8)); + sender_elements.push((increment(), 0)); + sender_elements.push((increment(), 5)); + sender_elements.push((increment(), 9)); + }); + children( + receiver_elements + .filter_map(|(x, y)| { + if y > 2 { + Some((x, y + 100)) + } else { + None + } + }) + .sort_by(|&(_, a), &(_, b)| { + a.cmp(&b).reverse() + }) + .map(|(x, y)| { + html!("div", { + style("border", "5px solid red"); + style("width", "100px"); + style("height", "50px"); + children(&mut [ + text(format!("({}, {})", x, y)) + ]); + }) + }) + .dynamic() + ); + }), + + html!("div", { + style("width", "50px"); + style("height", "50px"); + style("background-color", "red"); + class(&foobar, true); + children(&mut [ + html!("div", { + style("width", "10px"); + style("height", "10px"); + style("background-color", "orange"); + }) + ]); + }), + + Dom::with_state(Rc::new(vec![1, 2, 3]), |a| { + html!("div", { + style("width", "100px"); + style("height", "100px"); + style("background-color", "orange"); + class("foo", true); + class("bar", false); + event(clone!({ a } move |event: ClickEvent| { + console!(log, &*a, &event); + })); + }) + }), + + html!("input", { + focused(true); + }), + ]); + }) + ); +} diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index af20f18..8af49d2 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -1,174 +1,420 @@ -#![feature(trace_macros)] -#![feature(log_syntax)] - #[macro_use] extern crate stdweb; - #[macro_use] extern crate dominator; - #[macro_use] extern crate signals; use std::rc::Rc; -use stdweb::web::{document, HtmlElement}; -use stdweb::web::event::ClickEvent; -use stdweb::web::IParentNode; +use std::cell::Cell; +use stdweb::web::{window, document, HtmlElement}; +use stdweb::web::event::{InputEvent, ClickEvent, HashChangeEvent, KeyDownEvent, ChangeEvent, DoubleClickEvent, BlurEvent}; +use stdweb::web::html_element::InputElement; +use stdweb::unstable::TryInto; +use stdweb::traits::*; -use signals::signal; -use signals::signal_vec; use signals::signal::Signal; use signals::signal_vec::SignalVec; +use signals::signal::unsync::Mutable; +use signals::signal_vec::unsync::MutableVec; use dominator::traits::*; use dominator::{Dom, text}; +#[derive(Clone, PartialEq, Eq)] +enum Filter { + Active, + Completed, + All, +} + +#[derive(Clone)] +struct Todo { + id: u32, + title: Mutable, + completed: Mutable, + editing: Mutable>, +} + +struct State { + todo_id: Cell, + new_todo_title: Mutable, + todo_list: MutableVec, + filter: Mutable, +} + + +// TODO make this more efficient +#[inline] +fn trim(input: String) -> Option { + let trimmed = input.trim(); + + if trimmed.is_empty() { + None + + } else { + Some(trimmed.to_owned()) + } +} + +#[inline] +fn get_value(event: &InputEvent) -> String { + let target: InputElement = event.target().unwrap().try_into().unwrap(); + target.raw_value() +} + +#[inline] +fn get_checked(event: &ChangeEvent) -> bool { + js!( return @{&event.target()}.checked; ).try_into().unwrap() +} + +#[inline] +fn simple(kind: &str, children: &mut [Dom]) -> Dom { + html!(kind, { + children(children); + }) +} + +#[inline] +fn link(href: &str, t: &str) -> Dom { + html!("a", { + attribute("href", href); + children(&mut [ + text(t), + ]); + }) +} + +fn filter_button(state: Rc, kind: Filter) -> Dom { + html!("a", { + class("selected", state.filter.signal().map(clone!(kind => move |filter| filter == kind)).dynamic()); + attribute("href", match kind { + Filter::Active => "#/active", + Filter::Completed => "#/completed", + Filter::All => "#/", + }); + children(&mut [ + text(match kind { + Filter::Active => "Active", + Filter::Completed => "Completed", + Filter::All => "All", + }) + ]); + }) +} + + fn main() { - stylesheet!("div", { - style("border", "5px solid black"); + let state = Rc::new(State { + todo_id: Cell::new(0), + new_todo_title: Mutable::new("".to_owned()), + todo_list: MutableVec::new(), + filter: Mutable::new(Filter::All), }); - let foobar = class! { - style("border-right", "10px solid purple"); - }; - /*let media_query = stylesheet!(format!("@media (max-width: 500px) .{}", foobar), { - style("border-left", "10px solid teal"); - });*/ + fn update_filter(state: &Rc) { + let hash = document().location().unwrap().hash().unwrap(); - let mut count = 0; + state.filter.set(match hash.as_str() { + "#/active" => Filter::Active, + "#/completed" => Filter::Completed, + _ => Filter::All, + }); + } - let (sender_count, receiver_count) = signal::unsync::mutable(count); + update_filter(&state); + + window().add_event_listener(clone!(state => move |_: HashChangeEvent| { + update_filter(&state); + })); - let mut width: u32 = 10; + let body = document().query_selector("body").unwrap().unwrap(); - let (sender1, receiver1) = signal::unsync::mutable(width); - let (sender2, receiver2) = signal::unsync::mutable(vec![width]); - let (sender3, receiver3) = signal::unsync::mutable(vec![width]); - let (text_sender, text_receiver) = signal::unsync::mutable(format!("{}", width)); - - let (mut sender_elements, receiver_elements) = signal_vec::unsync::mutable(); - - /*let style_width = receiver1.switch(move |x| { - receiver2.clone().switch(move |y| { - receiver3.clone().map(move |z| { - Some(format!("{}px", x + y[0] + z[0])) - }) - }) - });*/ - - let style_width = map_rc! { - let x: Rc = receiver1, - let y: Rc> = receiver2, - let z: Rc> = receiver3 => - Some(format!("{}px", *x + y[0] + z[0])) - }; - - - let mut elements_index = 0; - - let mut increment = move || { - elements_index += 1; - elements_index - }; - - sender_elements.push((increment(), 1)); - sender_elements.push((increment(), 2)); - sender_elements.push((increment(), 3)); - sender_elements.push((increment(), 4)); - sender_elements.push((increment(), 5)); - sender_elements.push((increment(), 6)); - sender_elements.push((increment(), 7)); - - - dominator::append_dom(&document().query_selector("body").unwrap().unwrap(), - html!("div", { - style("border", "10px solid blue"); + dominator::append_dom(&body, + html!("section", { + class("todoapp", true); children(&mut [ - text("Testing testing!!!"), - - text(text_receiver.dynamic()), - - text(receiver_count.map(|x| format!(" - {}", x)).dynamic()), - - html!("div", { - style("width", style_width.dynamic()); - style("height", "50px"); - style("background-color", "green"); - event(move |event: ClickEvent| { - count += 1; - width += 5; - - console!(log, &event); - - sender1.set(width).unwrap(); - sender2.set(vec![width]).unwrap(); - sender3.set(vec![width]).unwrap(); - text_sender.set(format!("{}", width)).unwrap(); - sender_count.set(count).unwrap(); - sender_elements.push((increment(), 8)); - sender_elements.push((increment(), 0)); - sender_elements.push((increment(), 5)); - sender_elements.push((increment(), 9)); - }); - children( - receiver_elements - .filter_map(|(x, y)| { - if y > 2 { - Some((x, y + 100)) - } else { - None - } - }) - .sort_by(|&(_, a), &(_, b)| { - a.cmp(&b).reverse() - }) - .map(|(x, y)| { - html!("div", { - style("border", "5px solid red"); - style("width", "100px"); - style("height", "50px"); - children(&mut [ - text(format!("({}, {})", x, y)) - ]); - }) - }) - .dynamic() - ); - }), - - html!("div", { - style("width", "50px"); - style("height", "50px"); - style("background-color", "red"); - class(&foobar, true); + html!("header", { + class("header", true); children(&mut [ - html!("div", { - style("width", "10px"); - style("height", "10px"); - style("background-color", "orange"); - }) + simple("h1", &mut [ + text("todos"), + ]), + html!("input", { + focused(true); + class("new-todo", true); + attribute("placeholder", "What needs to be done?"); + + property("value", state.new_todo_title.signal().dynamic()); + + event(clone!(state => move |event: InputEvent| { + state.new_todo_title.set(get_value(&event)); + })); + + event(clone!(state => move |event: KeyDownEvent| { + if event.key() == "Enter" { + event.prevent_default(); + + // TODO don't reset it if it only has whitespace + if let Some(title) = trim(state.new_todo_title.replace("".to_owned())) { + let id = state.todo_id.get(); + + state.todo_id.set(id + 1); + + state.todo_list.push(Todo { + id: id, + title: Mutable::new(title), + completed: Mutable::new(false), + editing: Mutable::new(None), + }); + } + } + })); + }), ]); }), - Dom::with_state(Rc::new(vec![1, 2, 3]), |a| { - html!("div", { - style("width", "100px"); - style("height", "100px"); - style("background-color", "orange"); - class("foo", true); - class("bar", false); - event(clone!({ a } move |event: ClickEvent| { - console!(log, &*a, &event); - })); - }) + html!("section", { + class("main", true); + + // Hide if it doesn't have any todos. + property("hidden", state.todo_list.signal_vec() + .len() + .map(|len| len == 0) + .dynamic()); + + children(&mut [ + html!("input", { + class("toggle-all", true); + attribute("id", "toggle-all"); + attribute("type", "checkbox"); + + property("checked", state.todo_list.signal_vec() + .map_signal(|todo| todo.completed.signal()) + // TODO .filter() + .filter_map(|completed| if !completed { Some(()) } else { None }) + .len() + .map(|len| len != 0) + .dynamic()); + + event(clone!(state => move |event: ChangeEvent| { + let checked = !get_checked(&event); + + for todo in state.todo_list.as_slice().iter() { + todo.completed.set(checked); + } + })); + }), + html!("label", { + attribute("for", "toggle-all"); + children(&mut [ + text("Mark all as complete"), + ]); + }), + html!("ul", { + class("todo-list", true); + children(state.todo_list.signal_vec()/*.map_signal(clone!(state => move |todo| { + state.filter.signal().switch(clone!(todo => move |filter| { + // TODO figure out a way to avoid using Box + let filter: Box> = match filter { + // TODO .not() method + Filter::Active => Box::new(todo.completed.signal().map(|completed| !completed)), + Filter::Completed => Box::new(todo.completed.signal()), + Filter::All => Box::new(always(true)), + }; + + filter + })).map_dedupe(move |show| { + if *show { + // TODO figure out a way to avoid this clone + Some(todo.clone()) + } else { + None + } + }) + })).filter_map(|todo| todo)*/.map(clone!(state => move |todo| { + console!(log, "CREATING", todo.title.get()); + + html!("li", { + class("editing", todo.editing.signal().map(|x| x.is_some()).dynamic()); + class("completed", todo.completed.signal().dynamic()); + + property("hidden", + map_clone!( + let filter = state.filter.signal(), + let completed = todo.completed.signal() => + match filter { + Filter::Active => !completed, + Filter::Completed => completed, + Filter::All => true, + } + ) + .map_dedupe(|show| !*show) + .dynamic()); + + children(&mut [ + html!("div", { + class("view", true); + children(&mut [ + html!("input", { + attribute("type", "checkbox"); + class("toggle", true); + property("checked", todo.completed.signal().dynamic()); + event(clone!(todo => move |event: ChangeEvent| { + todo.completed.set(get_checked(&event)); + })); + }), + + html!("label", { + children(&mut [ + text(todo.title.signal().map(|x| { console!(log, &x); x }).dynamic()), + ]); + event(clone!(todo => move |_: DoubleClickEvent| { + todo.editing.set(Some(todo.title.get())); + })); + }), + + html!("button", { + class("destroy", true); + event(clone!(state, todo => move |_: ClickEvent| { + // TODO make this more efficient ? + state.todo_list.retain(|x| x.id != todo.id); + })); + }), + ]); + }), + + html!("input", { + class("edit", true); + property("value", todo.editing.signal().map(|x| x.unwrap_or_else(|| "".to_owned())).dynamic()); + property("hidden", todo.editing.signal().map(|x| x.is_none()).dynamic()); + + // TODO dedupe this somehow ? + focused(todo.editing.signal().map(|x| x.is_some()).dynamic()); + + event(clone!(todo => move |event: KeyDownEvent| { + let key = event.key(); + + if key == "Enter" { + let element: HtmlElement = event.target().unwrap().try_into().unwrap(); + element.blur(); + + } else if key == "Escape" { + todo.editing.set(None); + } + })); + + event(clone!(todo => move |event: InputEvent| { + todo.editing.set(Some(get_value(&event))); + })); + + event(clone!(state, todo => move |_: BlurEvent| { + if let Some(title) = todo.editing.replace(None) { + if let Some(title) = trim(title) { + todo.title.set(title); + + } else { + // TODO make this more efficient ? + state.todo_list.retain(|x| x.id != todo.id); + } + } + })); + }), + ]); + }) + })).dynamic()); + }), + ]); }), - html!("input", { - focused(true); + html!("footer", { + class("footer", true); + + // Hide if it doesn't have any todos. + property("hidden", state.todo_list.signal_vec().len().map(|len| len == 0).dynamic()); + + children(&mut [ + html!("span", { + class("todo-count", true); + children( + state.todo_list.signal_vec() + .map_signal(|todo| todo.completed.signal()) + // TODO .filter() + .filter_map(|completed| if !completed { Some(()) } else { None }) + .len() + .map(|len| { + vec![ + simple("strong", &mut [ + text(len.to_string()) + ]), + text(if len == 1 { + " item left" + } else { + " items left" + }), + ] + }) + .to_signal_vec() + .dynamic() + ); + }), + html!("ul", { + class("filters", true); + children(&mut [ + simple("li", &mut [ + filter_button(state.clone(), Filter::All), + ]), + simple("li", &mut [ + filter_button(state.clone(), Filter::Active), + ]), + simple("li", &mut [ + filter_button(state.clone(), Filter::Completed), + ]), + ]); + }), + html!("button", { + class("clear-completed", true); + + // Hide if it doesn't have any completed items. + property("hidden", state.todo_list.signal_vec() + .map_signal(|todo| todo.completed.signal()) + .filter_map(|completed| if completed { Some(()) } else { None }) + .len() + .map(|len| len == 0) + .dynamic()); + + event(clone!(state => move |_: ClickEvent| { + state.todo_list.retain(|todo| todo.completed.get() == false); + })); + + children(&mut [ + text("Clear completed"), + ]); + }), + ]); }), ]); - }) + }), + ); + + dominator::append_dom(&body, + html!("footer", { + class("info", true); + children(&mut [ + simple("p", &mut [ + text("Double-click to edit a todo"), + ]), + simple("p", &mut [ + text("Created by "), + link("https://github.com/Pauan", "Pauan"), + ]), + simple("p", &mut [ + text("Part of "), + link("http://todomvc.com", "TodoMVC"), + ]), + ]); + }), ); } diff --git a/examples/todomvc/static/index.html b/examples/todomvc/static/index.html new file mode 100644 index 0000000..70aa0b4 --- /dev/null +++ b/examples/todomvc/static/index.html @@ -0,0 +1,14 @@ + + + + + + rust-dominator • TodoMVC + + + + + + + + diff --git a/examples/todomvc/static/lib/todomvc-app-css/index.css b/examples/todomvc/static/lib/todomvc-app-css/index.css new file mode 100644 index 0000000..d8be205 --- /dev/null +++ b/examples/todomvc/static/lib/todomvc-app-css/index.css @@ -0,0 +1,376 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + text-align: center; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + 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/ + */ + 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'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + 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'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/examples/todomvc/static/lib/todomvc-common/base.css b/examples/todomvc/static/lib/todomvc-common/base.css new file mode 100644 index 0000000..da65968 --- /dev/null +++ b/examples/todomvc/static/lib/todomvc-common/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/todomvc/static/lib/todomvc-common/base.js b/examples/todomvc/static/lib/todomvc-common/base.js new file mode 100644 index 0000000..a56b5aa --- /dev/null +++ b/examples/todomvc/static/lib/todomvc-common/base.js @@ -0,0 +1,249 @@ +/* global _ */ +(function () { + 'use strict'; + + /* jshint ignore:start */ + // Underscore's Template Module + // Courtesy of underscorejs.org + var _ = (function (_) { + _.defaults = function (object) { + if (!object) { + return object; + } + for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { + var iterable = arguments[argsIndex]; + if (iterable) { + for (var key in iterable) { + if (object[key] == null) { + object[key] = iterable[key]; + } + } + } + } + return object; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + return _; + })({}); + + if (location.hostname === 'todomvc.com') { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-31081062-1', 'auto'); + ga('send', 'pageview'); + } + /* jshint ignore:end */ + + function redirect() { + if (location.hostname === 'tastejs.github.io') { + location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); + } + } + + function findRoot() { + var base = location.href.indexOf('examples/'); + return location.href.substr(0, base); + } + + function getFile(file, callback) { + if (!location.host) { + return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); + } + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', findRoot() + file, true); + xhr.send(); + + xhr.onload = function () { + if (xhr.status === 200 && callback) { + callback(xhr.responseText); + } + }; + } + + function Learn(learnJSON, config) { + if (!(this instanceof Learn)) { + return new Learn(learnJSON, config); + } + + var template, framework; + + if (typeof learnJSON !== 'object') { + try { + learnJSON = JSON.parse(learnJSON); + } catch (e) { + return; + } + } + + if (config) { + template = config.template; + framework = config.framework; + } + + if (!template && learnJSON.templates) { + template = learnJSON.templates.todomvc; + } + + if (!framework && document.querySelector('[data-framework]')) { + framework = document.querySelector('[data-framework]').dataset.framework; + } + + this.template = template; + + if (learnJSON.backend) { + this.frameworkJSON = learnJSON.backend; + this.frameworkJSON.issueLabel = framework; + this.append({ + backend: true + }); + } else if (learnJSON[framework]) { + this.frameworkJSON = learnJSON[framework]; + this.frameworkJSON.issueLabel = framework; + this.append(); + } + + this.fetchIssueCount(); + } + + Learn.prototype.append = function (opts) { + var aside = document.createElement('aside'); + aside.innerHTML = _.template(this.template, this.frameworkJSON); + aside.className = 'learn'; + + if (opts && opts.backend) { + // Remove demo link + var sourceLinks = aside.querySelector('.source-links'); + var heading = sourceLinks.firstElementChild; + var sourceLink = sourceLinks.lastElementChild; + // Correct link path + var href = sourceLink.getAttribute('href'); + sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); + sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; + } else { + // Localize demo links + var demoLinks = aside.querySelectorAll('.demo-link'); + Array.prototype.forEach.call(demoLinks, function (demoLink) { + if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { + demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); + } + }); + } + + document.body.className = (document.body.className + ' learn-bar').trim(); + document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); + }; + + Learn.prototype.fetchIssueCount = function () { + var issueLink = document.getElementById('issue-count-link'); + if (issueLink) { + var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onload = function (e) { + var parsedResponse = JSON.parse(e.target.responseText); + if (parsedResponse instanceof Array) { + var count = parsedResponse.length; + if (count !== 0) { + issueLink.innerHTML = 'This app has ' + count + ' open issues'; + document.getElementById('issue-count').style.display = 'inline'; + } + } + }; + xhr.send(); + } + }; + + redirect(); + getFile('learn.json', Learn); +})();