fix(typeahead): bypass Bloodhound's AJAX to properly display typeahead suggestions

This commit is contained in:
ryan 2025-04-17 17:51:58 +03:00
parent 486bf9cbad
commit fd0d4a5f38

View file

@ -132,87 +132,100 @@ extern "C" {
fn tt_adapter(this: &Bloodhound) -> JsValue; fn tt_adapter(this: &Bloodhound) -> JsValue;
} }
fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue { fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue {
let bloodhound_options = Object::new(); let bloodhound_options = Object::new();
let remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function| { // 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 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
let suggestions = fetch.call(query_str.clone()); let suggestions = fetch.call(query_str.clone());
let array = Array::new(); // Create a JavaScript array to hold the suggestions
for suggestion in &suggestions { let js_suggestions = Array::new();
// Convert each suggestion to a JavaScript object
for suggestion in suggestions {
let obj = Object::new(); let obj = Object::new();
// Set flattened structure for Typeahead compatibility // Store the original ID, label, and description
Reflect::set(&obj, &"id".into(), &suggestion.id.clone().into()).unwrap(); Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap();
Reflect::set(&obj, &"label".into(), &suggestion.label.clone().into()).unwrap(); Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap();
Reflect::set(&obj, &"description".into(), &suggestion.description.clone().into()).unwrap(); Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap();
// Flatten display values for direct access // Store the display values directly on the object for easier access
Reflect::set( Reflect::set(&obj, &"displayLabel".into(),
&obj, &JsValue::from_str(&suggestion.display.label.value)).unwrap();
&"displayLabel".into(), Reflect::set(&obj, &"displayDescription".into(),
&suggestion.display.label.value.clone().into() &JsValue::from_str(&suggestion.display.description.value)).unwrap();
).unwrap();
Reflect::set( // Store the full suggestion for later retrieval
&obj, let full_suggestion = JsValue::from_serde(&suggestion).unwrap();
&"displayDescription".into(), Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap();
&suggestion.display.description.value.clone().into()
).unwrap();
array.push(&obj); // Add the object to the array
js_suggestions.push(&obj);
} }
log!("[BLOODHOUND] suggestions: {:?}", array); log!("[BLOODHOUND] Processed suggestions: {:?}", js_suggestions);
let _ = sync.call1(&JsValue::NULL, &array); // Call the sync function with the suggestions
}) as Box<dyn Fn(JsValue, Function)>); let _ = sync.call1(&JsValue::NULL, &js_suggestions);
}) as Box<dyn Fn(JsValue, Function, Function)>);
// Configure the remote options
let remote_config = Object::new(); let remote_config = Object::new();
// Url function // Set transport function to avoid AJAX requests
let transport_fn = js_sys::Function::new_with_args(
"query, syncResults, asyncResults",
r#"
// Call our custom prepare function directly
window.bloodhoundPrepare(query, syncResults, asyncResults);
"#
);
Reflect::set(
&remote_config,
&"transport".into(),
&transport_fn
).unwrap();
// 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();
// Prepare function // Store our prepare function globally
Reflect::set( js_sys::Reflect::set(
&remote_config, &js_sys::global(),
&"prepare".into(), &"bloodhoundPrepare".into(),
remote_fn.as_ref().unchecked_ref() remote_fn.as_ref().unchecked_ref()
).unwrap(); ).unwrap();
// Rate limiting // 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();
// Response filter to prevent HTML parsing errors // Set the wildcard for query replacement
let filter_fn = js_sys::Function::new_no_args(
"return function(response) { return response || []; }"
);
Reflect::set(
&remote_config,
&"filter".into(),
&filter_fn
).unwrap();
// Wildcard function
Reflect::set( Reflect::set(
&remote_config, &remote_config,
&"wildcard".into(), &"wildcard".into(),
&JsValue::from_str("%QUERY") &JsValue::from_str("%QUERY")
).unwrap(); ).unwrap();
// 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();
// Tokenizer functions from Bloodhound // 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(query) {
@ -233,21 +246,17 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
&tokenizer &tokenizer
).unwrap(); ).unwrap();
// Create and initialize the Bloodhound instance
let bloodhound = Bloodhound::new(&bloodhound_options.into()); let bloodhound = Bloodhound::new(&bloodhound_options.into());
bloodhound.initialize(true); bloodhound.initialize(true);
// Prevent the closure from being garbage collected
remote_fn.forget(); remote_fn.forget();
// Explicit retention // Return the Bloodhound instance
js_sys::Reflect::set(
&js_sys::global(),
&"bloodhoundInstance".into(),
&bloodhound
).unwrap();
bloodhound.into() bloodhound.into()
} }
fn initialize_typeahead( fn initialize_typeahead(
input: &HtmlInputElement, input: &HtmlInputElement,
bloodhound: JsValue, bloodhound: JsValue,
@ -261,12 +270,26 @@ fn initialize_typeahead(
// 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");
if let Ok(data) = suggestion.into_serde::<WikidataSuggestion>() {
// Try to get the full suggestion from the suggestion object
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); log!("[TYPEAHEAD] Selected suggestion: {:?}", 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;
}
}
// 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 { } else {
log!("[ERROR] Failed to deserialize suggestion"); log!("[ERROR] Failed to deserialize suggestion");
} }
@ -287,6 +310,18 @@ fn initialize_typeahead(
console.log('[JS] Starting Typeahead init for #{id}'); console.log('[JS] Starting Typeahead init for #{id}');
try {{ try {{
var bloodhound = window.bloodhoundInstance; var bloodhound = window.bloodhoundInstance;
// Define a custom source function that directly uses our Rust callback
var customSource = function(query, syncResults, asyncResults) {{
console.log('[JS] Custom source called with query:', query);
// Call our global prepare function directly
window.bloodhoundPrepare(query, function(suggestions) {{
console.log('[JS] Suggestions from custom source:', suggestions);
syncResults(suggestions);
}}, asyncResults);
}};
$('#{id}').typeahead( $('#{id}').typeahead(
{{ {{
hint: true, hint: true,
@ -297,21 +332,9 @@ fn initialize_typeahead(
name: 'suggestions', name: 'suggestions',
display: function(data) {{ display: function(data) {{
console.log('[JS] Display function called with data:', data); console.log('[JS] Display function called with data:', data);
return data.display?.label?.value || data.label || ''; return data.displayLabel || data.label || '';
}},
source: function(query, syncResults) {{
console.log('[JS] Bloodhound source called with query:', query);
var bloodhound = window.bloodhoundInstance;
bloodhound.ttAdapter()(query, function(suggestions) {{
console.log('[JS] Suggestions from Bloodhound before syncResults:', suggestions);
if (Array.isArray(suggestions)) {{
console.log('[JS] Passing suggestions to syncResults:', suggestions);
syncResults(suggestions);
}} else {{
console.warn('[JS] Suggestions are not an array:', suggestions);
}}
}});
}}, }},
source: customSource,
templates: {{ templates: {{
suggestion: function(data) {{ suggestion: function(data) {{
console.log('[JS] Rendering suggestion:', data); console.log('[JS] Rendering suggestion:', data);
@ -327,15 +350,6 @@ fn initialize_typeahead(
}} }}
}} }}
) )
.on('typeahead:asyncreceive', function(ev, dataset, suggestions) {{
console.log('[JS] Received suggestions in typeahead:asyncreceive:', suggestions);
if (suggestions && suggestions.length > 0) {{
console.log('[JS] Suggestions passed to dropdown:', suggestions);
$(this).data('ttTypeahead').dropdown.open();
}} else {{
console.warn('[JS] No suggestions received or suggestions are empty.');
}}
}})
.on('typeahead:select', function(ev, suggestion) {{ .on('typeahead:select', function(ev, suggestion) {{
console.log('[JS] Selection event received with suggestion:', suggestion); console.log('[JS] Selection event received with suggestion:', suggestion);
window['{handler}'](ev, suggestion); window['{handler}'](ev, suggestion);