mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-03-25 06:21:06 +00:00
437 lines
15 KiB
Rust
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();
|
|
}
|
|
}
|
|
}
|