use std::{cell::RefCell, rc, sync::Mutex}; use common::{ron_api, utils::substitute}; use gloo::{ events::{EventListener, EventListenerOptions}, net::http::Request, utils::{document, window}, }; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{ DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, KeyboardEvent, }; use crate::{ modal_dialog, request, toast::{self, Level}, utils::{SelectorExt, by_id, selector, selector_and_clone}, }; pub fn setup_page(recipe_id: i64) { // Title. { let title: HtmlInputElement = by_id("input-title"); // Check if the recipe has been loaded. let mut current_title = title.value(); EventListener::new(&title.clone(), "blur", move |_event| { if title.value() != current_title { current_title = title.value(); let body = ron_api::SetRecipeTitle { recipe_id, title: title.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_title", body).await; reload_recipes_list(recipe_id).await; }); } }) .forget(); } // Description. { let description: HtmlTextAreaElement = by_id("text-area-description"); let mut current_description = description.value(); EventListener::new(&description.clone(), "blur", move |_event| { if description.value() != current_description { current_description = description.value(); let body = ron_api::SetRecipeDescription { recipe_id, description: description.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_description", body).await; }); } }) .forget(); } // Servings. { let servings: HtmlInputElement = by_id("input-servings"); let mut current_servings = servings.value_as_number(); EventListener::new(&servings.clone(), "input", move |_event| { let n = servings.value_as_number(); if n.is_nan() { servings.set_value(""); } if n != current_servings { let servings = if n.is_nan() { None } else { // TODO: Find a better way to validate integer numbers. let n = n as u32; servings.set_value_as_number(n as f64); Some(n) }; current_servings = n; let body = ron_api::SetRecipeServings { recipe_id, servings, }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_servings", body).await; }); } }) .forget(); } // Estimated time. { let estimated_time: HtmlInputElement = by_id("input-estimated-time"); let mut current_time = estimated_time.value_as_number(); EventListener::new(&estimated_time.clone(), "input", move |_event| { let n = estimated_time.value_as_number(); if n.is_nan() { estimated_time.set_value(""); } if n != current_time { let time = if n.is_nan() { None } else { // TODO: Find a better way to validate integer numbers. let n = n as u32; estimated_time.set_value_as_number(n as f64); Some(n) }; current_time = n; let body = ron_api::SetRecipeEstimatedTime { recipe_id, estimated_time: time, }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_estimated_time", body).await; }); } }) .forget(); } // Difficulty. { let difficulty: HtmlSelectElement = by_id("select-difficulty"); let mut current_difficulty = difficulty.value(); EventListener::new(&difficulty.clone(), "blur", move |_event| { if difficulty.value() != current_difficulty { current_difficulty = difficulty.value(); let body = ron_api::SetRecipeDifficulty { recipe_id, difficulty: ron_api::Difficulty::try_from( current_difficulty.parse::().unwrap(), ) .unwrap(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_difficulty", body).await; }); } }) .forget(); } // Tags. { spawn_local(async move { let tags: ron_api::Tags = request::get( "recipe/get_tags", ron_api::Id { id: recipe_id }, /*[("id", &recipe_id.to_string())]*/ ) .await .unwrap(); create_tag_elements(recipe_id, &tags.tags); }); fn add_tags(recipe_id: i64, tags: String) { spawn_local(async move { let tag_list: Vec = tags.split_whitespace().map(str::to_lowercase).collect(); if !tag_list.is_empty() { let body = ron_api::Tags { recipe_id, tags: tag_list.clone(), }; let _ = request::post::<(), _>("recipe/add_tags", body).await; create_tag_elements(recipe_id, &tag_list); } by_id::("input-tags").set_value(""); }); } let input_tags: HtmlInputElement = by_id("input-tags"); EventListener::new(&input_tags.clone(), "input", move |_event| { let tags = input_tags.value(); if tags.ends_with(' ') { add_tags(recipe_id, tags); } }) .forget(); let input_tags: HtmlInputElement = by_id("input-tags"); EventListener::new(&input_tags.clone(), "keypress", move |event| { if let Some(keyboard_event) = event.dyn_ref::() { if keyboard_event.key_code() == 13 { let tags = input_tags.value(); add_tags(recipe_id, tags); } } }) .forget(); let input_tags: HtmlInputElement = by_id("input-tags"); EventListener::new(&input_tags.clone(), "blur", move |_event| { let tags = input_tags.value(); add_tags(recipe_id, tags); }) .forget(); } // Language. { let language: HtmlSelectElement = by_id("select-language"); let mut current_language = language.value(); EventListener::new(&language.clone(), "blur", move |_event| { if language.value() != current_language { current_language = language.value(); let body = ron_api::SetRecipeLanguage { recipe_id, lang: language.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_language", body).await; }); } }) .forget(); } // Is published. { let is_published: HtmlInputElement = by_id("input-is-published"); EventListener::new(&is_published.clone(), "input", move |_event| { let body = ron_api::SetIsPublished { recipe_id, is_published: is_published.checked(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_is_published", body).await; reload_recipes_list(recipe_id).await; }); }) .forget(); } // Delete recipe button. let delete_button: HtmlInputElement = by_id("input-delete"); EventListener::new(&delete_button, "click", move |_event| { spawn_local(async move { if modal_dialog::show_and_initialize( "#hidden-templates .recipe-delete-confirmation", async |element| { let title: HtmlInputElement = by_id("input-title"); element.set_inner_html(&substitute( &element.inner_html(), "{}", &[&title.value()], )); }, ) .await .is_some() { let body = ron_api::Id { id: recipe_id }; let _ = request::delete::<(), _>("recipe/remove", body).await; window().location().set_href("/").unwrap(); } }); }) .forget(); // Load initial groups, steps and ingredients. { spawn_local(async move { let groups: Vec = request::get("recipe/get_groups", ron_api::Id { id: recipe_id }) .await .unwrap(); for group in groups { let group_element = create_group_element(&group); for step in group.steps { let step_element = create_step_element(&group_element.selector(".steps"), &step); for ingredient in step.ingredients { create_ingredient_element( &step_element.selector(".ingredients"), &ingredient, ); } } } }); } // Add a new group. { let button_add_group: HtmlInputElement = by_id("input-add-group"); EventListener::new(&button_add_group, "click", move |_event| { let body = ron_api::Id { id: recipe_id }; spawn_local(async move { let response: ron_api::Id = request::post("recipe/add_group", body).await.unwrap(); create_group_element(&ron_api::Group { id: response.id, name: "".to_string(), comment: "".to_string(), steps: vec![], }); }); }) .forget(); } } fn create_group_element(group: &ron_api::Group) -> Element { let group_id = group.id; let group_element: Element = selector_and_clone("#hidden-templates .group"); group_element.set_id(&format!("group-{}", group.id)); let groups_container: Element = by_id("groups-container"); groups_container.append_child(&group_element).unwrap(); set_draggable(&group_element, "group", |_element| { spawn_local(async move { let ids = by_id::("groups-container") .selector_all::(".group") .into_iter() .map(|e| e.id()[6..].parse::().unwrap()) .collect(); let body = ron_api::Ids { ids }; let _ = request::patch::<(), _>("recipe/set_groups_order", body).await; }); }); // Group name. let name = group_element.selector::(".input-group-name"); name.set_value(&group.name); let mut current_name = group.name.clone(); EventListener::new(&name.clone(), "blur", move |_event| { if name.value() != current_name { current_name = name.value(); let body = ron_api::SetGroupName { group_id, name: name.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_group_name", body).await; }) } }) .forget(); // Group comment. let comment: HtmlInputElement = group_element.selector(".input-group-comment"); comment.set_value(&group.comment); let mut current_comment = group.comment.clone(); EventListener::new(&comment.clone(), "blur", move |_event| { if comment.value() != current_comment { current_comment = comment.value(); let body = ron_api::SetGroupComment { group_id, comment: comment.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_group_comment", body).await; }); } }) .forget(); // Delete button. let group_element_cloned = group_element.clone(); let delete_button: HtmlInputElement = group_element.selector(".input-group-delete"); EventListener::new(&delete_button, "click", move |_event| { // FIXME: How to avoid cloning twice? let group_element_cloned = group_element_cloned.clone(); spawn_local(async move { if modal_dialog::show_and_initialize( "#hidden-templates .recipe-group-delete-confirmation", async move |element| { let name = group_element_cloned .selector::(".input-group-name") .value(); element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name])); }, ) .await .is_some() { let body = ron_api::Id { id: group_id }; let _ = request::delete::<(), _>("recipe/remove_group", body).await; let group_element = by_id::(&format!("group-{}", group_id)); group_element.next_element_sibling().unwrap().remove(); group_element.remove(); } }); }) .forget(); // Add step button. let add_step_button: HtmlInputElement = group_element.selector(".input-add-step"); EventListener::new(&add_step_button, "click", move |_event| { spawn_local(async move { let body = ron_api::Id { id: group_id }; let response: ron_api::Id = request::post("recipe/add_step", body).await.unwrap(); create_step_element( &selector::(&format!("#group-{} .steps", group_id)), &ron_api::Step { id: response.id, action: "".to_string(), ingredients: vec![], }, ); }); }) .forget(); group_element } fn create_tag_elements(recipe_id: i64, tags: &[T]) where T: AsRef, { let tags_span: Element = selector("#container-tags .tags"); // Collect current tags to avoid re-adding an existing tag. let mut current_tags: Vec = vec![]; let mut current_tag_element = tags_span.first_child(); while let Some(element) = current_tag_element { current_tags.push( element .dyn_ref::() .unwrap() .text_content() .unwrap(), ); current_tag_element = element.next_sibling(); } for tag in tags { let tag = tag.as_ref().to_string(); if current_tags.contains(&tag) { continue; } let tag_span = document().create_element("span").unwrap(); tag_span.set_inner_html(&tag); let delete_tag_button: HtmlInputElement = document() .create_element("input") .unwrap() .dyn_into() .unwrap(); delete_tag_button.set_attribute("type", "button").unwrap(); delete_tag_button.set_attribute("value", "X").unwrap(); tag_span.append_child(&delete_tag_button).unwrap(); tags_span.append_child(&tag_span).unwrap(); EventListener::new(&delete_tag_button, "click", move |_event| { let tag_span = tag_span.clone(); let tag = tag.clone(); spawn_local(async move { let body = ron_api::Tags { recipe_id, tags: vec![tag], }; let _ = request::delete::<(), _>("recipe/rm_tags", body).await; tag_span.remove(); }); }) .forget(); } } fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element { let step_id = step.id; let step_element: Element = selector_and_clone("#hidden-templates .step"); step_element.set_id(&format!("step-{}", step.id)); group_element.append_child(&step_element).unwrap(); set_draggable(&step_element, "step", |element| { let element = element.clone(); spawn_local(async move { let ids = element .parent_element() .unwrap() .selector_all::(".step") .into_iter() .map(|e| e.id()[5..].parse::().unwrap()) .collect(); let body = ron_api::Ids { ids }; let _ = request::patch::<(), _>("recipe/set_steps_order", body).await; }); }); // Step action. let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action"); action.set_value(&step.action); let mut current_action = step.action.clone(); EventListener::new(&action.clone(), "blur", move |_event| { if action.value() != current_action { current_action = action.value(); let body = ron_api::SetStepAction { step_id, action: action.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_step_action", body).await; }); } }) .forget(); // Delete button. let step_element_cloned = step_element.clone(); let delete_button: HtmlInputElement = step_element.selector(".input-step-delete"); EventListener::new(&delete_button, "click", move |_event| { // FIXME: How to avoid cloning twice? let step_element_cloned = step_element_cloned.clone(); spawn_local(async move { if modal_dialog::show_and_initialize( "#hidden-templates .recipe-step-delete-confirmation", async move |element| { let action = step_element_cloned .selector::(".text-area-step-action") .value(); element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&action])); }, ) .await .is_some() { let body = ron_api::Id { id: step_id }; let _ = request::delete::<(), _>("recipe/remove_step", body).await; let step_element = by_id::(&format!("step-{}", step_id)); step_element.next_element_sibling().unwrap().remove(); step_element.remove(); } }); }) .forget(); // Add ingredient button. let add_ingredient_button: HtmlInputElement = step_element.selector(".input-add-ingredient"); EventListener::new(&add_ingredient_button, "click", move |_event| { spawn_local(async move { let body = ron_api::Id { id: step_id }; let response: ron_api::Id = request::post("recipe/add_ingredient", body).await.unwrap(); create_ingredient_element( &selector::(&format!("#step-{} .ingredients", step_id)), &ron_api::Ingredient { id: response.id, name: "".to_string(), comment: "".to_string(), quantity_value: None, quantity_unit: "".to_string(), }, ); }); }) .forget(); step_element } fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingredient) -> Element { let ingredient_id = ingredient.id; let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient"); ingredient_element.set_id(&format!("ingredient-{}", ingredient.id)); step_element.append_child(&ingredient_element).unwrap(); set_draggable(&ingredient_element, "ingredient", |element| { let element = element.clone(); spawn_local(async move { let ids = element .parent_element() .unwrap() .selector_all::(".ingredient") .into_iter() .map(|e| e.id()[11..].parse::().unwrap()) .collect(); let body = ron_api::Ids { ids }; let _ = request::patch::<(), _>("recipe/set_ingredients_order", body).await; }); }); // Ingredient name. let name: HtmlInputElement = ingredient_element.selector(".input-ingredient-name"); name.set_value(&ingredient.name); let mut current_name = ingredient.name.clone(); EventListener::new(&name.clone(), "blur", move |_event| { if name.value() != current_name { current_name = name.value(); let body = ron_api::SetIngredientName { ingredient_id, name: name.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_ingredient_name", body).await; }); } }) .forget(); // Ingredient comment. let comment: HtmlInputElement = ingredient_element.selector(".input-ingredient-comment"); comment.set_value(&ingredient.comment); let mut current_comment = ingredient.comment.clone(); EventListener::new(&comment.clone(), "blur", move |_event| { if comment.value() != current_comment { current_comment = comment.value(); let body = ron_api::SetIngredientComment { ingredient_id, comment: comment.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_ingredient_comment", body).await; }); } }) .forget(); // Ingredient quantity. let quantity: HtmlInputElement = ingredient_element.selector(".input-ingredient-quantity"); quantity.set_value( &ingredient .quantity_value .map_or("".to_string(), |q| q.to_string()), ); let mut current_quantity = quantity.value_as_number(); EventListener::new(&quantity.clone(), "input", move |_event| { let n = quantity.value_as_number(); if n.is_nan() { quantity.set_value(""); } if n != current_quantity { let q = if n.is_nan() { None } else { Some(n) }; current_quantity = n; let body = ron_api::SetIngredientQuantity { ingredient_id, quantity: q, }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_ingredient_quantity", body).await; }); } }) .forget(); // Ingredient unit. let unit: HtmlInputElement = ingredient_element.selector(".input-ingredient-unit"); unit.set_value(&ingredient.quantity_unit); let mut current_unit = ingredient.quantity_unit.clone(); EventListener::new(&unit.clone(), "blur", move |_event| { if unit.value() != current_unit { current_unit = unit.value(); let body = ron_api::SetIngredientUnit { ingredient_id, unit: unit.value(), }; spawn_local(async move { let _ = request::patch::<(), _>("recipe/set_ingredient_unit", body).await; }); } }) .forget(); // Delete button. let ingredient_element_cloned = ingredient_element.clone(); let delete_button: HtmlInputElement = ingredient_element.selector(".input-ingredient-delete"); EventListener::new(&delete_button, "click", move |_event| { // FIXME: How to avoid cloning twice? let ingredient_element_cloned = ingredient_element_cloned.clone(); spawn_local(async move { if modal_dialog::show_and_initialize( "#hidden-templates .recipe-ingredient-delete-confirmation", async move |element| { let name = ingredient_element_cloned .selector::(".input-ingredient-name") .value(); element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name])); }, ) .await .is_some() { let body = ron_api::Id { id: ingredient_id }; let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await; let ingredient_element = by_id::(&format!("ingredient-{}", ingredient_id)); ingredient_element.next_element_sibling().unwrap().remove(); ingredient_element.remove(); } }); }) .forget(); ingredient_element } async fn reload_recipes_list(current_recipe_id: i64) { match Request::get("/fragments/recipes_list") .query([("current_recipe_id", current_recipe_id.to_string())]) .send() .await { Err(error) => { toast::show_message(Level::Info, &format!("Internal server error: {}", error)); } Ok(response) => { let list = document().get_element_by_id("recipes-list").unwrap(); list.set_outer_html(&response.text().await.unwrap()); } } } enum CursorPosition { UpperPart, LowerPart, } fn get_cursor_position(mouse_y: f64, element: &Element) -> CursorPosition { let element_y = element.get_bounding_client_rect().y(); // Between 0 (top) and 1 (bottom). let y_relative_pos = (mouse_y - element_y) / element.get_bounding_client_rect().height(); if y_relative_pos < 0.5 { CursorPosition::UpperPart } else { CursorPosition::LowerPart } } fn get_parent_with_id_starting_with(mut element: Element, prefix: &str) -> Element { while !element.id().starts_with(prefix) { element = element.parent_element().unwrap(); } element } // It replaces 'event.data_transfer().unwrap().get_data()/set_data()' because // Chrome prevent to read this during draghover event which is the correct behavior // according the specifications: // * https://html.spec.whatwg.org/multipage/dnd.html#the-drag-data-store static DATA_DRAGGED: Mutex> = Mutex::new(RefCell::new(String::new())); /// Set an element as draggable and add an element before and after /// cloned from "#hidden-templates .dropzone". /// All elements set as draggable in a given container can be dragged /// After or before another element. /// 'element' must have a sub-element with the class '.drag-handle' which /// will be used to drag the element. fn set_draggable(element: &Element, prefix: &str, dropped: T) where T: Fn(&Element) + 'static, { let dropped = rc::Rc::new(dropped); // Add a drop zone before the given element if there is none. if element.previous_element_sibling().is_none() { let dropzone = selector_and_clone::("#hidden-templates .dropzone"); element.before_with_node_1(&dropzone).unwrap(); setup_dragzone_events(&dropzone, prefix, dropped.clone()); } let dropzone = selector_and_clone::("#hidden-templates .dropzone"); element.after_with_node_1(&dropzone).unwrap(); setup_dragzone_events(&dropzone, prefix, dropped.clone()); // DRAGOVER. let prefix_copied = prefix.to_string(); EventListener::new_with_options( element, "dragover", EventListenerOptions::enable_prevent_default(), move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let element: Element = by_id(&drag_data); let element_target = event .current_target() .unwrap() .dyn_into::() .unwrap(); if element.parent_element() != element_target.parent_element() { return; } event.prevent_default(); let cursor_position = get_cursor_position(event.client_y() as f64, &element_target); element_target .previous_element_sibling() .unwrap() .set_class_name(match cursor_position { CursorPosition::UpperPart => "dropzone hover", CursorPosition::LowerPart => "dropzone active", }); element_target .next_element_sibling() .unwrap() .set_class_name(match cursor_position { CursorPosition::UpperPart => "dropzone active", CursorPosition::LowerPart => "dropzone hover", }); } }, ) .forget(); // DRAGLEAVE. let prefix_copied = prefix.to_string(); EventListener::new(element, "dragleave", move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let element: Element = by_id(&drag_data); let element_target = event .current_target() .unwrap() .dyn_into::() .unwrap(); if element.parent_element() != element_target.parent_element() { return; } element_target .previous_element_sibling() .unwrap() .set_class_name("dropzone active"); element_target .next_element_sibling() .unwrap() .set_class_name("dropzone active"); } }) .forget(); // DROP. let prefix_copied = prefix.to_string(); EventListener::new(element, "drop", move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let target: Element = event.current_target().unwrap().dyn_into().unwrap(); let element: Element = by_id(&drag_data); let dropzone: Element = element.next_element_sibling().unwrap(); if element.parent_element() != target.parent_element() { return; } match get_cursor_position(event.client_y() as f64, &element) { CursorPosition::UpperPart => { target .previous_element_sibling() .unwrap() .after_with_node_1(&element) .unwrap(); element.after_with_node_1(&dropzone).unwrap(); } CursorPosition::LowerPart => { target .next_element_sibling() .unwrap() .after_with_node_1(&element) .unwrap(); element.after_with_node_1(&dropzone).unwrap(); } } dropped(&element); } }) .forget(); // MOUSEDOWN. let drag_handle: Element = element.selector(".drag-handle"); EventListener::new(&drag_handle, "mousedown", |event| { event .current_target() .unwrap() .dyn_into::() .unwrap() .parent_element() .unwrap() .set_attribute("draggable", "true") .unwrap(); }) .forget(); // MOUSEUP. EventListener::new(&drag_handle, "mouseup", |event| { event .current_target() .unwrap() .dyn_into::() .unwrap() .parent_element() .unwrap() .set_attribute("draggable", "false") .unwrap(); }) .forget(); // DRAGSTART. let prefix_copied = prefix.to_string(); EventListener::new(element, "dragstart", move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let target_element: Element = event.current_target().unwrap().dyn_into().unwrap(); if target_element.id().starts_with(&prefix_copied) { event.stop_propagation(); // Highlight where the group can be droppped. for dp in target_element .parent_element() .unwrap() .selector_all::(".dropzone") { if dp.parent_element() == target_element.parent_element() { dp.set_class_name("dropzone active"); } } let drag_data_lock = DATA_DRAGGED.lock().unwrap(); drag_data_lock.replace(target_element.id()); event.data_transfer().unwrap().set_effect_allowed("move"); } }) .forget(); // DRAGEND. let prefix_copied = prefix.to_string(); EventListener::new(element, "dragend", move |event| { let target_element: Element = event.current_target().unwrap().dyn_into().unwrap(); target_element.set_attribute("draggable", "false").unwrap(); if target_element.id().starts_with(&prefix_copied) { for dp in target_element .parent_element() .unwrap() .selector_all::(".dropzone") { dp.set_class_name("dropzone"); } } }) .forget(); fn setup_dragzone_events(dropzone: &Element, prefix: &str, dropped: rc::Rc) where T: Fn(&Element) + 'static, { // DRAGOVER (dropzone). let prefix_copied = prefix.to_string(); EventListener::new_with_options( dropzone, "dragover", EventListenerOptions::enable_prevent_default(), move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let element: Element = by_id(&drag_data); let element_target = event .current_target() .unwrap() .dyn_into::() .unwrap(); if element.parent_element() != element_target.parent_element() { return; } event.prevent_default(); event .current_target() .unwrap() .dyn_into::() .unwrap() .set_class_name("dropzone hover"); } }, ) .forget(); // DRAGLEAVE (dropzone). let prefix_copied = prefix.to_string(); EventListener::new(dropzone, "dragleave", move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let element: Element = by_id(&drag_data); let element_target = event .current_target() .unwrap() .dyn_into::() .unwrap(); if element.parent_element() != element_target.parent_element() { return; } event .current_target() .unwrap() .dyn_into::() .unwrap() .set_class_name("dropzone active"); } }) .forget(); // DROP (dropzone). let prefix_copied = prefix.to_string(); EventListener::new(dropzone, "drop", move |event| { let event: &DragEvent = event.dyn_ref::().unwrap(); let drag_data_lock = DATA_DRAGGED.lock().unwrap(); let drag_data = drag_data_lock.borrow(); if drag_data.starts_with(&prefix_copied) { let target: Element = event.current_target().unwrap().dyn_into().unwrap(); let element: Element = by_id(&drag_data); let dropzone: Element = element.next_element_sibling().unwrap(); if element.parent_element() != target.parent_element() { return; } target.after_with_node_1(&element).unwrap(); element.after_with_node_1(&dropzone).unwrap(); dropped(&element); } }) .forget(); } }