feat(typeahead_input): add focus management for input fields and enhance initialization

This commit is contained in:
ryan 2025-05-19 15:53:03 +03:00
parent 8cfef276df
commit eeb8e100e8
2 changed files with 174 additions and 143 deletions

View file

@ -143,6 +143,11 @@ pub fn ItemsList(
let (property_cache, set_property_cache) = create_signal(HashMap::<String, HashMap<String, String>>::new()); let (property_cache, set_property_cache) = create_signal(HashMap::<String, HashMap<String, String>>::new());
let (suggestions, set_suggestions) = create_signal(Vec::<WikidataSuggestion>::new()); let (suggestions, set_suggestions) = create_signal(Vec::<WikidataSuggestion>::new());
// Add a state to track which input field should be focused after re-render
let (focus_index, set_focus_index) = create_signal(None::<usize>);
let (adding_new_row, set_adding_new_row) = create_signal(false);
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
fn get_current_url() -> String { fn get_current_url() -> String {
use leptos::use_context; use leptos::use_context;
@ -755,68 +760,81 @@ pub fn ItemsList(
let update_item = { let update_item = {
let set_items = set_items.clone(); let set_items = set_items.clone();
let current_url = Rc::clone(&current_url); let current_url = Rc::clone(&current_url);
Arc::new(move |index: usize, field: &str, value: String| { let set_focus_index = set_focus_index.clone();
let set_items = set_items.clone(); let set_adding_new_row = set_adding_new_row.clone();
let current_url = Rc::clone(&current_url);
set_items.update(move|items| {
if let Some(item) = items.get_mut(index) {
match field {
"name" => {
item.name = value.clone();
fetch_wikidata_suggestions(format!("name-{}", index), value.clone());
// Fetch Wikidata properties if the field is "name" and the item has a valid Wikidata ID Arc::new(move |index: usize, field: &str, value: String| {
if !value.is_empty() { let set_items = set_items.clone();
if let Some(wikidata_id) = &item.wikidata_id { let current_url = Rc::clone(&current_url);
let wikidata_id = wikidata_id.clone(); let set_focus_index = set_focus_index.clone();
spawn_local(async move { let set_adding_new_row = set_adding_new_row.clone();
let properties = fetch_item_properties(&wikidata_id, set_property_labels.clone(), property_cache.clone(), set_property_cache.clone(), property_labels.clone()).await;
log!("Fetched properties for index {}: {:?}", index, properties); set_items.update(move|items| {
}); if let Some(item) = items.get_mut(index) {
match field {
"name" => {
item.name = value.clone();
fetch_wikidata_suggestions(format!("name-{}", index), value.clone());
// Fetch Wikidata properties if the field is "name" and the item has a valid Wikidata ID
if !value.is_empty() {
if let Some(wikidata_id) = &item.wikidata_id {
let wikidata_id = wikidata_id.clone();
spawn_local(async move {
let properties = fetch_item_properties(&wikidata_id, set_property_labels.clone(), property_cache.clone(), set_property_cache.clone(), property_labels.clone()).await;
log!("Fetched properties for index {}: {:?}", index, properties);
});
}
} }
} }
"description" => {
item.description = value.clone();
}
_ => {
// Update custom property
item.custom_properties.insert(field.to_string(), value.clone());
}
} }
"description" => {
item.description = value.clone(); // Save the updated item to the database
} let item_clone = item.clone();
_ => { spawn_local({
// Update custom property let current_url = Rc::clone(&current_url);
item.custom_properties.insert(field.to_string(), value.clone()); async move {
} save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
}
});
} }
// Automatically add a new row when editing the last row
if index == items.len() - 1 && !value.is_empty() {
// Set flag that we're adding a new row and need to maintain focus
set_adding_new_row.set(true);
// Save the updated item to the database // Remember which index we were editing
let item_clone = item.clone(); set_focus_index.set(Some(index));
spawn_local({
let current_url = Rc::clone(&current_url);
async move {
save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
}
});
}
// Automatically add a new row when editing the last row
if index == items.len() - 1 && !value.is_empty() {
let new_item = Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
// reviews: vec![],
wikidata_id: None,
custom_properties: HashMap::new(),
};
items.push(new_item.clone());
// Save the new item to the database let new_item = Item {
spawn_local({ id: Uuid::new_v4().to_string(),
let current_url = Rc::clone(&current_url); name: String::new(),
async move { description: String::new(),
save_item_to_db(new_item, selected_properties, current_url.to_string()).await; // reviews: vec![],
} wikidata_id: None,
}); custom_properties: HashMap::new(),
} };
log!("Items updated: {:?}", items); items.push(new_item.clone());
});
})}; // Save the new item to the database
spawn_local({
let current_url = Rc::clone(&current_url);
async move {
save_item_to_db(new_item, selected_properties, current_url.to_string()).await;
}
});
}
log!("Items updated: {:?}", items);
});
})
};
// List of properties to display as rows // List of properties to display as rows
let properties = vec!["Name", "Description"]; let properties = vec!["Name", "Description"];
@ -860,102 +878,102 @@ pub fn ItemsList(
view! { view! {
<td> <td>
{match property { {match property {
"Name" => view! { "Name" => {
<div class="typeahead-container"> let node_ref = create_node_ref::<Input>();
<TypeaheadInput
value=item.name.clone()
fetch_suggestions=Callback::new({
let key = format!("name-{}", index);
let wikidata_suggestions_clone = wikidata_suggestions.clone();
move |query: String| -> Vec<WikidataSuggestion> { // Determine if this input should be focused
// Fetch suggestions in a separate function to avoid capturing too much let should_focus = move || {
fetch_wikidata_suggestions(key.clone(), query.clone()); if adding_new_row.get() {
if let Some(focus_idx) = focus_index.get() {
// Return current suggestions from the signal // Focus the input that was being edited before adding a new row
let suggestions = wikidata_suggestions_clone.get(); focus_idx == index
suggestions.get(&key).cloned().unwrap_or_default() } else {
false
} }
}) } else {
on_select=Callback::new({ false
let set_items_clone = set_items.clone(); }
let set_property_labels_clone = set_property_labels.clone(); };
let property_cache_clone = property_cache.clone();
let set_property_cache_clone = set_property_cache.clone();
let property_labels_clone = property_labels.clone();
move |suggestion: WikidataSuggestion| { // Create an effect to reset the adding_new_row flag after rendering
let wikidata_id = suggestion.id.clone(); create_effect(move |_| {
if adding_new_row.get() && should_focus() {
// Reset the flags after focus is applied
set_timeout(move || {
set_adding_new_row.set(false);
set_focus_index.set(None);
}, std::time::Duration::from_millis(50));
}
});
set_items_clone.update(|items| { view! {
if let Some(item) = items.get_mut(index) { <div class="typeahead-container">
item.name = suggestion.display.label.value.clone(); <TypeaheadInput
item.description = suggestion.display.description.value.clone(); value=item.name.clone()
item.wikidata_id = Some(wikidata_id.clone()); fetch_suggestions=Callback::new({
} let key = format!("name-{}", index);
}); let wikidata_suggestions_clone = wikidata_suggestions.clone();
// Fetch properties in a separate task move |query: String| -> Vec<WikidataSuggestion> {
let set_property_labels_for_task = set_property_labels_clone.clone(); // Fetch suggestions in a separate function to avoid capturing too much
let property_cache_for_task = property_cache_clone.clone(); fetch_wikidata_suggestions(key.clone(), query.clone());
let set_property_cache_for_task = set_property_cache_clone.clone();
let property_labels_for_task = property_labels_clone.clone();
let wikidata_id_for_task = wikidata_id.clone();
spawn_local(async move { // Return current suggestions from the signal
fetch_item_properties( let suggestions = wikidata_suggestions_clone.get();
&wikidata_id_for_task, suggestions.get(&key).cloned().unwrap_or_default()
set_property_labels_for_task, }
property_cache_for_task, })
set_property_cache_for_task, on_select=Callback::new({
property_labels_for_task let set_items_clone = set_items.clone();
).await; let set_property_labels_clone = set_property_labels.clone();
}); let property_cache_clone = property_cache.clone();
} let set_property_cache_clone = set_property_cache.clone();
}) let property_labels_clone = property_labels.clone();
is_last_row={index == items.len() - 1}
on_input=Callback::new({
// Clone items.len() before moving into the closure
let items_len = items.len();
let set_items_clone = set_items.clone();
let current_url_clone = Rc::clone(&current_url_clone);
let selected_properties_clone = selected_properties.clone();
move |value: String| { move |suggestion: WikidataSuggestion| {
if index == items_len - 1 && !value.is_empty() { let wikidata_id = suggestion.id.clone();
let current_url_for_new_item = Rc::clone(&current_url_clone);
let selected_properties_for_new_item = selected_properties_clone.clone();
set_items_clone.update(|items| { set_items_clone.update(|items| {
let new_item = Item { if let Some(item) = items.get_mut(index) {
id: Uuid::new_v4().to_string(), item.name = suggestion.display.label.value.clone();
name: String::new(), item.description = suggestion.display.description.value.clone();
description: String::new(), item.wikidata_id = Some(wikidata_id.clone());
wikidata_id: None, }
custom_properties: HashMap::new(), });
};
items.push(new_item.clone());
// Save the new item to the database in a separate task // Fetch properties in a separate task
let new_item_clone = new_item.clone(); let set_property_labels_for_task = set_property_labels_clone.clone();
let current_url_for_task = Rc::clone(&current_url_for_new_item); let property_cache_for_task = property_cache_clone.clone();
let selected_properties_for_task = selected_properties_for_new_item; let set_property_cache_for_task = set_property_cache_clone.clone();
let property_labels_for_task = property_labels_clone.clone();
let wikidata_id_for_task = wikidata_id.clone();
spawn_local(async move { spawn_local(async move {
save_item_to_db( fetch_item_properties(
new_item_clone, &wikidata_id_for_task,
selected_properties_for_task, set_property_labels_for_task,
current_url_for_task.to_string() property_cache_for_task,
).await; set_property_cache_for_task,
}); property_labels_for_task
).await;
}); });
} }
} })
}) is_last_row={index == items.len() - 1}
node_ref=create_node_ref() on_input=Callback::new({
/> let update_item_clone = Arc::clone(&update_item_clone);
</div>
}.into_view(), move |value: String| {
// Update the item with the new value
update_item_clone(index, "name", value);
}
})
node_ref=node_ref
should_focus=should_focus()
/>
</div>
}.into_view()
},
"Description" => view! { "Description" => view! {
<EditableCell <EditableCell

View file

@ -99,12 +99,25 @@ pub fn TypeaheadInput(
node_ref: NodeRef<Input>, node_ref: NodeRef<Input>,
#[prop(optional)] is_last_row: bool, #[prop(optional)] is_last_row: bool,
#[prop(optional)] on_input: Option<Callback<String>>, #[prop(optional)] on_input: Option<Callback<String>>,
#[prop(optional)] should_focus: bool,
) -> impl IntoView { ) -> impl IntoView {
let (is_initialized, set_initialized) = create_signal(false); let (is_initialized, set_initialized) = create_signal(false);
// Create a unique ID for this component instance // Create a unique ID for this component instance
let component_id = format!("typeahead-{}", uuid::Uuid::new_v4()); let component_id = format!("typeahead-{}", uuid::Uuid::new_v4());
// Clone component_id before moving it into the closure
let component_id_for_effect = component_id.clone();
// Effect to handle focus when should_focus is true
create_effect(move |_| {
if should_focus {
if let Some(input) = node_ref.get() {
let _ = input.focus();
log!("[FOCUS] Auto-focusing input: {}", component_id_for_effect);
}
}
});
// WASM-specific initialization // WASM-specific initialization
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {