From 07405db01727f956cdf432eb75efd639d88f5836 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 8 Apr 2025 17:55:46 +0300 Subject: [PATCH] feat(typeahead): Enhance Bloodhound and Typeahead initialization logic - Improved Bloodhound initialization by adding explicit global storage for the instance. - Enhanced `initialize_bloodhound` to include proper rate limiting, wildcard configuration, and tokenizer setup. - Refactored `initialize_typeahead` to include a more robust dataset configuration with templates for rendering suggestions. - Fixed DOM element access in the `on:input` handler to ensure proper interaction with the input element. - Simplified the `remote_fn` logic in `initialize_bloodhound` for fetching and syncing suggestions. - Added error handling and logging for better debugging during Typeahead initialization. - Ensured closures are properly registered in the global scope to handle Typeahead events. - Updated the JavaScript initialization script to use bracket notation for safer handler invocation. - Removed the additional inclusion of `corejs-typeahead` script from the `` section. --- src/app.rs | 1 - src/components/typeahead_input.rs | 158 ++++++++++++++++++++---------- 2 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8b69dc6..fb9c4f4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,7 +32,6 @@ pub fn App() -> impl IntoView { - diff --git a/src/components/typeahead_input.rs b/src/components/typeahead_input.rs index 501245e..40a482b 100644 --- a/src/components/typeahead_input.rs +++ b/src/components/typeahead_input.rs @@ -1,7 +1,7 @@ use leptos::*; use wasm_bindgen::prelude::*; use crate::models::item::WikidataSuggestion; -use js_sys::{Object, Array, Function, JSON, Reflect}; +use js_sys::{Object, Array, Function, Reflect}; use leptos::html::Input; use gloo_utils::format::JsValueSerdeExt; use wasm_bindgen::JsCast; @@ -26,6 +26,14 @@ pub fn TypeaheadInput( if let Some(input) = node_ref.get() { log!("[INIT] Input element found"); let bloodhound = initialize_bloodhound(fetch_suggestions.clone()); + + // Store bloodhound globally + js_sys::Reflect::set( + &js_sys::global(), + &"bloodhoundInstance".into(), + &bloodhound + ).unwrap(); + initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone()); set_initialized.set(true); break; @@ -46,12 +54,8 @@ pub fn TypeaheadInput( on:input=move |ev| { let value = event_target_value(&ev); log!("[INPUT] Value changed: {}", value); - if let Some(input) = node_ref.get() { - // Correct DOM element access using JsCast - let dom_input: &web_sys::HtmlInputElement = &*input; - let id = dom_input.id(); - let _ = js_sys::eval(&format!("console.log('JS Value:', $('#{}').val())", id)); - } + let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)"); + let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')"); } /> } @@ -75,35 +79,46 @@ extern "C" { fn initialize_bloodhound(fetch: Callback>) -> JsValue { let bloodhound_options = Object::new(); - // Configure Bloodhound remote with proper parameters - let remote_fn = Closure::wrap(Box::new(move |query: String, sync: Function| { - log!("[BLOODHOUND] Fetching suggestions for: {}", query); - let suggestions = fetch.call(query.clone()); + let remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function| { + let query_str = query.as_string().unwrap_or_default(); + log!("[BLOODHOUND] Fetching suggestions for: {}", query_str); + let suggestions = fetch.call(query_str.clone()); log!("[BLOODHOUND] Received {} suggestions", suggestions.len()); let array = Array::new(); for suggestion in &suggestions { let obj = Object::new(); - Reflect::set(&obj, &"label".into(), &suggestion.label.clone().into()).unwrap(); - Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap(); + Reflect::set(&obj, &"label".into(), &suggestion.label.clone().into()).unwrap_or_default(); + Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap_or_default(); array.push(&obj); } - - sync.call1(&JsValue::NULL, &array).unwrap(); - }) as Box); + let _ = sync.call1(&JsValue::NULL, &array); + }) as Box); let remote_config = Object::new(); + + // Url function + Reflect::set( + &remote_config, + &"url".into(), + &JsValue::from_str("/dummy?query=%QUERY") + ).unwrap(); + + // Prepare function Reflect::set( &remote_config, &"prepare".into(), - &js_sys::eval(&format!( - "function(query, callback) {{ - return {}(query, callback); - }}", - remote_fn.as_ref().unchecked_ref::().to_string() - )).unwrap() + remote_fn.as_ref().unchecked_ref() + ).unwrap(); + + // Rate limiting + Reflect::set( + &remote_config, + &"rateLimitWait".into(), + &JsValue::from(300) ).unwrap(); + // Wildcard function Reflect::set( &remote_config, &"wildcard".into(), @@ -111,16 +126,38 @@ fn initialize_bloodhound(fetch: Callback>) -> Js ).unwrap(); Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap(); - Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from_str("whitespace")).unwrap(); - Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from_str("whitespace")).unwrap(); + + // Tokenizer functions from Bloodhound + let tokenizer = js_sys::eval(r#"Bloodhound.tokenizers.whitespace"#) + .expect("Should get whitespace tokenizer"); + + Reflect::set( + &bloodhound_options, + &"datumTokenizer".into(), + &tokenizer + ).unwrap(); + + Reflect::set( + &bloodhound_options, + &"queryTokenizer".into(), + &tokenizer + ).unwrap(); let bloodhound = Bloodhound::new(&bloodhound_options.into()); bloodhound.initialize(true); remote_fn.forget(); + // Explicit retention + js_sys::Reflect::set( + &js_sys::global(), + &"bloodhoundInstance".into(), + &bloodhound + ).unwrap(); + bloodhound.into() } + fn initialize_typeahead( input: &HtmlInputElement, bloodhound: JsValue, @@ -138,52 +175,71 @@ fn initialize_typeahead( Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap(); Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap(); - // Create and register the closure + let templates = Object::new(); + let suggestion_fn = js_sys::Function::new_no_args( + "return '
' + data.label + '
';" + ); + Reflect::set(&templates, &"suggestion".into(), &suggestion_fn.into()).unwrap(); + Reflect::set(&dataset, &"templates".into(), &templates).unwrap(); + let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| { log!("[TYPEAHEAD] Selection made"); - let data: WikidataSuggestion = suggestion.into_serde().unwrap(); - on_select.call(data.clone()); - - if let Some(input) = node_ref.get() { - input.set_value(&data.label); + if let Ok(data) = suggestion.into_serde::() { + 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); - - // Register the closure in the JS global scope + let handler_name = format!("handler_{}", input_id); - let handler_name_global = handler_name.clone(); - let global = js_sys::global(); - Reflect::set( - &global, - &handler_name_global.into(), - &closure.as_ref() + js_sys::Reflect::set( + &js_sys::global(), + &handler_name.clone().into(), + closure.as_ref(), ).unwrap(); + closure.forget(); - // Typeahead initialization using jQuery + // Corrected initialization script using bracket notation for handler let init_script = format!( r#" - (function() {{ - console.log('[TYPEAHEAD] Initializing for #{id}'); + console.log('[JS] Starting Typeahead init for #{id}'); + try {{ + var bloodhound = window.bloodhoundInstance; $('#{id}').typeahead( {{ hint: true, highlight: true, minLength: 1 }}, - {dataset} + {{ + name: 'suggestions', + source: bloodhound.ttAdapter(), + display: 'label', + templates: {{ + suggestion: function(data) {{ + console.log('[JS] Rendering suggestion', data); + return $('
').text(data.label); + }} + }} + }} ).on('typeahead:select', function(ev, suggestion) {{ - console.log('[TYPEAHEAD] Select event triggered'); - {handler}(ev, suggestion); + console.log('[JS] Selection event received'); + window['{handler}'](ev, suggestion); }}); - console.log('[TYPEAHEAD] Initialization complete for #{id}'); - }})(); + console.log('[JS] Typeahead initialized successfully'); + }} catch (e) {{ + console.error('[JS] Typeahead init error:', e); + }} "#, id = input_id, - dataset = JSON::stringify(&dataset).unwrap(), - handler = handler_name + handler = handler_name.replace('-', "_") // Replace hyphens to avoid JS issues ); - log!("[TYPEAHEAD] Init script: {}", init_script); - let _ = js_sys::eval(&init_script).unwrap(); - closure.forget(); + log!("[RUST] Initialization script: {}", init_script); + if let Err(e) = js_sys::eval(&init_script) { + log!("[RUST] Eval error: {:?}", e); + } } \ No newline at end of file