feat(typeahead): Improve typeahead initialization and event handling
This commit is contained in:
parent
d6661c2ac9
commit
5ca277ee80
2 changed files with 102 additions and 94 deletions
|
@ -30,6 +30,8 @@ pub fn App() -> impl IntoView {
|
||||||
// });
|
// });
|
||||||
view! {
|
view! {
|
||||||
<head>
|
<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>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.3.1/typeahead.bundle.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<Router>
|
<Router>
|
||||||
|
|
|
@ -7,6 +7,8 @@ use gloo_utils::format::JsValueSerdeExt;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use leptos::logging::log;
|
use leptos::logging::log;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TypeaheadInput(
|
pub fn TypeaheadInput(
|
||||||
value: String,
|
value: String,
|
||||||
|
@ -16,11 +18,20 @@ pub fn TypeaheadInput(
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let (is_initialized, set_initialized) = create_signal(false);
|
let (is_initialized, set_initialized) = create_signal(false);
|
||||||
|
|
||||||
create_effect(move |_| {
|
spawn_local(async move {
|
||||||
if let (Some(input), false) = (node_ref.get(), is_initialized.get()) {
|
log!("[INIT] Component mounted");
|
||||||
|
|
||||||
|
let mut retries = 0;
|
||||||
|
while retries < 10 {
|
||||||
|
if let Some(input) = node_ref.get() {
|
||||||
|
log!("[INIT] Input element found");
|
||||||
let bloodhound = initialize_bloodhound(fetch_suggestions.clone());
|
let bloodhound = initialize_bloodhound(fetch_suggestions.clone());
|
||||||
initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone());
|
initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone());
|
||||||
set_initialized.set(true);
|
set_initialized.set(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gloo_timers::future::sleep(Duration::from_millis(100)).await;
|
||||||
|
retries += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,6 +41,18 @@ pub fn TypeaheadInput(
|
||||||
class="typeahead"
|
class="typeahead"
|
||||||
prop:value=value
|
prop:value=value
|
||||||
node_ref=node_ref
|
node_ref=node_ref
|
||||||
|
on:focus=move |_| log!("[FOCUS] Name input focused")
|
||||||
|
on:blur=move |_| log!("[FOCUS] Name input blurred")
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,10 +75,12 @@ extern "C" {
|
||||||
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();
|
||||||
|
|
||||||
// Store the Closure in a variable to prevent premature garbage collection
|
// Configure Bloodhound remote with proper parameters
|
||||||
let remote_fn = Closure::wrap(Box::new(move |query: String, sync: js_sys::Function| {
|
let remote_fn = Closure::wrap(Box::new(move |query: String, sync: Function| {
|
||||||
log!("Fetching suggestions for: {}", query);
|
log!("[BLOODHOUND] Fetching suggestions for: {}", query);
|
||||||
let suggestions = fetch.call(query.clone());
|
let suggestions = fetch.call(query.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();
|
||||||
|
@ -63,24 +88,34 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
|
||||||
Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap();
|
Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap();
|
||||||
array.push(&obj);
|
array.push(&obj);
|
||||||
}
|
}
|
||||||
sync.call1(&JsValue::NULL, &array).unwrap();
|
|
||||||
}) as Box<dyn Fn(String, js_sys::Function)>);
|
|
||||||
|
|
||||||
// Configure Bloodhound
|
sync.call1(&JsValue::NULL, &array).unwrap();
|
||||||
|
}) as Box<dyn Fn(String, Function)>);
|
||||||
|
|
||||||
let remote_config = Object::new();
|
let remote_config = Object::new();
|
||||||
Reflect::set(&remote_config, &"url".into(), &"".into()).unwrap();
|
Reflect::set(
|
||||||
Reflect::set(&remote_config, &"wildcard".into(), &"%QUERY".into()).unwrap();
|
&remote_config,
|
||||||
Reflect::set(&remote_config, &"prepare".into(), &remote_fn.as_ref()).unwrap();
|
&"prepare".into(),
|
||||||
Reflect::set(&remote_config, &"rateLimitWait".into(), &JsValue::from(300)).unwrap();
|
&js_sys::eval(&format!(
|
||||||
|
"function(query, callback) {{
|
||||||
|
return {}(query, callback);
|
||||||
|
}}",
|
||||||
|
remote_fn.as_ref().unchecked_ref::<js_sys::Function>().to_string()
|
||||||
|
)).unwrap()
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
Reflect::set(
|
||||||
|
&remote_config,
|
||||||
|
&"wildcard".into(),
|
||||||
|
&JsValue::from_str("%QUERY")
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap();
|
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap();
|
||||||
Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from("whitespace")).unwrap();
|
Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from_str("whitespace")).unwrap();
|
||||||
Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from("whitespace")).unwrap();
|
Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from_str("whitespace")).unwrap();
|
||||||
|
|
||||||
let bloodhound = Bloodhound::new(&bloodhound_options.into());
|
let bloodhound = Bloodhound::new(&bloodhound_options.into());
|
||||||
bloodhound.initialize(true);
|
bloodhound.initialize(true);
|
||||||
|
|
||||||
// Prevent Closure from being dropped
|
|
||||||
remote_fn.forget();
|
remote_fn.forget();
|
||||||
|
|
||||||
bloodhound.into()
|
bloodhound.into()
|
||||||
|
@ -92,92 +127,63 @@ fn initialize_typeahead(
|
||||||
on_select: Callback<WikidataSuggestion>,
|
on_select: Callback<WikidataSuggestion>,
|
||||||
node_ref: NodeRef<Input>,
|
node_ref: NodeRef<Input>,
|
||||||
) {
|
) {
|
||||||
// input event handler for direct typing
|
log!("[TYPEAHEAD] Initializing for input: {}", input.id());
|
||||||
let node_ref_clone = node_ref.clone();
|
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
||||||
let input_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
input.set_id(&input_id);
|
||||||
if let Some(input) = node_ref_clone.get() {
|
|
||||||
let value = input.value();
|
|
||||||
log!("Input updated: {}", value);
|
|
||||||
// Create synthetic change event for Leptos
|
|
||||||
let event = web_sys::CustomEvent::new("input").unwrap();
|
|
||||||
input.dispatch_event(&event).unwrap();
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
input.add_event_listener_with_callback(
|
|
||||||
"input",
|
|
||||||
input_handler.as_ref().unchecked_ref()
|
|
||||||
).unwrap();
|
|
||||||
input_handler.forget();
|
|
||||||
|
|
||||||
let typeahead_options = Object::new();
|
|
||||||
Reflect::set(&typeahead_options, &"hint".into(), &JsValue::TRUE).unwrap();
|
|
||||||
Reflect::set(&typeahead_options, &"highlight".into(), &JsValue::TRUE).unwrap();
|
|
||||||
Reflect::set(&typeahead_options, &"minLength".into(), &JsValue::from(1)).unwrap();
|
|
||||||
|
|
||||||
// Bloodhound remote configuration
|
|
||||||
let bloodhound_ref = bloodhound.unchecked_ref::<Bloodhound>();
|
|
||||||
let remote_config = Object::new();
|
|
||||||
Reflect::set(&remote_config, &"prepare".into(), &JsValue::from_str("function(q) { return { q: q }; }")).unwrap();
|
|
||||||
Reflect::set(&remote_config, &"transform".into(), &js_sys::Function::new_with_args("response", "return response;")).unwrap();
|
|
||||||
|
|
||||||
// Update dataset configuration
|
|
||||||
let dataset = Object::new();
|
let dataset = Object::new();
|
||||||
|
let bloodhound_ref = bloodhound.unchecked_ref::<Bloodhound>();
|
||||||
|
|
||||||
Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap();
|
Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap();
|
||||||
Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap();
|
Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap();
|
||||||
Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap();
|
Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap();
|
||||||
|
|
||||||
// Create proper templates
|
// Create and register the closure
|
||||||
let templates = Object::new();
|
let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| {
|
||||||
Reflect::set(&templates, &"suggestion".into(), &js_sys::eval(r#"
|
log!("[TYPEAHEAD] Selection made");
|
||||||
function(data) {
|
let data: WikidataSuggestion = suggestion.into_serde().unwrap();
|
||||||
return '<div class="suggestion-item">' +
|
on_select.call(data.clone());
|
||||||
'<strong>' + data.label + '</strong>' +
|
|
||||||
(data.description ? '<br><small>' + data.description + '</small>' : '') +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
"#).unwrap()).unwrap();
|
|
||||||
|
|
||||||
Reflect::set(&dataset, &"templates".into(), &templates).unwrap();
|
if let Some(input) = node_ref.get() {
|
||||||
|
input.set_value(&data.label);
|
||||||
|
}
|
||||||
|
}) 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()
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
// Typeahead initialization using jQuery
|
// Typeahead initialization using jQuery
|
||||||
let init_script = format!(
|
let init_script = format!(
|
||||||
r#"(function() {{
|
r#"
|
||||||
$('#{}').typeahead({}, {});
|
(function() {{
|
||||||
}})"#,
|
console.log('[TYPEAHEAD] Initializing for #{id}');
|
||||||
input.id(),
|
$('#{id}').typeahead(
|
||||||
JSON::stringify(&typeahead_options).unwrap(),
|
{{
|
||||||
JSON::stringify(&dataset).unwrap()
|
hint: true,
|
||||||
|
highlight: true,
|
||||||
|
minLength: 1
|
||||||
|
}},
|
||||||
|
{dataset}
|
||||||
|
).on('typeahead:select', function(ev, suggestion) {{
|
||||||
|
console.log('[TYPEAHEAD] Select event triggered');
|
||||||
|
{handler}(ev, suggestion);
|
||||||
|
}});
|
||||||
|
console.log('[TYPEAHEAD] Initialization complete for #{id}');
|
||||||
|
}})();
|
||||||
|
"#,
|
||||||
|
id = input_id,
|
||||||
|
dataset = JSON::stringify(&dataset).unwrap(),
|
||||||
|
handler = handler_name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log!("[TYPEAHEAD] Init script: {}", init_script);
|
||||||
let _ = js_sys::eval(&init_script).unwrap();
|
let _ = js_sys::eval(&init_script).unwrap();
|
||||||
|
|
||||||
// Handle selection events
|
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
||||||
if let Some(selected) = event.target() {
|
|
||||||
let js_value = selected.unchecked_into::<JsValue>();
|
|
||||||
let data: WikidataSuggestion = js_sys::JSON::parse(
|
|
||||||
&js_sys::JSON::stringify(&js_value)
|
|
||||||
.unwrap()
|
|
||||||
.as_string()
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
).unwrap()
|
|
||||||
.into_serde()
|
|
||||||
.unwrap();
|
|
||||||
let data_clone = data.clone();
|
|
||||||
|
|
||||||
on_select.call(data);
|
|
||||||
// Explicitly update the input value
|
|
||||||
if let Some(input) = node_ref.get() {
|
|
||||||
input.set_value(&data_clone.label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
input.add_event_listener_with_callback(
|
|
||||||
"typeahead:select",
|
|
||||||
closure.as_ref().unchecked_ref()
|
|
||||||
).unwrap();
|
|
||||||
closure.forget();
|
closure.forget();
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue