use std::{cell::RefCell, rc, sync::Mutex}; use common::{utils::substitute, web_api}; use gloo::{ events::{EventListener, EventListenerOptions}, 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, ron_request, utils::{SelectorExt, by_id, get_current_lang, selector, selector_and_clone}, }; pub fn setup_page(recipe_id: i64) { // Title. { // Here we check if the first element exists, // if not, the recipe edit page hasn't been loaded // and we return from the function without panicking. let title = match document().get_element_by_id("input-title") { Some(e) => e.dyn_into::().unwrap(), None => return, }; let mut current_title = title.value(); EventListener::new(&title.clone(), "blur", move |_event| { if title.value() != current_title { current_title = title.value(); let title = title.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/title"), title, ) .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 description = description.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/description"), description, ) .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 { let n = n as u32; servings.set_value_as_number(n as f64); Some(n) }; current_servings = n; spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/servings"), servings, ) .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 { let n = n as u32; estimated_time.set_value_as_number(n as f64); Some(n) }; current_time = n; spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/estimated_time"), time, ) .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 difficulty = web_api::Difficulty::from_repr(difficulty.value().parse::().unwrap()) .unwrap_or(web_api::Difficulty::Unknown); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/difficulty"), difficulty, ) .await; }); } }) .forget(); } // Tags. { spawn_local(async move { let tags: Vec = ron_request::get(&format!("/ron-api/recipe/{recipe_id}/tags")) .await .unwrap(); create_tag_elements(recipe_id, &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(); let _ = ron_request::post::<(), _>( &format!("/ron-api/recipe/{recipe_id}/tags"), Some(&tag_list), ) .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 language = language.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/language"), language, ) .await; }); } }) .forget(); } // Is public. { let is_public: HtmlInputElement = by_id("input-is-public"); EventListener::new(&is_public.clone(), "input", move |_event| { let is_public = is_public.checked(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/recipe/{recipe_id}/is_public"), is_public, ) .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() { if let Ok(()) = ron_request::delete::<_, ()>(&format!("/ron-api/recipe/{recipe_id}"), None) .await { window() .location() .set_href(&format!( "/{}/?{}={}&{}={}", get_current_lang(), common::consts::GET_PARAMETER_USER_MESSAGE, common::translation::Sentence::RecipeSuccessfullyDeleted as i64, common::consts::GET_PARAMETER_USER_MESSAGE_LEVEL, common::toast::Level::Success as usize )) .unwrap(); } } }); }) .forget(); // Load initial groups, steps and ingredients. { spawn_local(async move { let groups: Vec = ron_request::get(&format!("/ron-api/recipe/{recipe_id}/groups")) .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| { spawn_local(async move { let id: i64 = ron_request::post::<_, ()>(&format!("/ron-api/recipe/{recipe_id}/group"), None) .await .unwrap(); create_group_element(&web_api::Group { id, name: "".to_string(), comment: "".to_string(), steps: vec![], }); }); }) .forget(); } } fn create_group_element(group: &web_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: Vec = by_id::("groups-container") .selector_all::(".group") .into_iter() .map(|e| e.id()[6..].parse::().unwrap()) .collect(); let _ = ron_request::patch::<(), _>("/ron-api/groups/order", ids).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 name = name.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>(&format!("/ron-api/group/{group_id}/name"), name) .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 comment = comment.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/group/{group_id}/comment"), comment, ) .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 _ = ron_request::delete::<(), ()>(&format!("/ron-api/group/{group_id}"), None) .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 id: i64 = ron_request::post::<_, ()>(&format!("/ron-api/group/{group_id}/step"), None) .await .unwrap(); create_step_element( &selector::(&format!("#group-{group_id} .steps")), &web_api::Step { 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_class_name("tag"); 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", "✖").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 _ = ron_request::delete::<(), _>( &format!("/ron-api/recipe/{recipe_id}/tags"), Some(vec![tag]), ) .await; tag_span.remove(); }); }) .forget(); } } fn create_step_element(group_element: &Element, step: &web_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: Vec = element .parent_element() .unwrap() .selector_all::(".step") .into_iter() .map(|e| e.id()[5..].parse::().unwrap()) .collect(); let _ = ron_request::patch::<(), _>("/ron-api/steps/order", ids).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 action = action.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>(&format!("/ron-api/step/{step_id}/action"), action) .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 _ = ron_request::delete::<(), ()>(&format!("/ron-api/step/{step_id}"), None).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 id: i64 = ron_request::post::<_, ()>(&format!("/ron-api/step/{step_id}/ingredient"), None) .await .unwrap(); create_ingredient_element( &selector::(&format!("#step-{} .ingredients", step_id)), &web_api::Ingredient { 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: &web_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: Vec = element .parent_element() .unwrap() .selector_all::(".ingredient") .into_iter() .map(|e| e.id()[11..].parse::().unwrap()) .collect(); let _ = ron_request::patch::<(), _>("/ron-api/ingredients/order", ids).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 name = name.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/ingredient/{ingredient_id}/name"), name, ) .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 comment = comment.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/ingredient/{ingredient_id}/comment"), comment, ) .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; spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/ingredient/{ingredient_id}/quantity"), q, ) .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 unit = unit.value(); spawn_local(async move { let _ = ron_request::patch::<(), _>( &format!("/ron-api/ingredient/{ingredient_id}/unit"), unit, ) .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 _ = ron_request::delete::<(), ()>( &format!("/ron-api/ingredient/{ingredient_id}"), None, ) .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) { let fragment: String = request::get_with_params( "/fragments/recipes_list", web_api::RecipesListFragmentsParams { current_recipe_id: Some(current_recipe_id), }, ) .await .unwrap(); let list = document().get_element_by_id("recipes-list").unwrap(); list.set_outer_html(&fragment); } 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(); } }