feat(typeahead): use typeahead for name input field
This commit is contained in:
parent
4de14bb48b
commit
d6661c2ac9
9 changed files with 307 additions and 128 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -771,7 +771,9 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"gloo-net 0.5.0",
|
"gloo-net 0.5.0",
|
||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
|
"gloo-utils",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_actix",
|
"leptos_actix",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
|
|
|
@ -10,7 +10,9 @@ crate-type = ["cdylib", "rlib"]
|
||||||
actix-files = { version = "0.6", optional = true }
|
actix-files = { version = "0.6", optional = true }
|
||||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
gloo-utils = "0.2"
|
||||||
http = { version = "1.0.0", optional = true }
|
http = { version = "1.0.0", optional = true }
|
||||||
|
js-sys = "0.3"
|
||||||
leptos = { version = "0.6" }
|
leptos = { version = "0.6" }
|
||||||
leptos_meta = { version = "0.6" }
|
leptos_meta = { version = "0.6" }
|
||||||
leptos_actix = { version = "0.6", optional = true }
|
leptos_actix = { version = "0.6", optional = true }
|
||||||
|
|
|
@ -155,4 +155,52 @@ th {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typeahead-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-menu {
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-cursor {
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-hint {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-menu {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item.tt-cursor {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
|
@ -29,6 +29,9 @@ pub fn App() -> impl IntoView {
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
view! {
|
view! {
|
||||||
|
<head>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.3.1/typeahead.bundle.min.js"></script>
|
||||||
|
</head>
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/*url" view=move || {
|
<Route path="/*url" view=move || {
|
||||||
|
|
|
@ -22,19 +22,17 @@ pub fn EditableCell(
|
||||||
let new_value = match input_type_clone {
|
let new_value = match input_type_clone {
|
||||||
InputType::Text => event_target_value(&e),
|
InputType::Text => event_target_value(&e),
|
||||||
InputType::TextArea => event_target_value(&e),
|
InputType::TextArea => event_target_value(&e),
|
||||||
InputType::Search => event_target_value(&e),
|
|
||||||
};
|
};
|
||||||
log!("Input event: {}", new_value);
|
log!("Input event: {}", new_value);
|
||||||
set_local_value.set(new_value.clone());
|
set_local_value.set(new_value);
|
||||||
on_input(new_value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// // Commit the input value on blur or enter
|
// Commit the input value on blur or enter
|
||||||
// let commit_input = move || {
|
let commit_input = move || {
|
||||||
// let value = local_value.get();
|
let value = local_value.get();
|
||||||
// log!("Committing input: {}", value);
|
log!("Committing input: {}", value);
|
||||||
// on_input(value);
|
on_input(value);
|
||||||
// };
|
};
|
||||||
|
|
||||||
// Focus handling
|
// Focus handling
|
||||||
let handle_focus = {
|
let handle_focus = {
|
||||||
|
@ -51,7 +49,7 @@ pub fn EditableCell(
|
||||||
let handle_blur = move |_| {
|
let handle_blur = move |_| {
|
||||||
log!("Focus lost");
|
log!("Focus lost");
|
||||||
set_focused_cell.set(None);
|
set_focused_cell.set(None);
|
||||||
// commit_input();
|
commit_input();
|
||||||
if let Some(on_blur) = &on_blur {
|
if let Some(on_blur) = &on_blur {
|
||||||
on_blur.call(());
|
on_blur.call(());
|
||||||
}
|
}
|
||||||
|
@ -60,23 +58,10 @@ pub fn EditableCell(
|
||||||
// Update input field value when focused cell changes
|
// Update input field value when focused cell changes
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if focused_cell.get().as_deref() == Some(key.as_str()) {
|
if focused_cell.get().as_deref() == Some(key.as_str()) {
|
||||||
log!("Persisting focus for key: {}", key);
|
log!("Setting focus for key: {}", key);
|
||||||
let input_ref = input_ref.clone();
|
if let Some(input) = input_ref.get() {
|
||||||
let key_clone = key.clone();
|
let _ = input.focus();
|
||||||
|
}
|
||||||
// Use requestAnimationFrame for better focus timing
|
|
||||||
spawn_local(async move {
|
|
||||||
for _ in 0..3 { // Retry up to 3 times
|
|
||||||
gloo_timers::future::sleep(std::time::Duration::from_millis(10)).await;
|
|
||||||
if let Some(input) = input_ref.get() {
|
|
||||||
let _ = input.focus();
|
|
||||||
if document().active_element().as_ref() == Some(input.as_ref() as &web_sys::Element) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log!("Focus retry for {}", key_clone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,17 +88,6 @@ pub fn EditableCell(
|
||||||
node_ref=textarea_ref
|
node_ref=textarea_ref
|
||||||
class="editable-cell-input"
|
class="editable-cell-input"
|
||||||
/>
|
/>
|
||||||
}.into_view(),
|
|
||||||
InputType::Search => view! {
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
prop:value=move || local_value.get()
|
|
||||||
on:input=handle_input
|
|
||||||
on:focus=handle_focus
|
|
||||||
on:blur=handle_blur
|
|
||||||
node_ref=input_ref
|
|
||||||
class="editable-cell-input"
|
|
||||||
/>
|
|
||||||
}.into_view()
|
}.into_view()
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,5 +98,4 @@ pub fn EditableCell(
|
||||||
pub enum InputType {
|
pub enum InputType {
|
||||||
Text,
|
Text,
|
||||||
TextArea,
|
TextArea,
|
||||||
Search
|
|
||||||
}
|
}
|
|
@ -10,12 +10,9 @@ use std::sync::Arc;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
use crate::components::typeahead_input::TypeaheadInput;
|
||||||
struct WikidataSuggestion {
|
use crate::models::item::WikidataSuggestion;
|
||||||
id: String,
|
use leptos::html::Input;
|
||||||
label: String,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
//function to load items from database
|
//function to load items from database
|
||||||
pub async fn load_items_from_db(current_url: &str) -> Result<Vec<Item>, String> {
|
pub async fn load_items_from_db(current_url: &str) -> Result<Vec<Item>, String> {
|
||||||
|
@ -123,6 +120,7 @@ pub fn ItemsList(
|
||||||
items: ReadSignal<Vec<Item>>,
|
items: ReadSignal<Vec<Item>>,
|
||||||
set_items: WriteSignal<Vec<Item>>,
|
set_items: WriteSignal<Vec<Item>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let node_ref = create_node_ref::<Input>();
|
||||||
// State to track selected properties
|
// State to track selected properties
|
||||||
let (selected_properties, set_selected_properties) = create_signal(HashMap::<String, bool>::new());
|
let (selected_properties, set_selected_properties) = create_signal(HashMap::<String, bool>::new());
|
||||||
|
|
||||||
|
@ -143,6 +141,8 @@ pub fn ItemsList(
|
||||||
|
|
||||||
// State to manage property cache
|
// State to manage property cache
|
||||||
let (property_cache, set_property_cache) = create_signal(HashMap::<String, HashMap<String, String>>::new());
|
let (property_cache, set_property_cache) = create_signal(HashMap::<String, HashMap<String, String>>::new());
|
||||||
|
|
||||||
|
let (suggestions, set_suggestions) = create_signal(Vec::<WikidataSuggestion>::new());
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
fn get_current_url() -> String {
|
fn get_current_url() -> String {
|
||||||
use leptos::use_context;
|
use leptos::use_context;
|
||||||
|
@ -814,89 +814,49 @@ pub fn ItemsList(
|
||||||
<td>
|
<td>
|
||||||
{match property {
|
{match property {
|
||||||
"Name" => view! {
|
"Name" => view! {
|
||||||
<div class="search-container">
|
<div class="typeahead-container">
|
||||||
<EditableCell
|
<TypeaheadInput
|
||||||
value=item.name.clone()
|
value=item.name.clone()
|
||||||
on_input=move |value| {
|
fetch_suggestions=Callback::new(move |query: String| -> Vec<WikidataSuggestion> {
|
||||||
update_item_clone(index, "name", value.clone());
|
let key = format!("name-{}", index);
|
||||||
// Debounce with proper async handling
|
fetch_wikidata_suggestions(key.clone(), query.clone());
|
||||||
spawn_local({
|
|
||||||
let key = format!("name-{}", index);
|
// Add a small delay to ensure state is updated
|
||||||
let value = value.clone();
|
let suggestions = wikidata_suggestions.get();
|
||||||
let fetch = fetch_wikidata_suggestions.clone();
|
suggestions.get(&key).cloned().unwrap_or_default()
|
||||||
async move {
|
})
|
||||||
gloo_timers::future::sleep(std::time::Duration::from_millis(300)).await;
|
on_select=Callback::new(move |suggestion: WikidataSuggestion| {
|
||||||
fetch(key, value);
|
set_items.update(|items| {
|
||||||
|
if let Some(item) = items.get_mut(index) {
|
||||||
|
item.name = suggestion.label.clone();
|
||||||
|
item.description = suggestion.description.clone().unwrap_or_default();
|
||||||
|
item.wikidata_id = Some(suggestion.id.clone());
|
||||||
|
|
||||||
|
// Automatically fetch properties when Wikidata ID is set
|
||||||
|
if let Some(wikidata_id) = &item.wikidata_id {
|
||||||
|
spawn_local({
|
||||||
|
let set_property_labels = set_property_labels.clone();
|
||||||
|
let property_cache = property_cache.clone();
|
||||||
|
let set_property_cache = set_property_cache.clone();
|
||||||
|
let property_labels = property_labels.clone();
|
||||||
|
let wikidata_id = wikidata_id.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
fetch_item_properties(
|
||||||
|
&wikidata_id,
|
||||||
|
set_property_labels,
|
||||||
|
property_cache,
|
||||||
|
set_property_cache,
|
||||||
|
property_labels
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
key=Arc::new(format!("name-{}", index))
|
|
||||||
focused_cell=focused_cell
|
|
||||||
set_focused_cell=set_focused_cell.clone()
|
|
||||||
on_focus=Some(Callback::new(move |_| {
|
|
||||||
set_show_suggestions.update(|suggestions| {
|
|
||||||
suggestions.insert(format!("name-{}", index), true);
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
on_blur=Some(Callback::new(move |_| {
|
|
||||||
spawn_local(async move {
|
|
||||||
set_show_suggestions.update(|suggestions| {
|
|
||||||
suggestions.insert(format!("name-{}", index), false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
input_type=InputType::Search
|
|
||||||
/>
|
|
||||||
{move || {
|
|
||||||
if *show_suggestions.get().get(&format!("name-{}", index)).unwrap_or(&false) {
|
|
||||||
view! {
|
|
||||||
<div class="suggestions-container">
|
|
||||||
<ul class="suggestions-list">
|
|
||||||
{move || {
|
|
||||||
let suggestions = wikidata_suggestions.get()
|
|
||||||
.get(&format!("name-{}", index))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
suggestions.into_iter().map(|suggestion| {
|
|
||||||
let label = suggestion.label.clone();
|
|
||||||
let description = suggestion.description.clone().unwrap_or_default();
|
|
||||||
let id = suggestion.id.clone();
|
|
||||||
|
|
||||||
// Clone values for the closure
|
|
||||||
let closure_label = label.clone();
|
|
||||||
let closure_description = description.clone();
|
|
||||||
let closure_id = id.clone();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<li
|
|
||||||
class="suggestion-item"
|
|
||||||
on:click=move |_| {
|
|
||||||
set_items.update(|items| {
|
|
||||||
if let Some(item) = items.get_mut(index) {
|
|
||||||
item.name = closure_label.clone();
|
|
||||||
item.description = closure_description.clone();
|
|
||||||
item.wikidata_id = Some(closure_id.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
set_show_suggestions.update(|s| {
|
|
||||||
s.insert(format!("name-{}", index), false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="suggestion-title">{label}</div>
|
|
||||||
<div class="suggestion-description">{description}</div>
|
|
||||||
// <div class="suggestion-id">{id}</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}).collect::<Vec<_>>()
|
|
||||||
}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
view! { <div></div> }
|
})
|
||||||
}
|
node_ref=node_ref
|
||||||
}}
|
/>
|
||||||
</div>
|
</div>
|
||||||
}.into_view(),
|
}.into_view(),
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod items_list;
|
pub mod items_list;
|
||||||
pub mod editable_cell;
|
pub mod editable_cell;
|
||||||
|
pub mod typeahead_input;
|
183
src/components/typeahead_input.rs
Normal file
183
src/components/typeahead_input.rs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
use leptos::*;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use crate::models::item::WikidataSuggestion;
|
||||||
|
use js_sys::{Object, Array, Function, JSON, Reflect};
|
||||||
|
use leptos::html::Input;
|
||||||
|
use gloo_utils::format::JsValueSerdeExt;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::HtmlInputElement;
|
||||||
|
use leptos::logging::log;
|
||||||
|
#[component]
|
||||||
|
pub fn TypeaheadInput(
|
||||||
|
value: String,
|
||||||
|
on_select: Callback<WikidataSuggestion>,
|
||||||
|
fetch_suggestions: Callback<String, Vec<WikidataSuggestion>>,
|
||||||
|
node_ref: NodeRef<Input>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let (is_initialized, set_initialized) = create_signal(false);
|
||||||
|
|
||||||
|
create_effect(move |_| {
|
||||||
|
if let (Some(input), false) = (node_ref.get(), is_initialized.get()) {
|
||||||
|
let bloodhound = initialize_bloodhound(fetch_suggestions.clone());
|
||||||
|
initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone());
|
||||||
|
set_initialized.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="typeahead"
|
||||||
|
prop:value=value
|
||||||
|
node_ref=node_ref
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_name = "Bloodhound")]
|
||||||
|
type Bloodhound;
|
||||||
|
|
||||||
|
#[wasm_bindgen(constructor, js_namespace = window)]
|
||||||
|
fn new(options: &JsValue) -> Bloodhound;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
fn initialize(this: &Bloodhound, prefetch: bool);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = "ttAdapter")]
|
||||||
|
fn tt_adapter(this: &Bloodhound) -> JsValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue {
|
||||||
|
let bloodhound_options = Object::new();
|
||||||
|
|
||||||
|
// Store the Closure in a variable to prevent premature garbage collection
|
||||||
|
let remote_fn = Closure::wrap(Box::new(move |query: String, sync: js_sys::Function| {
|
||||||
|
log!("Fetching suggestions for: {}", query);
|
||||||
|
let suggestions = fetch.call(query.clone());
|
||||||
|
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();
|
||||||
|
array.push(&obj);
|
||||||
|
}
|
||||||
|
sync.call1(&JsValue::NULL, &array).unwrap();
|
||||||
|
}) as Box<dyn Fn(String, js_sys::Function)>);
|
||||||
|
|
||||||
|
// Configure Bloodhound
|
||||||
|
let remote_config = Object::new();
|
||||||
|
Reflect::set(&remote_config, &"url".into(), &"".into()).unwrap();
|
||||||
|
Reflect::set(&remote_config, &"wildcard".into(), &"%QUERY".into()).unwrap();
|
||||||
|
Reflect::set(&remote_config, &"prepare".into(), &remote_fn.as_ref()).unwrap();
|
||||||
|
Reflect::set(&remote_config, &"rateLimitWait".into(), &JsValue::from(300)).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("whitespace")).unwrap();
|
||||||
|
|
||||||
|
let bloodhound = Bloodhound::new(&bloodhound_options.into());
|
||||||
|
bloodhound.initialize(true);
|
||||||
|
|
||||||
|
// Prevent Closure from being dropped
|
||||||
|
remote_fn.forget();
|
||||||
|
|
||||||
|
bloodhound.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_typeahead(
|
||||||
|
input: &HtmlInputElement,
|
||||||
|
bloodhound: JsValue,
|
||||||
|
on_select: Callback<WikidataSuggestion>,
|
||||||
|
node_ref: NodeRef<Input>,
|
||||||
|
) {
|
||||||
|
// input event handler for direct typing
|
||||||
|
let node_ref_clone = node_ref.clone();
|
||||||
|
let input_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Create proper templates
|
||||||
|
let templates = Object::new();
|
||||||
|
Reflect::set(&templates, &"suggestion".into(), &js_sys::eval(r#"
|
||||||
|
function(data) {
|
||||||
|
return '<div class="suggestion-item">' +
|
||||||
|
'<strong>' + data.label + '</strong>' +
|
||||||
|
(data.description ? '<br><small>' + data.description + '</small>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
"#).unwrap()).unwrap();
|
||||||
|
|
||||||
|
Reflect::set(&dataset, &"templates".into(), &templates).unwrap();
|
||||||
|
|
||||||
|
// Typeahead initialization using jQuery
|
||||||
|
let init_script = format!(
|
||||||
|
r#"(function() {{
|
||||||
|
$('#{}').typeahead({}, {});
|
||||||
|
}})"#,
|
||||||
|
input.id(),
|
||||||
|
JSON::stringify(&typeahead_options).unwrap(),
|
||||||
|
JSON::stringify(&dataset).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();
|
||||||
|
}
|
|
@ -10,3 +10,10 @@ pub struct Item {
|
||||||
pub wikidata_id: Option<String>,
|
pub wikidata_id: Option<String>,
|
||||||
pub custom_properties: HashMap<String, String>,
|
pub custom_properties: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub struct WikidataSuggestion {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue