Tags can now be added and removed
This commit is contained in:
parent
f8333651fd
commit
9b0fcec5e2
5 changed files with 142 additions and 36 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ web-sys = { version = "0.3", features = [
|
|||
"HtmlTextAreaElement",
|
||||
"HtmlSelectElement",
|
||||
"HtmlDialogElement",
|
||||
"KeyboardEvent",
|
||||
] }
|
||||
|
||||
gloo = "0.11"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue