From 07405db01727f956cdf432eb75efd639d88f5836 Mon Sep 17 00:00:00 2001
From: ryan <ryannganga13325@gmail.com>
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 `<head>` 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 {
         <head>
             <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
             <script src="https://twitter.github.io/typeahead.js/releases/latest/typeahead.bundle.min.js"></script>
-            <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.3.1/typeahead.bundle.min.js"></script>
         </head>
         <Router>
             <Routes>
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<String, Vec<WikidataSuggestion>>) -> 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<dyn Fn(String, Function)>);
+        let _ = sync.call1(&JsValue::NULL, &array);
+    }) as Box<dyn Fn(JsValue, Function)>);
 
     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::<js_sys::Function>().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<String, Vec<WikidataSuggestion>>) -> 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 '<div class=\"suggestion-item\">' + data.label + '</div>';"
+    );
+    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::<WikidataSuggestion>() {
+            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)>);
-
-    // 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 $('<div>').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