feat(typeahead_input): enhance input handling by adding unique IDs and simplifying focus management

This commit is contained in:
ryan 2025-05-23 14:53:25 +03:00
parent f9fe3eb980
commit a8088c232b
2 changed files with 215 additions and 196 deletions

View file

@ -143,13 +143,6 @@ 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 when we're adding a new row
let (adding_new_row, set_adding_new_row) = create_signal(false);
// Track the length of the items array to detect when a new row is added
let (prev_items_len, set_prev_items_len) = create_signal(0);
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
fn get_current_url() -> String { fn get_current_url() -> String {
use leptos::use_context; use leptos::use_context;
@ -762,29 +755,9 @@ 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);
let set_adding_new_row = set_adding_new_row.clone();
let set_prev_items_len = set_prev_items_len.clone();
Arc::new(move |index: usize, field: &str, value: String| { Arc::new(move |index: usize, field: &str, value: String| {
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);
let set_adding_new_row = set_adding_new_row.clone();
let set_prev_items_len = set_prev_items_len.clone();
// Store the current length before updating
let current_len = items.get().len();
set_prev_items_len.set(current_len);
// Check if this is the last row and we're about to add a new one
let is_last_row = index == current_len - 1;
let will_add_new_row = is_last_row && !value.is_empty() && field == "name";
// Set the flag if we're about to add a new row
if will_add_new_row {
set_adding_new_row.set(true);
log!("[FOCUS] Setting adding_new_row flag to true");
}
set_items.update(move|items| { set_items.update(move|items| {
if let Some(item) = items.get_mut(index) { if let Some(item) = items.get_mut(index) {
match field { match field {
@ -843,8 +816,7 @@ pub fn ItemsList(
} }
log!("Items updated: {:?}", items); 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"];
@ -888,41 +860,7 @@ pub fn ItemsList(
view! { view! {
<td> <td>
{match property { {match property {
"Name" => { "Name" => view! {
let node_ref = create_node_ref::<Input>();
// Clone items.len() before creating the closure to avoid borrowing issues
let items_len = items.len();
// Create a signal to track whether this specific input should be focused
let (should_focus_this, set_should_focus_this) = create_signal(false);
let items_clone = items.clone();
// Determine if this input should be focused based on the adding_new_row flag and indices
create_effect(move |_| {
// Only run this effect when adding_new_row changes to true
if adding_new_row.get() {
// Check if a new row was actually added
let current_len = items_clone.len();
let prev_len = prev_items_len.get();
if current_len > prev_len {
// This is the input that was being edited before adding a new row
let should_focus = index == prev_len - 1;
set_should_focus_this.set(should_focus);
// Reset the adding_new_row flag after a short delay if this is the input that should be focused
if should_focus {
set_timeout(move || {
set_adding_new_row.set(false);
log!("[FOCUS] Reset adding_new_row flag to false");
}, std::time::Duration::from_millis(50));
}
}
}
});
view! {
<div class="typeahead-container"> <div class="typeahead-container">
<TypeaheadInput <TypeaheadInput
value=item.name.clone() value=item.name.clone()
@ -975,22 +913,109 @@ pub fn ItemsList(
}); });
} }
}) })
is_last_row={index == items_len - 1} is_last_row={index == items.len() - 1}
on_input=Callback::new({ on_input=Callback::new({
let update_item_clone = Arc::clone(&update_item_clone); // 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();
let node_ref_clone = node_ref.clone(); // Clone the node_ref for later use
move |value: String| { move |value: String| {
// Always update the item with the new value if index == items_len - 1 && !value.is_empty() {
// The update_item function will handle setting the adding_new_row flag // Store the current active element before modifying the DOM
update_item_clone(index, "name", value); let document = web_sys::window().unwrap().document().unwrap();
let active_element_id = document
.active_element()
.map(|el| el.id())
.unwrap_or_default();
// Store the current input value
let input_value = value.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| {
let new_item = Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
wikidata_id: None,
custom_properties: HashMap::new(),
};
items.push(new_item.clone());
// Save the new item to the database in a separate task
let new_item_clone = new_item.clone();
let current_url_for_task = Rc::clone(&current_url_for_new_item);
let selected_properties_for_task = selected_properties_for_new_item;
spawn_local(async move {
save_item_to_db(
new_item_clone,
selected_properties_for_task,
current_url_for_task.to_string()
).await;
});
});
// Schedule focus restoration after the DOM has been updated
spawn_local(async move {
// Small delay to ensure DOM is updated
gloo_timers::future::TimeoutFuture::new(50).await;
// Try to restore focus to the element that had it
if !active_element_id.is_empty() {
if let Some(element) = document.get_element_by_id(&active_element_id) {
if let Some(input) = element.dyn_ref::<web_sys::HtmlInputElement>() {
// Set the value before focusing to preserve what was typed
input.set_value(&input_value);
let _ = input.focus();
// Trigger the typeahead to show suggestions
let trigger_typeahead_script = format!(
r#"
try {{
// Get the input element
var $input = $('#{}');
if ($input.length > 0) {{
// Manually trigger the typeahead query
$input.typeahead('val', '{}');
// Force the menu to open
setTimeout(function() {{
var event = new Event('input', {{
bubbles: true,
cancelable: true,
}});
$input[0].dispatchEvent(event);
}}, 100);
}}
}} catch(e) {{
console.error('Error triggering typeahead:', e);
}}
"#,
active_element_id,
input_value.replace("'", "\\'") // Escape single quotes
);
// Execute the script after a short delay to ensure typeahead is initialized
gloo_timers::future::TimeoutFuture::new(200).await;
let _ = js_sys::eval(&trigger_typeahead_script);
}
}
}
});
}
} }
}) })
node_ref=node_ref node_ref=node_ref.clone() // Use the node_ref to track this input
should_focus=should_focus_this.get() id=format!("name-input-{}", index) // Add a unique ID to each input
/> />
</div> </div>
}.into_view() }.into_view(),
},
"Description" => view! { "Description" => view! {
<EditableCell <EditableCell

View file

@ -99,24 +99,15 @@ 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, #[prop(optional)] id: Option<String>,
) -> 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 // Use the provided id or generate one
let component_id_for_effect = component_id.clone(); let input_id = id.unwrap_or_else(|| format!("typeahead-input-{}", component_id));
// 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")]
@ -525,6 +516,7 @@ pub fn TypeaheadInput(
class="typeahead-input" class="typeahead-input"
prop:value=value prop:value=value
node_ref=node_ref node_ref=node_ref
id={input_id}
on:focus=move |_| log!("[FOCUS] Name input focused: {}", component_id_for_focus) on:focus=move |_| log!("[FOCUS] Name input focused: {}", component_id_for_focus)
on:blur=move |_| log!("[FOCUS] Name input blurred: {}", component_id_for_blur) on:blur=move |_| log!("[FOCUS] Name input blurred: {}", component_id_for_blur)
on:input=move |ev| { on:input=move |ev| {
@ -532,10 +524,12 @@ pub fn TypeaheadInput(
log!("[INPUT] Value changed: {} ({})", value, component_id_for_input); log!("[INPUT] Value changed: {} ({})", value, component_id_for_input);
// If this is the last row and we have an on_input callback, call it // If this is the last row and we have an on_input callback, call it
if is_last_row && !value.is_empty() {
if let Some(callback) = &on_input { if let Some(callback) = &on_input {
callback.call(value.clone()); callback.call(value.clone());
} }
} }
}
/> />
} }
} }
@ -840,7 +834,7 @@ fn initialize_typeahead(
closures: Rc<RefCell<TypeaheadClosures>>, closures: Rc<RefCell<TypeaheadClosures>>,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
log!("[TYPEAHEAD] Initializing for input: {} ({})", input.id(), component_id); log!("[TYPEAHEAD] Initializing for input: {} ({})", input.id(), component_id);
let input_id = format!("typeahead-input-{}", component_id); let input_id = input.id();
input.set_id(&input_id); input.set_id(&input_id);
// Get a clone of the is_alive flag for use in the closure // Get a clone of the is_alive flag for use in the closure