From: Greg Burri Date: Fri, 10 Jan 2025 21:38:34 +0000 (+0100) Subject: Groups can now be ordered (via drag and drop) X-Git-Url: https://git.euphorik.ch/?a=commitdiff_plain;h=975d1ceee262e728c44d3f779fded404a66dafad;p=recipes.git Groups can now be ordered (via drag and drop) --- diff --git a/Cargo.lock b/Cargo.lock index e2bbbc2..cd7e1d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,9 +290,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.24" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.24" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -718,7 +718,7 @@ dependencies = [ "gloo", "ron", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1934,7 +1934,7 @@ dependencies = [ "sqlx", "strum", "strum_macros", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tower", "tower-http", @@ -2121,9 +2121,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "log", "once_cell", @@ -2386,7 +2386,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-stream", "tracing", @@ -2470,7 +2470,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "whoami", ] @@ -2508,7 +2508,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "whoami", ] @@ -2600,9 +2600,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2651,11 +2651,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -2671,9 +2671,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2748,9 +2748,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -2766,9 +2766,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", diff --git a/backend/scss/style.scss b/backend/scss/style.scss index f2db295..bcfe72d 100644 --- a/backend/scss/style.scss +++ b/backend/scss/style.scss @@ -70,6 +70,8 @@ body { .drag-handle { width: 20px; height: 20px; + display: inline-block; + vertical-align: bottom; background-color: blue; } @@ -92,7 +94,6 @@ body { border: 0; } - .recipe-item { padding: 4px; } @@ -118,6 +119,13 @@ body { h1 { text-align: center; } + } + + #recipe-edit { + + .drag-handle { + cursor: move; + } .group { border: 0.1em solid lighten($color-3, 30%); @@ -131,6 +139,21 @@ body { border: 0.1em solid lighten($color-3, 30%); } + .dropzone-group, + .dropzone-step { + height: 10px; + background-color: white; + + &.active { + background-color: blue; + } + + &.hover { + background-color: red; + } + } + + #hidden-templates { display: none; } diff --git a/backend/sql/data_test.sql b/backend/sql/data_test.sql index 5f37367..7e4a46b 100644 --- a/backend/sql/data_test.sql +++ b/backend/sql/data_test.sql @@ -20,17 +20,17 @@ VALUES ( NULL ); -INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) -VALUES (1, 1, 'Croissant au jambon', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) +VALUES (1, 1, 'Croissant au jambon', true, '2025-01-07T10:41:05.697884837+00:00'); -INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) -VALUES (2, 1, 'Gratin de thon aux olives', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) +VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00'); -INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) -VALUES (3, 1, 'Saumon en croute', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) +VALUES (3, 1, 'Saumon en croute', true, '2025-01-07T10:41:05.697884837+00:00'); -INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) -VALUES (4, 2, 'Ouiche lorraine', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) +VALUES (4, 2, 'Ouiche lorraine', true, '2025-01-07T10:41:05.697884837+00:00'); -- Groups, steps and ingredients for 'Gratin de thon'. diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index 306f195..aa9c14d 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -28,6 +28,7 @@ CREATE TABLE [User] ( [is_admin] INTEGER NOT NULL DEFAULT FALSE ) STRICT; +CREATE INDEX [validation_token_index] ON [User]([validation_token]); CREATE UNIQUE INDEX [User_email_index] ON [User]([email]); CREATE TABLE [UserLoginToken] ( @@ -58,6 +59,7 @@ CREATE TABLE [Recipe] ( [difficulty] INTEGER NOT NULL DEFAULT 0, [servings] INTEGER DEFAULT 4, [is_published] INTEGER NOT NULL DEFAULT FALSE, + [creation_datetime] TEXT NOT NULL, FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL ) STRICT; diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index 1dc5c8f..364c83a 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -1,3 +1,6 @@ +use chrono::prelude::*; +use itertools::Itertools; + use super::{Connection, DBError, Result}; use crate::data::model; @@ -17,7 +20,7 @@ impl Connection { SELECT [id], [title] FROM [Recipe] WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2) - ORDER BY [title] + ORDER BY [title] COLLATE NOCASE "#, ) .bind(lang) @@ -28,7 +31,7 @@ impl Connection { SELECT [id], [title] FROM [Recipe] WHERE [is_published] = true AND [lang] = $1 - ORDER BY [title] + ORDER BY [title] COLLATE NOCASE "#, ) .bind(lang) @@ -83,6 +86,31 @@ WHERE [Group].[id] = $1 AND [user_id] = $2 .map_err(DBError::from) } + pub async fn can_edit_recipe_all_groups( + &self, + user_id: i64, + group_ids: &[i64], + ) -> Result { + let params = (0..group_ids.len()) + .map(|n| format!("${}", n + 2)) + .join(", "); + let query_str = format!( + r#" +SELECT COUNT(*) +FROM [Recipe] +INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] +WHERE [Group].[id] IN ({}) AND [user_id] = $1 + "#, + params + ); + + let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id); + for id in group_ids { + query = query.bind(id); + } + Ok(query.fetch_one(&self.pool).await? == group_ids.len() as u64) + } + pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result { sqlx::query_scalar( r#" @@ -173,10 +201,11 @@ WHERE [Recipe].[user_id] = $1 .await?; let db_result = sqlx::query( - "INSERT INTO [Recipe] ([user_id], [lang], [title]) VALUES ($1, $2, '')", + "INSERT INTO [Recipe] ([user_id], [lang], [title], [creation_datetime]) VALUES ($1, $2, '', $3)", ) .bind(user_id) .bind(lang) + .bind(Utc::now()) .execute(&mut *tx) .await?; @@ -482,6 +511,22 @@ ORDER BY [name] .map_err(DBError::from) } + pub async fn set_groups_order(&self, group_ids: &[i64]) -> Result<()> { + let mut tx = self.tx().await?; + + for (order, id) in group_ids.iter().enumerate() { + sqlx::query("UPDATE [Group] SET [order] = $2 WHERE [id] = $1") + .bind(id) + .bind(order as i64) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + + Ok(()) + } + pub async fn add_recipe_step(&self, group_id: i64) -> Result { let db_result = sqlx::query("INSERT INTO [Step] ([group_id]) VALUES ($1)") .bind(group_id) diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index 446bdf5..e23fd38 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -410,6 +410,28 @@ WHERE [email] = $1 Ok(GetTokenResetPasswordResult::Ok(token)) } + pub async fn is_reset_password_token_valid( + &self, + token: &str, + validation_time: Duration, + ) -> Result { + if let Some(Some(db_datetime)) = sqlx::query_scalar::<_, Option>>( + r#" +SELECT [password_reset_datetime] +FROM [User] +WHERE [password_reset_token] = $1 + "#, + ) + .bind(token) + .fetch_optional(&self.pool) + .await? + { + Ok(Utc::now() - db_datetime <= validation_time) + } else { + Ok(false) + } + } + pub async fn reset_password( &self, new_password: &str, diff --git a/backend/src/main.rs b/backend/src/main.rs index 90b169e..98297fd 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -118,6 +118,10 @@ async fn main() { "/recipe/set_group_comment", put(services::ron::set_group_comment), ) + .route( + "/recipe/set_groups_order", + put(services::ron::set_group_orders), + ) .route("/recipe/add_step", post(services::ron::add_step)) .route("/recipe/remove_step", delete(services::ron::rm_step)) .route( diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index f93c3b1..0bbeb63 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -104,6 +104,25 @@ async fn check_user_rights_recipe_group( } } +async fn check_user_rights_recipe_groups( + connection: &db::Connection, + user: &Option, + group_ids: &[i64], +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + async fn check_user_rights_recipe_step( connection: &db::Connection, user: &Option, @@ -396,6 +415,17 @@ pub async fn set_group_comment( Ok(StatusCode::OK) } +#[debug_handler] +pub async fn set_group_orders( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_groups(&connection, &user, &ron.group_ids).await?; + connection.set_groups_order(&ron.group_ids).await?; + Ok(StatusCode::OK) +} + #[debug_handler] pub async fn add_step( State(connection): State, diff --git a/backend/src/services/user.rs b/backend/src/services/user.rs index e54672e..59dfdc0 100644 --- a/backend/src/services/user.rs +++ b/backend/src/services/user.rs @@ -463,19 +463,34 @@ pub async fn ask_reset_password_post( #[debug_handler] pub async fn reset_password_get( + State(connection): State, Extension(user): Extension>, Extension(tr): Extension, Query(query): Query>, ) -> Result { if let Some(reset_token) = query.get("reset_token") { - Ok(ResetPasswordTemplate { - user, - tr, - reset_token, - message: "", - message_password: "", + // Check if the token is valid. + if connection + .is_reset_password_token_valid( + reset_token, + Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), + ) + .await? + { + Ok(ResetPasswordTemplate { + user, + tr, + reset_token, + message: "", + message_password: "", + } + .into_response()) + } else { + Ok( + MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user) + .into_response(), + ) } - .into_response()) } else { Ok( MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user) diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index 4b89f93..fd928f3 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -80,13 +80,13 @@
- +
-
+ @@ -96,13 +96,15 @@ -
+
+
+
-
+ diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 3f233fa..105a456 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -108,6 +108,11 @@ pub struct SetGroupComment { pub comment: String, } +#[derive(Serialize, Deserialize, Clone)] +pub struct SetGroupOrders { + pub group_ids: Vec, +} + #[derive(Serialize, Deserialize, Clone)] pub struct AddRecipeStep { pub group_id: i64, diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 452c3c8..8ce594e 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -24,18 +24,22 @@ wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ "console", "Document", - "Element", - "HtmlElement", "Node", + "NodeList", "Window", "Location", "EventTarget", + "DragEvent", + "DataTransfer", + "KeyboardEvent", + "Element", + "HtmlElement", + "HtmlDivElement", "HtmlLabelElement", "HtmlInputElement", "HtmlTextAreaElement", "HtmlSelectElement", "HtmlDialogElement", - "KeyboardEvent", ] } gloo = "0.11" diff --git a/frontend/src/handles.rs b/frontend/src/handles.rs index d05fa41..0e11df8 100644 --- a/frontend/src/handles.rs +++ b/frontend/src/handles.rs @@ -1,18 +1,22 @@ use gloo::{ - events::EventListener, + console::log, + events::{EventListener, EventListenerOptions}, net::http::Request, utils::{document, window}, }; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{Element, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, KeyboardEvent}; +use web_sys::{ + DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, + KeyboardEvent, +}; use common::ron_api; use crate::{ modal_dialog, request, toast::{self, Level}, - utils::{by_id, selector, selector_and_clone, SelectorExt}, + utils::{by_id, selector, selector_all, selector_and_clone, SelectorExt}, }; async fn reload_recipes_list(current_recipe_id: i64) { @@ -276,6 +280,92 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { }) .forget(); + let group_dropzone: Element = selector(".dropzone-group"); + setup_dragzone_events(&group_dropzone); + + fn setup_dragzone_events(dropzone: &Element) { + EventListener::new_with_options( + dropzone, + "dragover", + EventListenerOptions::enable_prevent_default(), + |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + let drag_data = event + .data_transfer() + .unwrap() + .get_data("text/plain") + .unwrap(); + + if drag_data.starts_with("group") { + event.prevent_default(); + // event.data_transfer().unwrap().set_effect_allowed("move"); + log!("drag over"); + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .set_class_name("dropzone-group hover"); + } + }, + ) + .forget(); + + EventListener::new(dropzone, "dragleave", |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + let drag_data = event + .data_transfer() + .unwrap() + .get_data("text/plain") + .unwrap(); + + if drag_data.starts_with("group") { + log!("drag leave"); + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .set_class_name("dropzone-group active"); + } + }) + .forget(); + + EventListener::new(dropzone, "drop", |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + let drag_data = event + .data_transfer() + .unwrap() + .get_data("text/plain") + .unwrap(); + + if drag_data.starts_with("group") { + let id: i64 = drag_data[6..].parse().unwrap(); + let target: Element = event.target().unwrap().dyn_into().unwrap(); + let group: Element = by_id(&format!("group-{}", id)); + let group_dropzone: Element = by_id(&format!("dropzone-group-{}", id)); + target.after_with_node_1(&group).unwrap(); + group.after_with_node_1(&group_dropzone).unwrap(); + + send_groups_order(); + } + }) + .forget(); + } + + fn send_groups_order() { + spawn_local(async move { + let group_ids = by_id::("groups-container") + .selector_all::(".group") + .into_iter() + .map(|e| e.get_attribute("id").unwrap()[6..].parse::().unwrap()) + .collect(); + + let body = ron_api::SetGroupOrders { group_ids }; + let _ = request::put::<(), _>("recipe/set_groups_order", body).await; + }); + } + fn create_tag_elements(recipe_id: i64, tags: &[T]) where T: AsRef, @@ -339,6 +429,77 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let groups_container: Element = by_id("groups-container"); groups_container.append_child(&group_element).unwrap(); + let dropzone_group: Element = selector_and_clone(".dropzone-group"); + dropzone_group + .set_attribute("id", &format!("dropzone-group-{}", group.id)) + .unwrap(); + groups_container.append_child(&dropzone_group).unwrap(); + setup_dragzone_events(&dropzone_group); + + let drag_handle: Element = group_element.selector(".drag-handle"); + EventListener::new(&drag_handle, "mousedown", |event| { + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .parent_element() + .unwrap() + .set_attribute("draggable", "true") + .unwrap(); + }) + .forget(); + + EventListener::new(&drag_handle, "mouseup", |event| { + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .parent_element() + .unwrap() + .set_attribute("draggable", "false") + .unwrap(); + }) + .forget(); + + EventListener::new(&group_element, "dragstart", |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + let target_element: Element = event.target().unwrap().dyn_into().unwrap(); + if target_element.get_attribute("class").unwrap() == "group" { + // Highlight where the group can be droppped. + for dp in selector_all::(".dropzone-group") { + dp.set_class_name("dropzone-group active"); + } + event + .data_transfer() + .unwrap() + .set_data("text/plain", &target_element.get_attribute("id").unwrap()) + .unwrap(); + event.data_transfer().unwrap().set_effect_allowed("move"); + } + }) + .forget(); + + EventListener::new(&group_element, "dragend", |event| { + // let event: &DragEvent = event.dyn_ref::().unwrap(); + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .set_attribute("draggable", "false") + .unwrap(); + + let target_element: Element = event.target().unwrap().dyn_into().unwrap(); + if target_element.get_attribute("class").unwrap() == "group" { + for dp in selector_all::(".dropzone-group") { + dp.set_class_name("dropzone-group"); + } + } + }) + .forget(); + // Group name. let name = group_element.selector::(".input-group-name"); name.set_value(&group.name); @@ -388,6 +549,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let body = ron_api::RemoveRecipeGroup { group_id }; let _ = request::delete::<(), _>("recipe/remove_group", body).await; by_id::(&format!("group-{}", group_id)).remove(); + by_id::(&format!("dropzone-group-{}", group_id)).remove(); } }); }) @@ -423,6 +585,69 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .unwrap(); group_element.append_child(&step_element).unwrap(); + let dropzone_step: Element = selector_and_clone(".dropzone-step"); + dropzone_step + .set_attribute("id", &format!("dropzone-step-{}", step.id)) + .unwrap(); + group_element.append_child(&dropzone_step).unwrap(); + + let drag_handle: Element = step_element.selector(".drag-handle"); + + EventListener::new(&drag_handle, "mousedown", |event| { + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .parent_element() + .unwrap() + .set_attribute("draggable", "true") + .unwrap(); + }) + .forget(); + + EventListener::new(&drag_handle, "mouseup", |event| { + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .parent_element() + .unwrap() + .set_attribute("draggable", "false") + .unwrap(); + }) + .forget(); + + EventListener::new(&step_element, "dragstart", |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + // let target_element: Element = event.target().unwrap().dyn_into().unwrap(); + // if target_element.get_attribute("class").unwrap() == "step" { + // Highlight where the step can be droppped. + log!("START DRAG STEP"); + // log!(event); + // } + }) + .forget(); + EventListener::new(&step_element, "dragend", |event| { + let event: &DragEvent = event.dyn_ref::().unwrap(); + // let target_element: Element = event.target().unwrap().dyn_into().unwrap(); + // if target_element.get_attribute("class").unwrap() == "step" { + // Highlight where the step can be droppped. + event + .target() + .unwrap() + .dyn_into::() + .unwrap() + .set_attribute("draggable", "false") + .unwrap(); + + log!("STOP DRAG STEP"); + // log!(event); + // } + }) + .forget(); + // Step action. let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action"); action.set_value(&step.action); @@ -455,6 +680,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let body = ron_api::RemoveRecipeStep { step_id }; let _ = request::delete::<(), _>("recipe/remove_step", body).await; by_id::(&format!("step-{}", step_id)).remove(); + by_id::(&format!("dropzone-step-{}", step_id)).remove(); } }); }) diff --git a/frontend/src/utils.rs b/frontend/src/utils.rs index 89d256b..58e7803 100644 --- a/frontend/src/utils.rs +++ b/frontend/src/utils.rs @@ -1,4 +1,4 @@ -use gloo::utils::document; +use gloo::{console::log, utils::document}; use wasm_bindgen::prelude::*; use web_sys::Element; @@ -6,6 +6,10 @@ pub trait SelectorExt { fn selector(&self, selectors: &str) -> T where T: JsCast; + + fn selector_all(&self, selectors: &str) -> Vec + where + T: JsCast; } impl SelectorExt for Element { @@ -19,6 +23,18 @@ impl SelectorExt for Element { .dyn_into::() .unwrap() } + + fn selector_all(&self, selectors: &str) -> Vec + where + T: JsCast, + { + self.query_selector_all(selectors) + .unwrap() + .values() + .into_iter() + .map(|e| e.unwrap().dyn_into::().unwrap()) + .collect() + } } pub fn selector(selectors: &str) -> T @@ -33,6 +49,19 @@ where .unwrap() } +pub fn selector_all(selectors: &str) -> Vec +where + T: JsCast, +{ + document() + .query_selector_all(selectors) + .unwrap() + .values() + .into_iter() + .map(|e| e.unwrap().dyn_into::().unwrap()) + .collect() +} + pub fn selector_and_clone(selectors: &str) -> T where T: JsCast,