2018-12-11 13:39:36 -02:00

437 lines
15 KiB
Rust

use crate::controller::ControllerMessage;
use crate::element::Element;
use crate::exit;
use crate::store::ItemList;
use crate::{Message, Scheduler};
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use crate::template::Template;
const ENTER_KEY: u32 = 13;
const ESCAPE_KEY: u32 = 27;
use wasm_bindgen::prelude::*;
/// Messages that represent the methods to be called on the View
pub enum ViewMessage {
UpdateFilterButtons(String),
ClearNewTodo(),
ShowItems(ItemList),
SetItemsLeft(usize),
SetClearCompletedButtonVisibility(bool),
SetCompleteAllCheckbox(bool),
SetMainVisibility(bool),
RemoveItem(String),
EditItemDone(String, String),
SetItemComplete(String, bool),
}
fn item_id(mut element: Element) -> Option<String> {
element.parent_element().map(|mut parent| {
let mut res = None;
let parent_id = parent.dataset_get("id");
if parent_id != "" {
res = Some(parent_id);
} else {
if let Some(mut ep) = parent.parent_element() {
res = Some(ep.dataset_get("id"));
}
}
res.unwrap()
})
}
/// Presentation layer
#[wasm_bindgen]
pub struct View {
sched: RefCell<Rc<Scheduler>>,
todo_list: Element,
todo_item_counter: Element,
clear_completed: Element,
main: Element,
toggle_all: Element,
new_todo: Element,
callbacks: Vec<(web_sys::EventTarget, String, Closure<dyn FnMut()>)>,
}
impl View {
/// Construct a new view
pub fn new(sched: Rc<Scheduler>) -> Option<View> {
let todo_list = Element::qs(".todo-list")?;
let todo_item_counter = Element::qs(".todo-count")?;
let clear_completed = Element::qs(".clear-completed")?;
let main = Element::qs(".main")?;
let toggle_all = Element::qs(".toggle-all")?;
let new_todo = Element::qs(".new-todo")?;
Some(View {
sched: RefCell::new(sched),
todo_list,
todo_item_counter,
clear_completed,
main,
toggle_all,
new_todo,
callbacks: Vec::new(),
})
}
pub fn init(&mut self) {
let window = match web_sys::window() {
Some(w) => w,
None => return,
};
let document = match window.document() {
Some(d) => d,
None => return,
};
let sched = self.sched.clone();
let set_page = Closure::wrap(Box::new(move || {
if let Some(location) = document.location() {
if let Ok(hash) = location.hash() {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::SetPage(hash)));
}
}
}
}) as Box<dyn FnMut()>);
let window_et: web_sys::EventTarget = window.into();
window_et
.add_event_listener_with_callback("hashchange", set_page.as_ref().unchecked_ref())
.unwrap();
set_page.forget(); // Cycle collect this
//self.callbacks.push((window_et, "hashchange".to_string(), set_page));
self.bind_add_item();
self.bind_edit_item_save();
self.bind_edit_item_cancel();
self.bind_remove_item();
self.bind_toggle_item();
self.bind_edit_item();
self.bind_remove_completed();
self.bind_toggle_all();
}
fn bind_edit_item(&mut self) {
self.todo_list.delegate(
"li label",
"dblclick",
|e: web_sys::Event| {
if let Some(target) = e.target() {
View::edit_item(target.into());
}
},
false,
);
}
/// Put an item into edit mode.
fn edit_item(mut el: Element) {
if let Some(mut parent_element) = el.parent_element() {
if let Some(mut list_item) = parent_element.parent_element() {
list_item.class_list_add("editing");
if let Some(mut input) = Element::create_element("input") {
input.set_class_name("edit");
if let Some(text) = el.text_content() {
input.set_value(&text);
}
list_item.append_child(&mut input);
input.focus();
}
}
}
}
/// Used by scheduler to convert a `ViewMessage` into a function call on the `View`
pub fn call(&mut self, method_name: ViewMessage) {
use self::ViewMessage::*;
match method_name {
UpdateFilterButtons(route) => self.update_filter_buttons(&route),
ClearNewTodo() => self.clear_new_todo(),
ShowItems(item_list) => self.show_items(item_list),
SetItemsLeft(count) => self.set_items_left(count),
SetClearCompletedButtonVisibility(visible) => {
self.set_clear_completed_button_visibility(visible)
}
SetCompleteAllCheckbox(complete) => self.set_complete_all_checkbox(complete),
SetMainVisibility(complete) => self.set_main_visibility(complete),
RemoveItem(id) => self.remove_item(&id),
EditItemDone(id, title) => self.edit_item_done(&id, &title),
SetItemComplete(id, completed) => self.set_item_complete(&id, completed),
}
}
/// Populate the todo list with a list of items.
fn show_items(&mut self, items: ItemList) {
self.todo_list.set_inner_html(Template::item_list(items));
}
/// Gets the selector to find a todo item in the DOM
fn get_selector_string(id: &str) -> String {
let mut selector = String::from("[data-id=\"");
selector.push_str(id);
selector.push_str("\"]");
selector
}
/// Remove an item from the view.
fn remove_item(&mut self, id: &str) {
let elem = Element::qs(&View::get_selector_string(id));
if let Some(elem) = elem {
self.todo_list.remove_child(elem);
}
}
/// Set the number in the 'items left' display.
fn set_items_left(&mut self, items_left: usize) {
self.todo_item_counter
.set_inner_html(Template::item_counter(items_left));
}
/// Set the visibility of the "Clear completed" button.
fn set_clear_completed_button_visibility(&mut self, visible: bool) {
self.clear_completed.set_visibility(visible);
}
/// Set the visibility of the main content and footer.
fn set_main_visibility(&mut self, visible: bool) {
self.main.set_visibility(visible);
}
/// Set the checked state of the Complete All checkbox.
fn set_complete_all_checkbox(&mut self, checked: bool) {
self.toggle_all.set_checked(checked);
}
/// Change the appearance of the filter buttons based on the route.
fn update_filter_buttons(&self, route: &str) {
if let Some(mut el) = Element::qs(".filters .selected") {
el.set_class_name("");
}
let mut selector = String::from(".filters [href=\"");
selector.push_str(route);
selector.push_str("\"]");
if let Some(mut el) = Element::qs(&selector) {
el.set_class_name("selected");
}
}
/// Clear the new todo input
fn clear_new_todo(&mut self) {
self.new_todo.set_value("");
}
/// Render an item as either completed or not.
fn set_item_complete(&self, id: &str, completed: bool) {
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
let class_name = if completed { "completed" } else { "" };
list_item.set_class_name(class_name);
// In case it was toggled from an event and not by clicking the checkbox
if let Some(mut el) = list_item.qs_from("input") {
el.set_checked(completed);
}
}
}
/// Bring an item out of edit mode.
fn edit_item_done(&self, id: &str, title: &str) {
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
if let Some(input) = list_item.qs_from("input.edit") {
list_item.class_list_remove("editing");
if let Some(mut list_item_label) = list_item.qs_from("label") {
list_item_label.set_text_content(title);
}
list_item.remove_child(input);
}
}
}
fn bind_add_item(&mut self) {
let sched = self.sched.clone();
let cb = move |event: web_sys::Event| {
if let Some(target) = event.target() {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
let v = input_el.value(); // TODO remove with nll
let title = v.trim();
if title != "" {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::AddItem(
String::from(title),
)));
}
}
}
}
};
self.new_todo.add_event_listener("change", cb);
}
fn bind_remove_completed(&mut self) {
let sched = self.sched.clone();
let handler = move |_| {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::RemoveCompleted()));
}
};
self.clear_completed.add_event_listener("click", handler);
}
fn bind_toggle_all(&mut self) {
let sched = self.sched.clone();
self.toggle_all
.add_event_listener("click", move |event: web_sys::Event| {
if let Some(target) = event.target() {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::ToggleAll(
input_el.checked(),
)));
}
}
}
});
}
fn bind_remove_item(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
".destroy",
"click",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
let el: Element = target.into();
if let Some(item_id) = item_id(el) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::RemoveItem(
item_id,
)));
}
}
}
},
false,
);
}
fn bind_toggle_item(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
".toggle",
"click",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
let mut el: Element = target.into();
let checked = el.checked();
if let Some(item_id) = item_id(el) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::ToggleItem(
item_id, checked,
)));
}
}
}
},
false,
);
}
fn bind_edit_item_save(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
"li .edit",
"blur",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
let mut target_el: Element = target.into();
if target_el.dataset_get("iscancelled") != "true" {
let val = target_el.value();
if let Some(item) = item_id(target_el) {
// TODO refactor back into fn
// Was: &self.add_message(ControllerMessage::SetPage(hash));
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(
ControllerMessage::EditItemSave(item, val),
));
}
// TODO refactor back into fn
}
}
}
},
true,
);
// Remove the cursor from the input when you hit enter just like if it were a real form
self.todo_list.delegate(
"li .edit",
"keypress",
|e: web_sys::Event| {
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
if key_e.key_code() == ENTER_KEY {
if let Some(target) = e.target() {
let mut el: Element = target.into();
el.blur();
}
}
}
},
false,
);
}
fn bind_edit_item_cancel(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
"li .edit",
"keyup",
move |e: web_sys::Event| {
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
if key_e.key_code() == ESCAPE_KEY {
if let Some(target) = e.target() {
let mut el: Element = target.into();
el.dataset_set("iscanceled", "true");
el.blur();
if let Some(item_id) = item_id(el) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(
ControllerMessage::EditItemCancel(item_id),
));
}
}
}
}
}
},
false,
);
}
}
impl Drop for View {
fn drop(&mut self) {
exit("calling drop on view");
let callbacks: Vec<(web_sys::EventTarget, String, Closure<dyn FnMut()>)> =
self.callbacks.drain(..).collect();
for callback in callbacks {
callback
.0
.remove_event_listener_with_callback(
callback.1.as_str(),
&callback.2.as_ref().unchecked_ref(),
)
.unwrap();
}
}
}