feat(typeahead): enhance TypeaheadInput component for improved functionality
- Improved Bloodhound initialization: - Added nested display structure for suggestions (label and description). - Included a response filter to handle empty responses gracefully. - Added rate-limiting and wildcard support for remote queries. - Enhanced Typeahead initialization: - Added custom templates for rendering suggestions with nested display fields. - Included debug logs for better traceability of Typeahead and Bloodhound instances. - Improved error handling for deserialization of suggestions. - Added support for opening the dropdown on receiving suggestions. - Updated input event handler to include additional debug checks for Typeahead instance. - Ensured proper cleanup and memory management for closures and global handlers. - Added custom CSS styles for `.typeahead` input and suggestion dropdown to enhance UI/UX.
This commit is contained in:
parent
07405db017
commit
f646b92d3a
1 changed files with 135 additions and 32 deletions
|
@ -44,9 +44,65 @@ pub fn TypeaheadInput(
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
<style>
|
||||||
|
{r#"
|
||||||
|
.typeahead.tt-input {{
|
||||||
|
background: transparent !important;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.tt-menu {{
|
||||||
|
width: 100% !important;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.tt-dataset-suggestions {{
|
||||||
|
padding: 8px 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.suggestion-item * {{
|
||||||
|
pointer-events: none; /* Prevent element interception */
|
||||||
|
white-space: nowrap; /* Prevent text wrapping */
|
||||||
|
overflow: hidden; /* Hide overflow */
|
||||||
|
text-overflow: ellipsis; /* Add ellipsis for long text */
|
||||||
|
}}
|
||||||
|
|
||||||
|
.suggestion-item {{
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.suggestion-item:hover {{
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: pointer;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.label {{
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.description {{
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 2px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.empty-suggestion {{
|
||||||
|
padding: 8px 15px;
|
||||||
|
color: #999;
|
||||||
|
}}
|
||||||
|
"#}
|
||||||
|
</style>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="typeahead"
|
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")
|
||||||
|
@ -56,6 +112,15 @@ pub fn TypeaheadInput(
|
||||||
log!("[INPUT] Value changed: {}", value);
|
log!("[INPUT] Value changed: {}", value);
|
||||||
let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)");
|
let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)");
|
||||||
let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')");
|
let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')");
|
||||||
|
// Add debug check for Typeahead instance
|
||||||
|
if let Some(input) = node_ref.get() {
|
||||||
|
let dom_input: web_sys::HtmlInputElement = input.unchecked_into();
|
||||||
|
let id = dom_input.id();
|
||||||
|
let _ = js_sys::eval(&format!(
|
||||||
|
"console.log('Typeahead instance for #{id}:', $('#{id}').data('ttTypeahead'))",
|
||||||
|
id = id
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -83,13 +148,29 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
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);
|
||||||
let suggestions = fetch.call(query_str.clone());
|
let suggestions = fetch.call(query_str.clone());
|
||||||
log!("[BLOODHOUND] Received {} suggestions", suggestions.len());
|
|
||||||
|
|
||||||
let array = Array::new();
|
let array = Array::new();
|
||||||
for suggestion in &suggestions {
|
for suggestion in &suggestions {
|
||||||
let obj = Object::new();
|
let obj = Object::new();
|
||||||
Reflect::set(&obj, &"label".into(), &suggestion.label.clone().into()).unwrap_or_default();
|
|
||||||
Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap_or_default();
|
// Create nested display structure matching API response
|
||||||
|
let display = Object::new();
|
||||||
|
|
||||||
|
let label_obj = Object::new();
|
||||||
|
Reflect::set(&label_obj, &"value".into(), &suggestion.label.clone().into()).unwrap();
|
||||||
|
Reflect::set(&display, &"label".into(), &label_obj).unwrap();
|
||||||
|
|
||||||
|
let desc_obj = Object::new();
|
||||||
|
Reflect::set(&desc_obj, &"value".into(), &suggestion.description.clone().into()).unwrap();
|
||||||
|
Reflect::set(&display, &"description".into(), &desc_obj).unwrap();
|
||||||
|
|
||||||
|
Reflect::set(&obj, &"display".into(), &display).unwrap();
|
||||||
|
|
||||||
|
// Add flat fields as fallback
|
||||||
|
Reflect::set(&obj, &"label".into(), &suggestion.label.clone().into()).unwrap();
|
||||||
|
Reflect::set(&obj, &"description".into(), &suggestion.description.clone().into()).unwrap();
|
||||||
|
|
||||||
|
log!("[BLOODHOUND] Constructed suggestion object: {:?}", obj);
|
||||||
array.push(&obj);
|
array.push(&obj);
|
||||||
}
|
}
|
||||||
let _ = sync.call1(&JsValue::NULL, &array);
|
let _ = sync.call1(&JsValue::NULL, &array);
|
||||||
|
@ -117,6 +198,16 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
&"rateLimitWait".into(),
|
&"rateLimitWait".into(),
|
||||||
&JsValue::from(300)
|
&JsValue::from(300)
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
|
// Response filter to prevent HTML parsing errors
|
||||||
|
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
|
// Wildcard function
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
|
@ -128,9 +219,14 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap();
|
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap();
|
||||||
|
|
||||||
// Tokenizer functions from Bloodhound
|
// Tokenizer functions from Bloodhound
|
||||||
let tokenizer = js_sys::eval(r#"Bloodhound.tokenizers.whitespace"#)
|
let tokenizer = js_sys::Function::new_no_args(
|
||||||
.expect("Should get whitespace tokenizer");
|
r#"
|
||||||
|
return function(query) {
|
||||||
|
return query.trim().split(/\s+/);
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&bloodhound_options,
|
&bloodhound_options,
|
||||||
&"datumTokenizer".into(),
|
&"datumTokenizer".into(),
|
||||||
|
@ -168,20 +264,7 @@ fn initialize_typeahead(
|
||||||
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
||||||
input.set_id(&input_id);
|
input.set_id(&input_id);
|
||||||
|
|
||||||
let dataset = Object::new();
|
// Create selection handler closure
|
||||||
let bloodhound_ref = bloodhound.unchecked_ref::<Bloodhound>();
|
|
||||||
|
|
||||||
Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap();
|
|
||||||
Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap();
|
|
||||||
Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap();
|
|
||||||
|
|
||||||
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| {
|
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>() {
|
if let Ok(data) = suggestion.into_serde::<WikidataSuggestion>() {
|
||||||
|
@ -194,6 +277,7 @@ fn initialize_typeahead(
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(web_sys::Event, JsValue)>);
|
}) as Box<dyn FnMut(web_sys::Event, JsValue)>);
|
||||||
|
|
||||||
|
// Register global handler
|
||||||
let handler_name = format!("handler_{}", input_id);
|
let handler_name = format!("handler_{}", input_id);
|
||||||
js_sys::Reflect::set(
|
js_sys::Reflect::set(
|
||||||
&js_sys::global(),
|
&js_sys::global(),
|
||||||
|
@ -202,7 +286,7 @@ fn initialize_typeahead(
|
||||||
).unwrap();
|
).unwrap();
|
||||||
closure.forget();
|
closure.forget();
|
||||||
|
|
||||||
// Corrected initialization script using bracket notation for handler
|
// Initialization script
|
||||||
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 #{id}');
|
||||||
|
@ -216,30 +300,49 @@ fn initialize_typeahead(
|
||||||
}},
|
}},
|
||||||
{{
|
{{
|
||||||
name: 'suggestions',
|
name: 'suggestions',
|
||||||
source: bloodhound.ttAdapter(),
|
|
||||||
display: 'label',
|
display: 'label',
|
||||||
|
source: bloodhound.ttAdapter(),
|
||||||
templates: {{
|
templates: {{
|
||||||
suggestion: function(data) {{
|
suggestion: function(data) {{
|
||||||
console.log('[JS] Rendering suggestion', data);
|
// Handle nested Wikidata structure
|
||||||
return $('<div>').text(data.label);
|
var label = data.label || '';
|
||||||
}}
|
var description = data.description || '';
|
||||||
|
|
||||||
|
// If nested display exists, use those values
|
||||||
|
if (data.display) {{
|
||||||
|
label = data.display.label?.value || label;
|
||||||
|
description = data.display.description?.value || description;
|
||||||
|
}}
|
||||||
|
|
||||||
|
return $('<div>')
|
||||||
|
.addClass('suggestion-item')
|
||||||
|
.append($('<div>').addClass('label').text(label))
|
||||||
|
.append($('<div>').addClass('description').text(description));
|
||||||
|
}},
|
||||||
|
empty: $('<div>').addClass('empty-suggestion').text('No matches found')
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
).on('typeahead:select', function(ev, suggestion) {{
|
)
|
||||||
console.log('[JS] Selection event received');
|
.on('typeahead:asyncreceive', function(ev, dataset, suggestions) {{
|
||||||
|
console.log('[JS] Received suggestions:', suggestions);
|
||||||
|
if (suggestions && suggestions.length > 0) {{
|
||||||
|
$(this).data('ttTypeahead').dropdown.open();
|
||||||
|
}}
|
||||||
|
}})
|
||||||
|
.on('typeahead:select', function(ev, suggestion) {{
|
||||||
|
console.log('[JS] Selection data:', JSON.stringify(suggestion, null, 2));
|
||||||
window['{handler}'](ev, suggestion);
|
window['{handler}'](ev, suggestion);
|
||||||
}});
|
}});
|
||||||
console.log('[JS] Typeahead initialized successfully');
|
|
||||||
}} catch (e) {{
|
}} catch (e) {{
|
||||||
console.error('[JS] Typeahead init error:', e);
|
console.error('[JS] Typeahead init error:', e);
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
id = input_id,
|
id = input_id,
|
||||||
handler = handler_name.replace('-', "_") // Replace hyphens to avoid JS issues
|
handler = handler_name.replace('-', "_")
|
||||||
);
|
);
|
||||||
|
|
||||||
log!("[RUST] Initialization script: {}", init_script);
|
log!("[RUST] Initialization script: {}", init_script);
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue