From 8cd277d66abb1a111e9dd05104748f74aca33f1a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 27 Dec 2024 03:01:03 +0300 Subject: [PATCH 1/6] feat(item_list): Added active cell tracking with position updates for popup rendering. --- Cargo.lock | 1 + Cargo.toml | 3 +- assets/style.css | 24 +++++++ src/components/items_list.rs | 126 ++++++++++++++++++++++++----------- 4 files changed, 115 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ef3255..0a0052c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "futures", "gloo-net 0.5.0", "http 1.2.0", + "js-sys", "leptos", "leptos_actix", "leptos_meta", diff --git a/Cargo.toml b/Cargo.toml index f58b996..c9a4cd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ leptos_router = { version = "0.6" } wasm-bindgen = "=0.2.99" serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.0", features = ["v4"] } -web-sys = { version = "0.3", features = ["Event"] } +web-sys = { version = "0.3", features = ["Event", "HtmlElement", "Window", "EventTarget", "Element", "DomRect"] } +js-sys = "0.3" nostr-sdk = "0.37" tokio = "1" gloo-net = "0.5" diff --git a/assets/style.css b/assets/style.css index 49b83d8..5b52e70 100644 --- a/assets/style.css +++ b/assets/style.css @@ -68,4 +68,28 @@ th, td { th { background-color: #f2f2f2; +} + +.suggestions-popup { + background-color: white; + border: 1px solid #ccc; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + z-index: 1000; +} + +.suggestions-popup ul { + list-style-type: none; + padding: 0; + margin: 0; +} + +.suggestions-popup li { + padding: 8px 12px; + cursor: pointer; +} + +.suggestions-popup li:hover { + background-color: #f0f0f0; } \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index c279b3b..d3fc4eb 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -6,6 +6,8 @@ use uuid::Uuid; use leptos::logging::log; use crate::models::item::Item; use std::sync::{Arc, Mutex}; +use wasm_bindgen::JsCast; +use web_sys::{FocusEvent, HtmlElement, Element, Node}; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -30,6 +32,8 @@ pub fn ItemsList( wikidata_id: None, }]); + let (active_cell_position, set_active_cell_position) = create_signal(None::<(f64, f64)>); + let (active_row_index, set_active_row_index) = create_signal(None::); let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(Vec::::new()); @@ -65,13 +69,19 @@ pub fn ItemsList( "name" => { item.name = value.clone(); fetch_wikidata_suggestions(value.clone()); + set_active_row_index.set(Some(index)); + // Set active cell position + if let Some(element) = document().get_element_by_id(&format!("name-{}", index)) { + let rect = element.get_bounding_client_rect(); + set_active_cell_position.set(Some((rect.left(), rect.bottom()))); + } } "description" => { item.description = value.clone(); } _ => (), } - } + } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { @@ -112,6 +122,54 @@ pub fn ItemsList( }); }; + // Position and render the popup + let render_popup = move || { + view! { +
+
    + {move || wikidata_suggestions.get().iter().map(|suggestion| { + let label_for_click = suggestion.label.clone(); + let description_for_click = suggestion.description.clone().unwrap_or_default(); + let id = suggestion.id.clone(); + let label_for_display = label_for_click.clone(); + let description_for_display = description_for_click.clone(); + + view! { +
  • + {format!("{} - {}", label_for_display, description_for_display)} +
  • + } + }).collect::>()} +
