Compare commits

...

6 commits

Author SHA1 Message Date
a02fcabe19 fix(editable cells): enhance EditableCell component with editing toggle and Rc for on_input
- Replaced `on_input` function with `Rc<dyn Fn(String)>` 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.
2024-12-31 00:39:31 +03:00
b84cd37c44 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.
2024-12-30 16:10:13 +03:00
b616fbb438 build(deps): add futures-timer v3.0.0 to Cargo.toml 2024-12-30 14:23:38 +03:00
40140b40c4 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.
2024-12-30 14:20:50 +03:00
c4a45d9185 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.)
2024-12-30 14:18:33 +03:00
8cd277d66a feat(item_list): Added active cell tracking with position updates for popup rendering. 2024-12-27 03:01:03 +03:00
5 changed files with 193 additions and 58 deletions

8
Cargo.lock generated
View file

@ -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"

View file

@ -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"]

View file

@ -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;
}

View file

@ -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<dyn Fn(String)>, // Use `Rc` to allow cloning
#[prop(into)] on_focus: Callback<FocusEvent>,
#[prop(optional)] key: Option<String>, // 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! {
<input
type="text"
value={input_value.get()}
on:input=handle_input.clone()
on:focus=handle_focus.clone()
on:blur=handle_blur.clone()
class={if has_focus.get() { "focused" } else { "not-focused" }}
/>
}.into_view()
} else {
view! {
<div
tabindex="0"
on:focus=handle_focus.clone()
>
{input_value.get()}
</div>
}.into_view()
}
};
view! {
<input
type="text"
value={input_value.get()}
on:input=handle_input
on:focus=handle_focus
on:blur=handle_blur
class={if has_focus.get() { "focused" } else { "not-focused" }}
/>
<div key={key_ref.clone()}>
{cell_view}
</div>
}
}

View file

@ -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::<WikidataSuggestion>::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::<usize>);
let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(Vec::<WikidataSuggestion>::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::<HtmlElement>() {
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! {
<div
class="suggestions-popup"
style=move || {
let suggestions = wikidata_suggestions.get();
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()
}
}
>
<ul>
{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! {
<li on:click=move |_| {
if let Some(index) = active_row_index.get() {
set_items.update(|items| {
if let Some(item) = items.get_mut(index) {
item.name = label_for_click.clone();
item.description = description_for_click.clone();
item.wikidata_id = Some(id.clone());
item.tags.push(("wikidata_id".to_string(), id.clone()));
}
});
}
set_wikidata_suggestions.set(Vec::new());
set_active_cell_position.set(None);
}>
{format!("{} - {}", label_for_display, description_for_display)}
</li>
}
}).collect::<Vec<_>>()}
</ul>
</div>
}
};
view! {
<div>
<h1>{ "Items List" }</h1>
@ -130,50 +210,19 @@ pub fn ItemsList(
<tr>
// Editable Name Field with Wikidata Integration
<td>
<EditableCell
value=item.name.clone()
on_input=move |value| update_item(index, "name", value)
key=format!("name-{}", index) // Unique key per cell
/>
<ul>
{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! {
<li on:click=move |_| {
set_items.update(|items| {
if let Some(item) = items.get_mut(index) {
item.description = description_for_click.clone();
item.tags.extend(tags.clone());
item.wikidata_id = Some(id.clone());
item.name = label_for_click.clone();
}
});
}>
{ format!("{} - {}", label_for_display, description_for_display) }
</li>
}
}).collect::<Vec<_>>()
}}
</ul>
<EditableCell
value=item.name.clone()
on_input=Rc::new(move |value| update_item(index, "name", value))
on_focus=move |event| handle_focus(index, "name", event)
key=format!("name-{}", index)
/>
</td>
// Editable Description Field
<td>
<EditableCell
value=item.description.clone()
on_input=move |value| update_item(index, "description", value)
on_input=Rc::new(move |value| update_item(index, "description", value))
on_focus=move |event| handle_focus(index, "description", event)
key=format!("description-{}", index)
/>
</td>
@ -194,6 +243,7 @@ pub fn ItemsList(
}).collect::<Vec<_>>()}
</tbody>
</table>
{render_popup()}
</div>
}
}