fix(typeahead): implement component isolation to prevent memory leaks and race conditions
This commit is contained in:
parent
1fd4131298
commit
0f28394fce
1 changed files with 260 additions and 137 deletions
|
@ -22,69 +22,133 @@ pub fn TypeaheadInput(
|
||||||
) -> 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
|
||||||
|
let component_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
||||||
|
|
||||||
// Flag to track if component is mounted
|
// Flag to track if component is mounted
|
||||||
let is_mounted = Rc::new(RefCell::new(true));
|
let is_mounted = Rc::new(RefCell::new(true));
|
||||||
let is_mounted_clone = is_mounted.clone();
|
let is_mounted_clone = is_mounted.clone();
|
||||||
|
|
||||||
// Cleanup function to run when component is unmounted
|
|
||||||
on_cleanup(move || {
|
|
||||||
log!("[CLEANUP] TypeaheadInput component unmounting");
|
|
||||||
*is_mounted_clone.borrow_mut() = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clone necessary values for the async task
|
// Clone necessary values for the async task
|
||||||
let fetch_suggestions_clone = fetch_suggestions.clone();
|
let fetch_suggestions_clone = fetch_suggestions.clone();
|
||||||
let on_select_clone = on_select.clone();
|
let on_select_clone = on_select.clone();
|
||||||
let node_ref_clone = node_ref.clone();
|
let node_ref_clone = node_ref.clone();
|
||||||
|
let component_id_clone = component_id.clone();
|
||||||
|
|
||||||
|
// Create a cancellation token for the async task
|
||||||
|
let (cancel_token, set_cancel_token) = create_signal(false);
|
||||||
|
|
||||||
|
// Spawn the initialization task using spawn_local instead of spawn_local_with_handle
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
log!("[INIT] Component mounted");
|
log!("[INIT] Component mounted: {}", component_id_clone);
|
||||||
|
|
||||||
let mut retries = 0;
|
let mut retries = 0;
|
||||||
while retries < 10 {
|
while retries < 10 && !cancel_token.get() {
|
||||||
// Check if component is still mounted before proceeding
|
// Check if component is still mounted before proceeding
|
||||||
if !*is_mounted.borrow() {
|
if !*is_mounted.borrow() {
|
||||||
log!("[INIT] Component unmounted, aborting initialization");
|
log!("[INIT] Component unmounted, aborting initialization: {}", component_id_clone);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(input) = node_ref_clone.get() {
|
if let Some(input) = node_ref_clone.get() {
|
||||||
log!("[INIT] Input element found");
|
log!("[INIT] Input element found: {}", component_id_clone);
|
||||||
|
|
||||||
// Only proceed if component is still mounted
|
// Only proceed if component is still mounted
|
||||||
if !*is_mounted.borrow() {
|
if !*is_mounted.borrow() || cancel_token.get() {
|
||||||
log!("[INIT] Component unmounted after input found, aborting");
|
log!("[INIT] Component unmounted after input found, aborting: {}", component_id_clone);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone());
|
let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone(), &component_id_clone);
|
||||||
|
|
||||||
// Store bloodhound globally
|
// Store bloodhound in a component-specific global variable
|
||||||
js_sys::Reflect::set(
|
let bloodhound_var = format!("bloodhoundInstance_{}", component_id_clone.replace("-", "_"));
|
||||||
|
if let Err(_) = js_sys::Reflect::set(
|
||||||
&js_sys::global(),
|
&js_sys::global(),
|
||||||
&"bloodhoundInstance".into(),
|
&bloodhound_var.into(),
|
||||||
&bloodhound
|
&bloodhound
|
||||||
).unwrap();
|
) {
|
||||||
|
log!("[ERROR] Failed to store bloodhound instance: {}", component_id_clone);
|
||||||
|
}
|
||||||
|
|
||||||
// Only proceed if component is still mounted
|
// Only proceed if component is still mounted
|
||||||
if !*is_mounted.borrow() {
|
if !*is_mounted.borrow() || cancel_token.get() {
|
||||||
log!("[INIT] Component unmounted before typeahead init, aborting");
|
log!("[INIT] Component unmounted before typeahead init, aborting: {}", component_id_clone);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone());
|
initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone(), &component_id_clone);
|
||||||
|
|
||||||
// Only set initialized if component is still mounted
|
// Only set initialized if component is still mounted
|
||||||
if *is_mounted.borrow() {
|
if *is_mounted.borrow() && !cancel_token.get() {
|
||||||
set_initialized.set(true);
|
// Use a try_update to safely update the signal
|
||||||
|
let _ = try_with_owner(Owner::current().unwrap(), move || {
|
||||||
|
set_initialized.set(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if component is still mounted before sleeping
|
||||||
|
if !*is_mounted.borrow() || cancel_token.get() {
|
||||||
|
log!("[INIT] Component unmounted during retry loop, aborting: {}", component_id_clone);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
gloo_timers::future::sleep(Duration::from_millis(100)).await;
|
gloo_timers::future::sleep(Duration::from_millis(100)).await;
|
||||||
retries += 1;
|
retries += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clone component_id for on_cleanup
|
||||||
|
let component_id_for_cleanup = component_id.clone();
|
||||||
|
|
||||||
|
// Comprehensive cleanup function
|
||||||
|
on_cleanup(move || {
|
||||||
|
log!("[CLEANUP] TypeaheadInput component unmounting: {}", component_id_for_cleanup);
|
||||||
|
|
||||||
|
// Signal the async task to cancel
|
||||||
|
set_cancel_token.set(true);
|
||||||
|
|
||||||
|
// Mark component as unmounted
|
||||||
|
*is_mounted_clone.borrow_mut() = false;
|
||||||
|
|
||||||
|
// Clean up component-specific global references
|
||||||
|
let bloodhound_var = format!("bloodhoundInstance_{}", component_id_for_cleanup.replace("-", "_"));
|
||||||
|
let prepare_fn_var = format!("bloodhoundPrepare_{}", component_id_for_cleanup.replace("-", "_"));
|
||||||
|
|
||||||
|
let cleanup_script = format!(r#"
|
||||||
|
try {{
|
||||||
|
// Clean up the bloodhound instance
|
||||||
|
if (window['{bloodhound_var}']) {{
|
||||||
|
delete window['{bloodhound_var}'];
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Clean up the prepare function
|
||||||
|
if (window['{prepare_fn_var}']) {{
|
||||||
|
delete window['{prepare_fn_var}'];
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Clean up any typeahead instances
|
||||||
|
if (window.typeaheadCleanupFunctions && window.typeaheadCleanupFunctions['{component_id}']) {{
|
||||||
|
window.typeaheadCleanupFunctions['{component_id}']();
|
||||||
|
delete window.typeaheadCleanupFunctions['{component_id}'];
|
||||||
|
}}
|
||||||
|
|
||||||
|
console.log('[JS] Global cleanup completed for {component_id}');
|
||||||
|
}} catch (e) {{
|
||||||
|
console.error('[JS] Cleanup error for {component_id}:', e);
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
bloodhound_var = bloodhound_var,
|
||||||
|
prepare_fn_var = prepare_fn_var,
|
||||||
|
component_id = component_id_for_cleanup
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = js_sys::eval(&cleanup_script) {
|
||||||
|
log!("[RUST] Cleanup script eval error: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
let css = r#"
|
let css = r#"
|
||||||
|
@ -141,6 +205,11 @@ pub fn TypeaheadInput(
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
// Clone component_id for event handlers
|
||||||
|
let component_id_for_focus = component_id.clone();
|
||||||
|
let component_id_for_blur = component_id.clone();
|
||||||
|
let component_id_for_input = component_id.clone();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<style>
|
<style>
|
||||||
{css}
|
{css}
|
||||||
|
@ -151,11 +220,11 @@ pub fn TypeaheadInput(
|
||||||
class="typeahead-input"
|
class="typeahead-input"
|
||||||
prop:value=value
|
prop:value=value
|
||||||
node_ref=node_ref
|
node_ref=node_ref
|
||||||
on:focus=move |_| log!("[FOCUS] Name input focused")
|
on:focus=move |_| log!("[FOCUS] Name input focused: {}", component_id_for_focus)
|
||||||
on:blur=move |_| log!("[FOCUS] Name input blurred")
|
on:blur=move |_| log!("[FOCUS] Name input blurred: {}", component_id_for_blur)
|
||||||
on:input=move |ev| {
|
on:input=move |ev| {
|
||||||
let value = event_target_value(&ev);
|
let value = event_target_value(&ev);
|
||||||
log!("[INPUT] Value changed: {}", value);
|
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 is_last_row && !value.is_empty() {
|
||||||
|
@ -163,40 +232,32 @@ pub fn TypeaheadInput(
|
||||||
callback.call(value.clone());
|
callback.call(value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)");
|
|
||||||
let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')");
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>, component_id: &str) -> JsValue {
|
||||||
extern "C" {
|
|
||||||
#[wasm_bindgen(js_name = "Bloodhound")]
|
|
||||||
type Bloodhound;
|
|
||||||
|
|
||||||
#[wasm_bindgen(constructor, js_namespace = window)]
|
|
||||||
fn new(options: &JsValue) -> Bloodhound;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
fn initialize(this: &Bloodhound, prefetch: bool);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, js_name = "ttAdapter")]
|
|
||||||
fn tt_adapter(this: &Bloodhound) -> JsValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue {
|
|
||||||
let bloodhound_options = Object::new();
|
let bloodhound_options = Object::new();
|
||||||
|
|
||||||
|
// Use component-specific names for global functions
|
||||||
|
let prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_"));
|
||||||
|
|
||||||
// Create a closure that will be called by Bloodhound to fetch suggestions
|
// Create a closure that will be called by Bloodhound to fetch suggestions
|
||||||
let remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function, async_fn: Function| {
|
let remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function, _async_fn: Function| {
|
||||||
let query_str = query.as_string().unwrap_or_default();
|
let query_str = query.as_string().unwrap_or_default();
|
||||||
log!("[BLOODHOUND] Fetching suggestions for: {}", query_str);
|
log!("[BLOODHOUND] Fetching suggestions for: {}", query_str);
|
||||||
|
|
||||||
// Get suggestions from the callback
|
// Safely call the fetch callback
|
||||||
let suggestions = fetch.call(query_str.clone());
|
let suggestions = match try_with_owner(Owner::current().unwrap(), move || {
|
||||||
|
fetch.call(query_str.clone())
|
||||||
|
}) {
|
||||||
|
Ok(suggs) => suggs,
|
||||||
|
Err(e) => {
|
||||||
|
log!("[ERROR] Failed to fetch suggestions: {:?}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create a JavaScript array to hold the suggestions
|
// Create a JavaScript array to hold the suggestions
|
||||||
let js_suggestions = Array::new();
|
let js_suggestions = Array::new();
|
||||||
|
@ -206,19 +267,20 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
let obj = Object::new();
|
let obj = Object::new();
|
||||||
|
|
||||||
// Store the original ID, label, and description
|
// Store the original ID, label, and description
|
||||||
Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap();
|
Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap_or_default();
|
||||||
Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap();
|
Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap_or_default();
|
||||||
Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap();
|
Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap_or_default();
|
||||||
|
|
||||||
// Store the display values directly on the object for easier access
|
// Store the display values directly on the object for easier access
|
||||||
Reflect::set(&obj, &"displayLabel".into(),
|
Reflect::set(&obj, &"displayLabel".into(),
|
||||||
&JsValue::from_str(&suggestion.display.label.value)).unwrap();
|
&JsValue::from_str(&suggestion.display.label.value)).unwrap_or_default();
|
||||||
Reflect::set(&obj, &"displayDescription".into(),
|
Reflect::set(&obj, &"displayDescription".into(),
|
||||||
&JsValue::from_str(&suggestion.display.description.value)).unwrap();
|
&JsValue::from_str(&suggestion.display.description.value)).unwrap_or_default();
|
||||||
|
|
||||||
// Store the full suggestion for later retrieval
|
// Store the full suggestion for later retrieval
|
||||||
let full_suggestion = JsValue::from_serde(&suggestion).unwrap();
|
if let Ok(full_suggestion) = JsValue::from_serde(&suggestion) {
|
||||||
Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap();
|
Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
// Add the object to the array
|
// Add the object to the array
|
||||||
js_suggestions.push(&obj);
|
js_suggestions.push(&obj);
|
||||||
|
@ -236,55 +298,64 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
// Set transport function to avoid AJAX requests
|
// Set transport function to avoid AJAX requests
|
||||||
let transport_fn = js_sys::Function::new_with_args(
|
let transport_fn = js_sys::Function::new_with_args(
|
||||||
"query, syncResults, asyncResults",
|
"query, syncResults, asyncResults",
|
||||||
r#"
|
&format!(r#"
|
||||||
// Call our custom prepare function directly
|
// Call our custom prepare function directly
|
||||||
window.bloodhoundPrepare(query, syncResults, asyncResults);
|
if (window['{prepare_fn_var}']) {{
|
||||||
"#
|
window['{prepare_fn_var}'](query, syncResults, asyncResults);
|
||||||
|
}} else {{
|
||||||
|
console.error('[JS] Prepare function not found: {prepare_fn_var}');
|
||||||
|
syncResults([]);
|
||||||
|
}}
|
||||||
|
"#, prepare_fn_var = prepare_fn_var)
|
||||||
);
|
);
|
||||||
|
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&remote_config,
|
&remote_config,
|
||||||
&"transport".into(),
|
&"transport".into(),
|
||||||
&transport_fn
|
&transport_fn
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
// Set a dummy URL (not actually used with custom transport)
|
// Set a dummy URL (not actually used with custom transport)
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&remote_config,
|
&remote_config,
|
||||||
&"url".into(),
|
&"url".into(),
|
||||||
&JsValue::from_str("/dummy?query=%QUERY")
|
&JsValue::from_str("/dummy?query=%QUERY")
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
// Store our prepare function globally
|
// Store our prepare function globally with component-specific name
|
||||||
js_sys::Reflect::set(
|
// Clone prepare_fn_var before using .into() which consumes it
|
||||||
|
let prepare_fn_var_for_log = prepare_fn_var.clone();
|
||||||
|
if let Err(_) = js_sys::Reflect::set(
|
||||||
&js_sys::global(),
|
&js_sys::global(),
|
||||||
&"bloodhoundPrepare".into(),
|
&prepare_fn_var.into(),
|
||||||
remote_fn.as_ref().unchecked_ref()
|
remote_fn.as_ref().unchecked_ref()
|
||||||
).unwrap();
|
) {
|
||||||
|
log!("[ERROR] Failed to store prepare function: {}", prepare_fn_var_for_log);
|
||||||
|
}
|
||||||
|
|
||||||
// Set rate limiting to prevent too many requests
|
// Set rate limiting to prevent too many requests
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&remote_config,
|
&remote_config,
|
||||||
&"rateLimitWait".into(),
|
&"rateLimitWait".into(),
|
||||||
&JsValue::from(300)
|
&JsValue::from(300)
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
// Set the wildcard for query replacement
|
// Set the wildcard for query replacement
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&remote_config,
|
&remote_config,
|
||||||
&"wildcard".into(),
|
&"wildcard".into(),
|
||||||
&JsValue::from_str("%QUERY")
|
&JsValue::from_str("%QUERY")
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
// Add the remote config to the options
|
// Add the remote config to the options
|
||||||
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap();
|
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap_or_default();
|
||||||
|
|
||||||
// Set the tokenizers
|
// Set the tokenizers
|
||||||
let tokenizer = js_sys::Function::new_no_args(
|
let tokenizer = js_sys::Function::new_no_args(
|
||||||
r#"
|
r#"
|
||||||
return function(query) {
|
return function(datum) {
|
||||||
return query.trim().split(/\s+/);
|
return datum.toString().trim().split(/\s+/);
|
||||||
}
|
};
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -292,13 +363,13 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
&bloodhound_options,
|
&bloodhound_options,
|
||||||
&"datumTokenizer".into(),
|
&"datumTokenizer".into(),
|
||||||
&tokenizer
|
&tokenizer
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&bloodhound_options,
|
&bloodhound_options,
|
||||||
&"queryTokenizer".into(),
|
&"queryTokenizer".into(),
|
||||||
&tokenizer
|
&tokenizer
|
||||||
).unwrap();
|
).unwrap_or_default();
|
||||||
|
|
||||||
// Create and initialize the Bloodhound instance
|
// Create and initialize the Bloodhound instance
|
||||||
let bloodhound = Bloodhound::new(&bloodhound_options.into());
|
let bloodhound = Bloodhound::new(&bloodhound_options.into());
|
||||||
|
@ -316,52 +387,62 @@ fn initialize_typeahead(
|
||||||
bloodhound: JsValue,
|
bloodhound: JsValue,
|
||||||
on_select: Callback<WikidataSuggestion>,
|
on_select: Callback<WikidataSuggestion>,
|
||||||
node_ref: NodeRef<Input>,
|
node_ref: NodeRef<Input>,
|
||||||
|
component_id: &str,
|
||||||
) {
|
) {
|
||||||
log!("[TYPEAHEAD] Initializing for input: {}", input.id());
|
log!("[TYPEAHEAD] Initializing for input: {} ({})", input.id(), component_id);
|
||||||
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
let input_id = format!("typeahead-input-{}", component_id);
|
||||||
input.set_id(&input_id);
|
input.set_id(&input_id);
|
||||||
|
|
||||||
// Create selection handler closure
|
// Create selection handler closure
|
||||||
let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| {
|
let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| {
|
||||||
log!("[TYPEAHEAD] Selection made");
|
log!("[TYPEAHEAD] Selection made");
|
||||||
|
|
||||||
// Try to get the full suggestion from the suggestion object
|
// Safely call the on_select callback
|
||||||
if let Some(full_suggestion) = js_sys::Reflect::get(&suggestion, &"fullSuggestion".into()).ok() {
|
let _ = try_with_owner(Owner::current().unwrap(), move || {
|
||||||
if let Ok(data) = full_suggestion.into_serde::<WikidataSuggestion>() {
|
// Try to get the full suggestion from the suggestion object
|
||||||
log!("[TYPEAHEAD] Selected suggestion: {:?}", data);
|
if let Some(full_suggestion) = js_sys::Reflect::get(&suggestion, &"fullSuggestion".into()).ok() {
|
||||||
|
if let Ok(data) = full_suggestion.into_serde::<WikidataSuggestion>() {
|
||||||
|
log!("[TYPEAHEAD] Selected suggestion: {:?}", data);
|
||||||
|
on_select.call(data.clone());
|
||||||
|
if let Some(input) = node_ref.get() {
|
||||||
|
input.set_value(&data.label);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to deserialize the suggestion directly
|
||||||
|
if let Ok(data) = suggestion.into_serde::<WikidataSuggestion>() {
|
||||||
|
log!("[TYPEAHEAD] Selected suggestion (fallback): {:?}", data);
|
||||||
on_select.call(data.clone());
|
on_select.call(data.clone());
|
||||||
if let Some(input) = node_ref.get() {
|
if let Some(input) = node_ref.get() {
|
||||||
input.set_value(&data.label);
|
input.set_value(&data.label);
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
|
log!("[ERROR] Failed to deserialize suggestion");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Fallback: try to deserialize the suggestion directly
|
|
||||||
if let Ok(data) = suggestion.into_serde::<WikidataSuggestion>() {
|
|
||||||
log!("[TYPEAHEAD] Selected suggestion (fallback): {:?}", data);
|
|
||||||
on_select.call(data.clone());
|
|
||||||
if let Some(input) = node_ref.get() {
|
|
||||||
input.set_value(&data.label);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log!("[ERROR] Failed to deserialize suggestion");
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(web_sys::Event, JsValue)>);
|
}) as Box<dyn FnMut(web_sys::Event, JsValue)>);
|
||||||
|
|
||||||
// Register global handler
|
// Register global handler with component-specific name
|
||||||
let handler_name = format!("handler_{}", input_id.replace("-", "_"));
|
let handler_name = format!("handler_{}", input_id.replace("-", "_"));
|
||||||
|
|
||||||
log!("[TYPEAHEAD] Registering handler with name: {}", handler_name);
|
log!("[TYPEAHEAD] Registering handler with name: {}", handler_name);
|
||||||
|
|
||||||
js_sys::Reflect::set(
|
// Clone handler_name before using it in error log
|
||||||
|
let handler_name_for_log = handler_name.clone();
|
||||||
|
if let Err(_) = js_sys::Reflect::set(
|
||||||
&js_sys::global(),
|
&js_sys::global(),
|
||||||
&handler_name.clone().into(),
|
&handler_name.clone().into(),
|
||||||
closure.as_ref().unchecked_ref(),
|
closure.as_ref().unchecked_ref(),
|
||||||
).unwrap();
|
) {
|
||||||
|
log!("[ERROR] Failed to register handler: {}", handler_name_for_log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll clean this up in the component's cleanup function
|
||||||
closure.forget();
|
closure.forget();
|
||||||
|
|
||||||
// Cleanup code to remove the typeahead when component is unmounted
|
// Register cleanup function for this specific typeahead instance
|
||||||
let cleanup_script = format!(
|
let cleanup_script = format!(
|
||||||
r#"
|
r#"
|
||||||
// Store a reference to the cleanup function for this input
|
// Store a reference to the cleanup function for this input
|
||||||
|
@ -369,43 +450,71 @@ fn initialize_typeahead(
|
||||||
window.typeaheadCleanupFunctions = {{}};
|
window.typeaheadCleanupFunctions = {{}};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
window.typeaheadCleanupFunctions['{id}'] = function() {{
|
window.typeaheadCleanupFunctions['{component_id}'] = function() {{
|
||||||
try {{
|
try {{
|
||||||
$('#{id}').typeahead('destroy');
|
// Destroy the typeahead instance
|
||||||
delete window['{handler}'];
|
$('#{input_id}').typeahead('destroy');
|
||||||
console.log('[JS] Typeahead cleanup for #{id} completed');
|
|
||||||
|
// Remove the handler
|
||||||
|
if (window['{handler_name}']) {{
|
||||||
|
delete window['{handler_name}'];
|
||||||
|
}}
|
||||||
|
|
||||||
|
console.log('[JS] Typeahead cleanup for #{input_id} completed');
|
||||||
}} catch (e) {{
|
}} catch (e) {{
|
||||||
console.error('[JS] Typeahead cleanup error:', e);
|
console.error('[JS] Typeahead cleanup error for #{input_id}:', e);
|
||||||
}}
|
}}
|
||||||
}};
|
}};
|
||||||
"#,
|
"#,
|
||||||
id = input_id,
|
component_id = component_id,
|
||||||
handler = handler_name
|
input_id = input_id,
|
||||||
|
handler_name = handler_name_for_log
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = js_sys::eval(&cleanup_script) {
|
if let Err(e) = js_sys::eval(&cleanup_script) {
|
||||||
log!("[RUST] Cleanup script eval error: {:?}", e);
|
log!("[RUST] Cleanup script eval error: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization script with enhanced logging
|
// Get the bloodhound instance from the component-specific global variable
|
||||||
|
let bloodhound_var = format!("bloodhoundInstance_{}", component_id.replace("-", "_"));
|
||||||
|
|
||||||
|
// Initialization script with enhanced logging and error handling
|
||||||
let init_script = format!(
|
let init_script = format!(
|
||||||
r#"
|
r#"
|
||||||
console.log('[JS] Starting Typeahead init for #{id}');
|
console.log('[JS] Starting Typeahead init for #{input_id}');
|
||||||
try {{
|
try {{
|
||||||
var bloodhound = window.bloodhoundInstance;
|
// Get the bloodhound instance from our component-specific variable
|
||||||
|
var bloodhound = window['{bloodhound_var}'];
|
||||||
|
if (!bloodhound) {{
|
||||||
|
throw new Error('Bloodhound instance not found: {bloodhound_var}');
|
||||||
|
}}
|
||||||
|
|
||||||
// Define a custom source function that directly uses our Rust callback
|
// Define a custom source function that directly uses our Rust callback
|
||||||
var customSource = function(query, syncResults, asyncResults) {{
|
var customSource = function(query, syncResults, asyncResults) {{
|
||||||
console.log('[JS] Custom source called with query:', query);
|
console.log('[JS] Custom source called with query:', query);
|
||||||
|
|
||||||
// Call our global prepare function directly
|
// Get the prepare function from our component-specific variable
|
||||||
window.bloodhoundPrepare(query, function(suggestions) {{
|
var prepareFn = window['{prepare_fn_var}'];
|
||||||
|
if (!prepareFn) {{
|
||||||
|
console.error('[JS] Prepare function not found: {prepare_fn_var}');
|
||||||
|
syncResults([]);
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Call our prepare function
|
||||||
|
prepareFn(query, function(suggestions) {{
|
||||||
console.log('[JS] Suggestions from custom source:', suggestions);
|
console.log('[JS] Suggestions from custom source:', suggestions);
|
||||||
syncResults(suggestions);
|
syncResults(suggestions);
|
||||||
}}, asyncResults);
|
}}, asyncResults);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
$('#{id}').typeahead(
|
// Initialize typeahead with error handling
|
||||||
|
var $input = $('#{input_id}');
|
||||||
|
if ($input.length === 0) {{
|
||||||
|
throw new Error('Input element not found: #{input_id}');
|
||||||
|
}}
|
||||||
|
|
||||||
|
$input.typeahead(
|
||||||
{{
|
{{
|
||||||
hint: true,
|
hint: true,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
|
@ -414,58 +523,72 @@ fn initialize_typeahead(
|
||||||
{{
|
{{
|
||||||
name: 'suggestions',
|
name: 'suggestions',
|
||||||
display: function(data) {{
|
display: function(data) {{
|
||||||
console.log('[JS] Display function called with data:', data);
|
if (!data) return '';
|
||||||
return data.displayLabel || data.label || '';
|
return data.displayLabel || data.label || '';
|
||||||
}},
|
}},
|
||||||
source: customSource,
|
source: customSource,
|
||||||
templates: {{
|
templates: {{
|
||||||
suggestion: function(data) {{
|
suggestion: function(data) {{
|
||||||
console.log('[JS] Rendering suggestion:', data);
|
if (!data) return $('<div>').addClass('empty-suggestion').text('Invalid data');
|
||||||
|
|
||||||
return $('<div>')
|
return $('<div>')
|
||||||
.addClass('suggestion-item')
|
.addClass('suggestion-item')
|
||||||
.append($('<div>').addClass('label').text(data.displayLabel || data.label))
|
.append($('<div>').addClass('label').text(data.displayLabel || data.label || ''))
|
||||||
.append($('<div>').addClass('description').text(data.displayDescription || data.description));
|
.append($('<div>').addClass('description').text(data.displayDescription || data.description || ''));
|
||||||
}},
|
}},
|
||||||
empty: function() {{
|
empty: function() {{
|
||||||
console.log('[JS] No suggestions found');
|
|
||||||
return $('<div>').addClass('empty-suggestion').text('No matches found');
|
return $('<div>').addClass('empty-suggestion').text('No matches found');
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
.on('typeahead:select', function(ev, suggestion) {{
|
.on('typeahead:select', function(ev, suggestion) {{
|
||||||
|
if (!suggestion) {{
|
||||||
|
console.error('[JS] Selection event received with null suggestion');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
console.log('[JS] Selection event received with suggestion:', suggestion);
|
console.log('[JS] Selection event received with suggestion:', suggestion);
|
||||||
window['{handler}'](ev, suggestion);
|
|
||||||
|
// Get the handler from our component-specific variable
|
||||||
|
var handler = window['{handler_name}'];
|
||||||
|
if (!handler) {{
|
||||||
|
console.error('[JS] Handler function not found: {handler_name}');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Call the handler
|
||||||
|
handler(ev, suggestion);
|
||||||
}});
|
}});
|
||||||
console.log('[JS] Typeahead initialized successfully');
|
|
||||||
|
console.log('[JS] Typeahead initialized successfully for #{input_id}');
|
||||||
}} catch (e) {{
|
}} catch (e) {{
|
||||||
console.error('[JS] Typeahead init error:', e);
|
console.error('[JS] Typeahead init error for #{input_id}:', e);
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
id = input_id,
|
input_id = input_id,
|
||||||
handler = handler_name
|
bloodhound_var = bloodhound_var,
|
||||||
|
prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_")),
|
||||||
|
handler_name = handler_name_for_log
|
||||||
);
|
);
|
||||||
|
|
||||||
log!("[RUST] Initialization script: {}", init_script);
|
log!("[RUST] Running initialization script for: {}", input_id);
|
||||||
if let Err(e) = js_sys::eval(&init_script) {
|
if let Err(e) = js_sys::eval(&init_script) {
|
||||||
log!("[RUST] Eval error: {:?}", e);
|
log!("[RUST] Eval error: {:?}", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Register cleanup function to run when component is unmounted
|
|
||||||
on_cleanup(move || {
|
#[wasm_bindgen]
|
||||||
log!("[CLEANUP] Running typeahead cleanup for input: {}", input_id);
|
extern "C" {
|
||||||
let cleanup_call = format!(
|
#[wasm_bindgen(js_name = "Bloodhound")]
|
||||||
r#"
|
type Bloodhound;
|
||||||
if (window.typeaheadCleanupFunctions && window.typeaheadCleanupFunctions['{id}']) {{
|
|
||||||
window.typeaheadCleanupFunctions['{id}']();
|
#[wasm_bindgen(constructor, js_namespace = window)]
|
||||||
delete window.typeaheadCleanupFunctions['{id}'];
|
fn new(options: &JsValue) -> Bloodhound;
|
||||||
}}
|
|
||||||
"#,
|
#[wasm_bindgen(method)]
|
||||||
id = input_id
|
fn initialize(this: &Bloodhound, prefetch: bool);
|
||||||
);
|
|
||||||
|
#[wasm_bindgen(method, js_name = "ttAdapter")]
|
||||||
if let Err(e) = js_sys::eval(&cleanup_call) {
|
fn tt_adapter(this: &Bloodhound) -> JsValue;
|
||||||
log!("[RUST] Cleanup call eval error: {:?}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue