Tags can now be added and removed
authorGreg Burri <greg.burri@gmail.com>
Sat, 4 Jan 2025 21:43:20 +0000 (22:43 +0100)
committerGreg Burri <greg.burri@gmail.com>
Sat, 4 Jan 2025 21:43:20 +0000 (22:43 +0100)
backend/templates/recipe_edit.html
frontend/Cargo.toml
frontend/src/handles.rs
frontend/src/modal_dialog.rs
frontend/src/utils.rs

index 923d6d8..529457f 100644 (file)
@@ -65,9 +65,9 @@
             * Remove the tag to the html list (DOM)
     * 'enter' key to add the current tag
     -->
-    <div id="widget-tags">
+    <div id="container-tags">
         <label for="input-tags" >Tags</label>
-        <div class="tags"></div>
+        <span class="tags"></span>
         <input
             id="input-tags"
             type="text"
index 5495232..452c3c8 100644 (file)
@@ -35,6 +35,7 @@ web-sys = { version = "0.3", features = [
     "HtmlTextAreaElement",
     "HtmlSelectElement",
     "HtmlDialogElement",
+    "KeyboardEvent",
 ] }
 
 gloo = "0.11"
index a65e948..f190eee 100644 (file)
@@ -8,6 +8,7 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::spawn_local;
 use web_sys::{
     Element, Event, HtmlDialogElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
+    KeyboardEvent,
 };
 
 use common::ron_api::{self, Ingredient};
@@ -15,7 +16,7 @@ use common::ron_api::{self, Ingredient};
 use crate::{
     modal_dialog, request,
     toast::{self, Level},
-    utils::{by_id, select, select_and_clone, SelectExt},
+    utils::{by_id, selector, selector_and_clone, SelectorExt},
 };
 
 async fn reload_recipes_list(current_recipe_id: i64) {
@@ -164,7 +165,55 @@ pub fn recipe_edit(recipe_id: i64) -> 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<String> = 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::<HtmlInputElement>("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::<KeyboardEvent>() {
+                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<T>(recipe_id: i64, tags: &[T])
+    where
+        T: AsRef<str>,
+    {
+        let tags_span: Element = selector("#container-tags .tags");
+
+        // Collect current tags to avoid re-adding an existing tag.
+        let mut current_tags: Vec<String> = vec![];
+        let mut current_tag_element = tags_span.first_child();
+        while let Some(element) = current_tag_element {
+            current_tags.push(
+                element
+                    .dyn_ref::<Element>()
+                    .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::<HtmlInputElement>(".input-group-name");
+        let name = group_element.selector::<HtmlInputElement>(".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::<HtmlInputElement>(".input-group-name")
+                .selector::<HtmlInputElement>(".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::<Element>(&format!("#group-{} .steps", group_id)),
+                    &selector::<Element>(&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::<HtmlTextAreaElement>(".text-area-step-action")
+                .selector::<HtmlTextAreaElement>(".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::<Element>(&format!("#step-{} .ingredients", step_id)),
+                    &selector::<Element>(&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::<HtmlInputElement>(".input-ingredient-name")
+                .selector::<HtmlInputElement>(".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,
                         );
                     }
index 7d3cd02..a42a33a 100644 (file)
@@ -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::<Element>(".content").set_inner_html(message);
+    dialog.selector::<Element>(".content").set_inner_html(message);
 
     dialog.show_modal().unwrap();
 
index d49fff4..89d256b 100644 (file)
@@ -2,14 +2,14 @@ use gloo::utils::document;
 use wasm_bindgen::prelude::*;
 use web_sys::Element;
 
-pub trait SelectExt {
-    fn select<T>(&self, selectors: &str) -> T
+pub trait SelectorExt {
+    fn selector<T>(&self, selectors: &str) -> T
     where
         T: JsCast;
 }
 
-impl SelectExt for Element {
-    fn select<T>(&self, selectors: &str) -> T
+impl SelectorExt for Element {
+    fn selector<T>(&self, selectors: &str) -> T
     where
         T: JsCast,
     {
@@ -21,7 +21,7 @@ impl SelectExt for Element {
     }
 }
 
-pub fn select<T>(selectors: &str) -> T
+pub fn selector<T>(selectors: &str) -> T
 where
     T: JsCast,
 {
@@ -33,7 +33,7 @@ where
         .unwrap()
 }
 
-pub fn select_and_clone<T>(selectors: &str) -> T
+pub fn selector_and_clone<T>(selectors: &str) -> T
 where
     T: JsCast,
 {