diff --git a/Cargo.lock b/Cargo.lock index 9ef3255..dd603c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,8 +722,10 @@ dependencies = [ "actix-web", "console_error_panic_hook", "futures", + "futures-timer", "gloo-net 0.5.0", "http 1.2.0", + "js-sys", "leptos", "leptos_actix", "leptos_meta", @@ -1082,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 f58b996..02e7e11 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" @@ -27,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"] 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/editable_cell.rs b/src/components/editable_cell.rs index 2604931..4787b57 100644 --- a/src/components/editable_cell.rs +++ b/src/components/editable_cell.rs @@ -1,39 +1,90 @@ 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 || { + 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| { + 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; + } 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| { + let handle_focus = move |ev:FocusEvent| { + if is_disposed.get() { + return; + } + set_is_editing.set(true); 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; + } + 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 c279b3b..fb54ddc 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -6,6 +6,11 @@ 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}; +use futures_timer::Delay; +use std::time::Duration; +use std::rc::Rc; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -30,12 +35,15 @@ pub fn ItemsList( wikidata_id: None, }]); - let (wikidata_suggestions, set_wikidata_suggestions) = - create_signal(Vec::::new()); + 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 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; @@ -52,7 +60,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()); + } } }); }; @@ -65,13 +76,14 @@ pub fn ItemsList( "name" => { item.name = value.clone(); fetch_wikidata_suggestions(value.clone()); + set_active_row_index.set(Some(index)); } "description" => { item.description = value.clone(); } _ => (), } - } + } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { @@ -87,6 +99,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| { @@ -112,6 +139,59 @@ 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,50 +210,19 @@ 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) } -
  • - } - }).collect::>() - }} -
+ // Editable Description Field @@ -194,6 +243,7 @@ pub fn ItemsList( }).collect::>()} + {render_popup()}
} }