From: Greg Burri Date: Sat, 4 Jan 2025 21:43:20 +0000 (+0100) Subject: Tags can now be added and removed X-Git-Url: https://git.euphorik.ch/?a=commitdiff_plain;h=9b0fcec5e23fc6dbd670eb551bb7ebd76c046ba5;p=recipes.git Tags can now be added and removed --- diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index 923d6d8..529457f 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -65,9 +65,9 @@ * Remove the tag to the html list (DOM) * 'enter' key to add the current tag --> -
+
-
+ Result<(), JsValue> { // Tags. { - + spawn_local(async move { + let tags: ron_api::Tags = + request::get("recipe/get_tags", [("recipe_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(String::from).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. @@ -224,9 +273,62 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { }) .forget(); + 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_group_element(group: &ron_api::Group) -> Element { let group_id = group.id; - let group_element: Element = select_and_clone("#hidden-templates .group"); + let group_element: Element = selector_and_clone("#hidden-templates .group"); group_element .set_attribute("id", &format!("group-{}", group.id)) .unwrap(); @@ -235,7 +337,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { groups_container.append_child(&group_element).unwrap(); // Group name. - let name = group_element.select::(".input-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| { @@ -253,7 +355,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Group comment. - let comment: HtmlInputElement = group_element.select(".input-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| { @@ -272,10 +374,10 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { // Delete button. let group_element_cloned = group_element.clone(); - let delete_button: HtmlInputElement = group_element.select(".input-group-delete"); + let delete_button: HtmlInputElement = group_element.selector(".input-group-delete"); EventListener::new(&delete_button, "click", move |_event| { let name = group_element_cloned - .select::(".input-group-name") + .selector::(".input-group-name") .value(); spawn_local(async move { if modal_dialog::show(&format!("Are you sure to delete the group '{}'", name)).await @@ -289,14 +391,14 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Add step button. - let add_step_button: HtmlInputElement = group_element.select(".input-add-step"); + 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::AddRecipeStep { group_id }; let response: ron_api::AddRecipeStepResult = request::post("recipe/add_step", body).await.unwrap(); create_step_element( - &select::(&format!("#group-{} .steps", group_id)), + &selector::(&format!("#group-{} .steps", group_id)), &ron_api::Step { id: response.step_id, action: "".to_string(), @@ -312,14 +414,14 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element { let step_id = step.id; - let step_element: Element = select_and_clone("#hidden-templates .step"); + let step_element: Element = selector_and_clone("#hidden-templates .step"); step_element .set_attribute("id", &format!("step-{}", step.id)) .unwrap(); group_element.append_child(&step_element).unwrap(); // Step action. - let action: HtmlTextAreaElement = step_element.select(".text-area-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| { @@ -338,10 +440,10 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { // Delete button. let step_element_cloned = step_element.clone(); - let delete_button: HtmlInputElement = step_element.select(".input-step-delete"); + let delete_button: HtmlInputElement = step_element.selector(".input-step-delete"); EventListener::new(&delete_button, "click", move |_event| { let action = step_element_cloned - .select::(".text-area-step-action") + .selector::(".text-area-step-action") .value(); spawn_local(async move { if modal_dialog::show(&format!("Are you sure to delete the step '{}'", action)) @@ -356,14 +458,15 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Add ingredient button. - let add_ingredient_button: HtmlInputElement = step_element.select(".input-add-ingredient"); + 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::AddRecipeIngredient { step_id }; let response: ron_api::AddRecipeIngredientResult = request::post("recipe/add_ingredient", body).await.unwrap(); create_ingredient_element( - &select::(&format!("#step-{} .ingredients", step_id)), + &selector::(&format!("#step-{} .ingredients", step_id)), &ron_api::Ingredient { id: response.ingredient_id, name: "".to_string(), @@ -384,14 +487,14 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { ingredient: &ron_api::Ingredient, ) -> Element { let ingredient_id = ingredient.id; - let ingredient_element: Element = select_and_clone("#hidden-templates .ingredient"); + let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient"); ingredient_element .set_attribute("id", &format!("ingredient-{}", ingredient.id)) .unwrap(); step_element.append_child(&ingredient_element).unwrap(); // Ingredient name. - let name: HtmlInputElement = ingredient_element.select(".input-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| { @@ -409,7 +512,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Ingredient comment. - let comment: HtmlInputElement = ingredient_element.select(".input-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| { @@ -427,7 +530,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Ingredient quantity. - let quantity: HtmlInputElement = ingredient_element.select(".input-ingredient-quantity"); + let quantity: HtmlInputElement = ingredient_element.selector(".input-ingredient-quantity"); quantity.set_value( &ingredient .quantity_value @@ -454,7 +557,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Ingredient unit. - let unit: HtmlInputElement = ingredient_element.select(".input-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| { @@ -473,10 +576,11 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { // Delete button. let ingredient_element_cloned = ingredient_element.clone(); - let delete_button: HtmlInputElement = ingredient_element.select(".input-ingredient-delete"); + let delete_button: HtmlInputElement = + ingredient_element.selector(".input-ingredient-delete"); EventListener::new(&delete_button, "click", move |_event| { let name = ingredient_element_cloned - .select::(".input-ingredient-name") + .selector::(".input-ingredient-name") .value(); spawn_local(async move { if modal_dialog::show(&format!("Are you sure to delete the ingredient '{}'", name)) @@ -505,11 +609,12 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let group_element = create_group_element(&group); for step in group.steps { - let step_element = create_step_element(&group_element.select(".steps"), &step); + let step_element = + create_step_element(&group_element.selector(".steps"), &step); for ingredient in step.ingredients { create_ingredient_element( - &step_element.select(".ingredients"), + &step_element.selector(".ingredients"), &ingredient, ); } diff --git a/frontend/src/modal_dialog.rs b/frontend/src/modal_dialog.rs index 7d3cd02..a42a33a 100644 --- a/frontend/src/modal_dialog.rs +++ b/frontend/src/modal_dialog.rs @@ -1,16 +1,16 @@ use futures::{future::FutureExt, pin_mut, select}; use web_sys::{Element, HtmlDialogElement}; -use crate::utils::{by_id, SelectExt}; +use crate::utils::{by_id, SelectorExt}; use crate::on_click; pub async fn show(message: &str) -> bool { let dialog: HtmlDialogElement = by_id("modal-dialog"); - let input_ok: Element = dialog.select(".ok"); - let input_cancel: Element = dialog.select(".cancel"); + let input_ok: Element = dialog.selector(".ok"); + let input_cancel: Element = dialog.selector(".cancel"); - dialog.select::(".content").set_inner_html(message); + dialog.selector::(".content").set_inner_html(message); dialog.show_modal().unwrap(); diff --git a/frontend/src/utils.rs b/frontend/src/utils.rs index d49fff4..89d256b 100644 --- a/frontend/src/utils.rs +++ b/frontend/src/utils.rs @@ -2,14 +2,14 @@ use gloo::utils::document; use wasm_bindgen::prelude::*; use web_sys::Element; -pub trait SelectExt { - fn select(&self, selectors: &str) -> T +pub trait SelectorExt { + fn selector(&self, selectors: &str) -> T where T: JsCast; } -impl SelectExt for Element { - fn select(&self, selectors: &str) -> T +impl SelectorExt for Element { + fn selector(&self, selectors: &str) -> T where T: JsCast, { @@ -21,7 +21,7 @@ impl SelectExt for Element { } } -pub fn select(selectors: &str) -> T +pub fn selector(selectors: &str) -> T where T: JsCast, { @@ -33,7 +33,7 @@ where .unwrap() } -pub fn select_and_clone(selectors: &str) -> T +pub fn selector_and_clone(selectors: &str) -> T where T: JsCast, {