+
+ } + }; + view! {

{ "Items List" }

@@ -130,44 +188,35 @@ pub fn ItemsList( // Editable Name Field with Wikidata Integration - -
    - {move || { - let suggestions = wikidata_suggestions.get().to_vec(); - suggestions.into_iter().map(|suggestion| { - let label_for_click = suggestion.label.clone(); - let label_for_display = suggestion.label.clone(); - let description_for_click = suggestion.description.clone().unwrap_or_default(); - let description_for_display = suggestion.description.clone().unwrap_or_default(); - let id = suggestion.id.clone(); - - // Tags for the item - let tags = vec![ - ("source".to_string(), "wikidata".to_string()), - ("wikidata_id".to_string(), id.clone()), - ]; - - view! { -
  • - { format!("{} - {}", label_for_display, description_for_display) } -
  • +
    () { + if let Some(element_ref) = node.dyn_ref::(){ + if let Some(element) = element_ref.dyn_ref::() { + let rect = element.get_bounding_client_rect(); + set_active_cell_position.set(Some((rect.left(), rect.top() + rect.height()))); + set_active_row_index.set(Some(index)); + } else { + log!("Failed to cast to Element"); + } + } else { + log!("Target is not a valid HTML element"); + } + } else { + log!("Target is not a valid Node"); } - }).collect::>() - }} -
+ } + } + on:blur=move |_| set_active_row_index.set(None) + > + +
// Editable Description Field @@ -194,6 +243,7 @@ pub fn ItemsList( }).collect::>()} + {render_popup()} } } From c4a45d91855d0098d3a0ee9219fb1e6d6dc437aa Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 30 Dec 2024 14:18:33 +0300 Subject: [PATCH 2/6] Fix (ItemsList)(inprogress): Improve Popup Positioning and Debounced Fetching - Introduced `ActiveCell` struct to manage active cell state and position more robustly. - Added debounce mechanism using `futures_timer::Delay` for optimized Wikidata suggestion fetching. - Enhanced popup rendering logic to include z-index and improved styling for better positioning. - Implemented validation for bounding box dimensions during active cell positioning. - Adjusted event handling for focus and blur with asynchronous state updates. - General refactoring for better readability and maintainability. (Note: Popup functionality remains incomplete and will be addressed in future iterations.) --- src/components/items_list.rs | 70 ++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index d3fc4eb..8c4ee0a 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -8,6 +8,8 @@ use crate::models::item::Item; use std::sync::{Arc, Mutex}; use wasm_bindgen::JsCast; use web_sys::{FocusEvent, HtmlElement, Element, Node}; +use futures_timer::Delay; +use std::time::Duration; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -15,6 +17,11 @@ struct WikidataSuggestion { label: String, description: Option, } +#[derive(Clone)] +struct ActiveCell { + row_index: usize, + position: (f64, f64), +} #[component] pub fn ItemsList( @@ -32,14 +39,16 @@ pub fn ItemsList( wikidata_id: None, }]); + let (active_cell, set_active_cell) = create_signal(None::); let (active_cell_position, set_active_cell_position) = create_signal(None::<(f64, f64)>); let (active_row_index, set_active_row_index) = create_signal(None::); - let (wikidata_suggestions, set_wikidata_suggestions) = - create_signal(Vec::::new()); + let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(Vec::::new()); + let debounce_duration = Duration::from_millis(300); // Fetch Wikidata suggestions let fetch_wikidata_suggestions = move |query: String| { spawn_local(async move { + Delay::new(debounce_duration).await; if query.is_empty() { set_wikidata_suggestions.set(Vec::new()); return; @@ -56,7 +65,10 @@ pub fn ItemsList( set_wikidata_suggestions.set(data.search); } } - Err(_) => log!("Failed to fetch Wikidata suggestions"), + Err(err) => { + log!("Failed to fetch Wikidata suggestions: {:?}", err); + set_wikidata_suggestions.set(Vec::new()); + } } }); }; @@ -70,10 +82,15 @@ pub fn ItemsList( item.name = value.clone(); fetch_wikidata_suggestions(value.clone()); set_active_row_index.set(Some(index)); - // Set active cell position + // Set active cell position with validation if let Some(element) = document().get_element_by_id(&format!("name-{}", index)) { let rect = element.get_bounding_client_rect(); - set_active_cell_position.set(Some((rect.left(), rect.bottom()))); + log!("Bounding rect: {:?}", rect); // Log rect details + if rect.width() > 0.0 && rect.height() > 0.0 { + set_active_cell_position.set(Some((rect.left(), rect.bottom()))); + } else { + log!("Element bounding box is not valid for popup positioning."); + } } } "description" => { @@ -129,10 +146,15 @@ pub fn ItemsList( class="suggestions-popup" style=move || { let suggestions = wikidata_suggestions.get(); - let position = active_cell_position.get(); - if !suggestions.is_empty() && position.is_some() { - let (x, y) = position.unwrap(); - format!("position: absolute; left: {}px; top: {}px; display: block;", x, y) + if !suggestions.is_empty() { + if let Some((x, y)) = active_cell_position.get() { + format!( + "position: absolute; left: {}px; top: {}px; display: block; z-index: 1000;", + x, y + ) + } else { + "display: none;".to_string() + } } else { "display: none;".to_string() } @@ -190,26 +212,20 @@ pub fn ItemsList(
() { - if let Some(element_ref) = node.dyn_ref::(){ - if let Some(element) = element_ref.dyn_ref::() { - let rect = element.get_bounding_client_rect(); - set_active_cell_position.set(Some((rect.left(), rect.top() + rect.height()))); - set_active_row_index.set(Some(index)); - } else { - log!("Failed to cast to Element"); - } - } else { - log!("Target is not a valid HTML element"); - } - } else { - log!("Target is not a valid Node"); - } + if let Some(element) = event.target().and_then(|t| t.dyn_into::().ok()) { + let rect = element.get_bounding_client_rect(); + set_active_cell.set(Some(ActiveCell { + row_index: index, + position: (rect.left(), rect.top() + rect.height()), + })); } } - on:blur=move |_| set_active_row_index.set(None) + on:blur=move |_| { + spawn_local(async move { + Delay::new(Duration::from_millis(100)).await; + set_active_cell.set(None); + }); + } > Date: Mon, 30 Dec 2024 14:20:50 +0300 Subject: [PATCH 3/6] feat(EditableCell): add signal disposal handling and logging - Introduced `is_disposed` signal to track component disposal state. - Added `on_cleanup` hook to set disposal flag and log when the component is disposed. - Enhanced signal access with `log_signal_get` to prevent usage after disposal. - Improved input, focus, and blur handlers to respect disposal state. - Ensured robust handling of signals during the component's lifecycle. This improves resilience and debugging for EditableCell, especially in dynamic UI contexts. --- src/components/editable_cell.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/editable_cell.rs b/src/components/editable_cell.rs index 2604931..b282038 100644 --- a/src/components/editable_cell.rs +++ b/src/components/editable_cell.rs @@ -1,5 +1,5 @@ use leptos::*; - +use leptos::logging::log; #[component] pub fn EditableCell( value: String, @@ -9,17 +9,41 @@ pub fn EditableCell( let (input_value, set_input_value) = create_signal(value.clone()); let (has_focus, set_has_focus) = create_signal(false); // Track focus state locally + let (is_disposed, set_disposed) = create_signal(false); // Track disposal state + + // Ensure signals aren't updated after disposal + on_cleanup(move || { + log!("Component disposed"); + set_disposed.set(true); + }); + + let log_signal_get = move |signal_name: &str| { + if is_disposed.get() { + panic!("Attempted to get disposed signal: {}", signal_name); + } + }; + let handle_input = move |e: web_sys::Event| { + log_signal_get("input_value"); + if is_disposed.get_untracked() { + return; + } let new_value = event_target_value(&e); set_input_value.set(new_value.clone()); on_input(new_value); }; let handle_focus = move |_: web_sys::FocusEvent| { + if is_disposed.get() { + return; + } set_has_focus.set(true); }; let handle_blur = move |_: web_sys::FocusEvent| { + if is_disposed.get() { + return; + } set_has_focus.set(false); }; From b616fbb438a015f5912a1107d9324dae9a728b3f Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 30 Dec 2024 14:23:38 +0300 Subject: [PATCH 4/6] build(deps): add futures-timer v3.0.0 to Cargo.toml --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + 2 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0a0052c..dd603c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,6 +722,7 @@ dependencies = [ "actix-web", "console_error_panic_hook", "futures", + "futures-timer", "gloo-net 0.5.0", "http 1.2.0", "js-sys", @@ -1083,6 +1084,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" diff --git a/Cargo.toml b/Cargo.toml index c9a4cd8..02e7e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ wasm-bindgen-futures = "0.4" serde_json="1.0.133" thiserror = "2.0.9" zerofrom = "0.1" +futures-timer = "3.0.0" [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] From b84cd37c4405f795e98b330ab76899fedccbca82 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 30 Dec 2024 16:10:13 +0300 Subject: [PATCH 5/6] refactor(formlist): streamline ItemsList component and clean up unused code - Removed unused imports, including `Element`, `Node`, and `ActiveCell` struct. - Consolidated signal management for `active_cell_position` and `active_row_index`. - Improved focus handling by adding a reusable `handle_focus` function. - Optimized EditableCell's event handlers for better maintainability. --- src/components/editable_cell.rs | 7 +++-- src/components/items_list.rs | 53 +++++++++++---------------------- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/components/editable_cell.rs b/src/components/editable_cell.rs index b282038..11ac43a 100644 --- a/src/components/editable_cell.rs +++ b/src/components/editable_cell.rs @@ -1,9 +1,11 @@ use leptos::*; use leptos::logging::log; +use web_sys::FocusEvent; #[component] pub fn EditableCell( value: String, on_input: impl Fn(String) + 'static, + #[prop(into)] on_focus: Callback, #[prop(optional)] key: Option, // Optional `key` prop ) -> impl IntoView { let (input_value, set_input_value) = create_signal(value.clone()); @@ -33,14 +35,15 @@ pub fn EditableCell( on_input(new_value); }; - let handle_focus = move |_: web_sys::FocusEvent| { + let handle_focus = move |ev:FocusEvent| { if is_disposed.get() { return; } set_has_focus.set(true); + on_focus.call(ev); }; - let handle_blur = move |_: web_sys::FocusEvent| { + let handle_blur = move |_:FocusEvent| { if is_disposed.get() { return; } diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 8c4ee0a..b80da9e 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -7,7 +7,7 @@ use leptos::logging::log; use crate::models::item::Item; use std::sync::{Arc, Mutex}; use wasm_bindgen::JsCast; -use web_sys::{FocusEvent, HtmlElement, Element, Node}; +use web_sys::{FocusEvent, HtmlElement}; use futures_timer::Delay; use std::time::Duration; @@ -17,11 +17,6 @@ struct WikidataSuggestion { label: String, description: Option, } -#[derive(Clone)] -struct ActiveCell { - row_index: usize, - position: (f64, f64), -} #[component] pub fn ItemsList( @@ -39,7 +34,6 @@ pub fn ItemsList( wikidata_id: None, }]); - let (active_cell, set_active_cell) = create_signal(None::); let (active_cell_position, set_active_cell_position) = create_signal(None::<(f64, f64)>); let (active_row_index, set_active_row_index) = create_signal(None::); let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(Vec::::new()); @@ -82,16 +76,6 @@ pub fn ItemsList( item.name = value.clone(); fetch_wikidata_suggestions(value.clone()); set_active_row_index.set(Some(index)); - // Set active cell position with validation - if let Some(element) = document().get_element_by_id(&format!("name-{}", index)) { - let rect = element.get_bounding_client_rect(); - log!("Bounding rect: {:?}", rect); // Log rect details - if rect.width() > 0.0 && rect.height() > 0.0 { - set_active_cell_position.set(Some((rect.left(), rect.bottom()))); - } else { - log!("Element bounding box is not valid for popup positioning."); - } - } } "description" => { item.description = value.clone(); @@ -114,6 +98,21 @@ pub fn ItemsList( }); }; + // Handle focus event for EditableCell + let handle_focus = move |index: usize, field: &str, event: FocusEvent| { + set_active_row_index.set(Some(index)); + if field == "name" { + if let Some(target) = event.target() { + if let Some(element) = target.dyn_ref::() { + let rect = element.get_bounding_client_rect(); + set_active_cell_position.set(Some((rect.left(), rect.bottom()))); + } + } + } else { + set_active_cell_position.set(None); + } + }; + // Add a new tag to an item let add_tag = move |index: usize, key: String, value: String| { set_items.update(|items| { @@ -210,35 +209,19 @@ pub fn ItemsList( // Editable Name Field with Wikidata Integration -
().ok()) { - let rect = element.get_bounding_client_rect(); - set_active_cell.set(Some(ActiveCell { - row_index: index, - position: (rect.left(), rect.top() + rect.height()), - })); - } - } - on:blur=move |_| { - spawn_local(async move { - Delay::new(Duration::from_millis(100)).await; - set_active_cell.set(None); - }); - } - > -
// Editable Description Field From a02fcabe190fdf74ab452312024e57c98d1a3719 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 31 Dec 2024 00:39:31 +0300 Subject: [PATCH 6/6] fix(editable cells): enhance EditableCell component with editing toggle and Rc for on_input - Replaced `on_input` function with `Rc` to enable cloning and safe usage in closures. - Introduced `is_editing` signal to manage toggling between display and input modes. - Added a persistent default key reference to avoid unnecessary updates. - Improved focus and blur handling for better state management. - Wrapped input and display logic in a dynamic `cell_view` function for cleaner separation. --- src/components/editable_cell.rs | 48 ++++++++++++++++++++++++--------- src/components/items_list.rs | 5 ++-- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/components/editable_cell.rs b/src/components/editable_cell.rs index 11ac43a..4787b57 100644 --- a/src/components/editable_cell.rs +++ b/src/components/editable_cell.rs @@ -1,17 +1,22 @@ use leptos::*; use leptos::logging::log; +use std::rc::Rc; use web_sys::FocusEvent; #[component] pub fn EditableCell( value: String, - on_input: impl Fn(String) + 'static, + on_input: Rc, // Use `Rc` to allow cloning #[prop(into)] on_focus: Callback, #[prop(optional)] key: Option, // Optional `key` prop ) -> impl IntoView { let (input_value, set_input_value) = create_signal(value.clone()); let (has_focus, set_has_focus) = create_signal(false); // Track focus state locally - let (is_disposed, set_disposed) = create_signal(false); // Track disposal state + let (is_editing, set_is_editing) = create_signal(false); + + // persistent default key value + let default_key = String::new(); + let key_ref = key.as_ref().unwrap_or(&default_key); // Ensure signals aren't updated after disposal on_cleanup(move || { @@ -26,6 +31,7 @@ pub fn EditableCell( }; let handle_input = move |e: web_sys::Event| { + let on_input = Rc::clone(&on_input); // Clone `on_input` to use inside the closure log_signal_get("input_value"); if is_disposed.get_untracked() { return; @@ -39,6 +45,7 @@ pub fn EditableCell( if is_disposed.get() { return; } + set_is_editing.set(true); set_has_focus.set(true); on_focus.call(ev); }; @@ -47,20 +54,37 @@ pub fn EditableCell( if is_disposed.get() { return; } + set_is_editing.set(false); set_has_focus.set(false); }; - // Use key to force updates only when necessary - let _key = key.unwrap_or_default(); + let cell_view = move || { + if is_editing.get() { + view! { + + }.into_view() + } else { + view! { +
+ {input_value.get()} +
+ }.into_view() + } + }; view! { - +
+ {cell_view} +
} } diff --git a/src/components/items_list.rs b/src/components/items_list.rs index b80da9e..fb54ddc 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -10,6 +10,7 @@ use wasm_bindgen::JsCast; use web_sys::{FocusEvent, HtmlElement}; use futures_timer::Delay; use std::time::Duration; +use std::rc::Rc; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -211,7 +212,7 @@ pub fn ItemsList( @@ -220,7 +221,7 @@ pub fn ItemsList(