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.
This commit is contained in:
ryan 2025-04-08 17:55:46 +03:00
parent 5ca277ee80
commit 07405db017
2 changed files with 107 additions and 52 deletions

View file

@ -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>

View file

@ -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);
}
}