Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
f35c7cd085 |
8 changed files with 107 additions and 360 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -771,9 +771,7 @@ dependencies = [
|
|||
"futures",
|
||||
"gloo-net 0.5.0",
|
||||
"gloo-timers",
|
||||
"gloo-utils",
|
||||
"http 1.2.0",
|
||||
"js-sys",
|
||||
"leptos",
|
||||
"leptos_actix",
|
||||
"leptos_meta",
|
||||
|
|
|
@ -10,9 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
|||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo-utils = "0.2"
|
||||
http = { version = "1.0.0", optional = true }
|
||||
js-sys = "0.3"
|
||||
leptos = { version = "0.6" }
|
||||
leptos_meta = { version = "0.6" }
|
||||
leptos_actix = { version = "0.6", optional = true }
|
||||
|
|
|
@ -156,51 +156,3 @@ th {
|
|||
resize: vertical;
|
||||
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,10 +29,6 @@ pub fn App() -> impl IntoView {
|
|||
// }
|
||||
// });
|
||||
view! {
|
||||
<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>
|
||||
</head>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/*url" view=move || {
|
||||
|
|
|
@ -10,9 +10,12 @@ use std::sync::Arc;
|
|||
use wasm_bindgen::JsCast;
|
||||
use std::rc::Rc;
|
||||
use urlencoding::encode;
|
||||
use crate::components::typeahead_input::TypeaheadInput;
|
||||
use crate::models::item::WikidataSuggestion;
|
||||
use leptos::html::Input;
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct WikidataSuggestion {
|
||||
id: String,
|
||||
label: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
//function to load items from database
|
||||
pub async fn load_items_from_db(current_url: &str) -> Result<Vec<Item>, String> {
|
||||
|
@ -120,7 +123,6 @@ pub fn ItemsList(
|
|||
items: ReadSignal<Vec<Item>>,
|
||||
set_items: WriteSignal<Vec<Item>>,
|
||||
) -> impl IntoView {
|
||||
let node_ref = create_node_ref::<Input>();
|
||||
// State to track selected properties
|
||||
let (selected_properties, set_selected_properties) = create_signal(HashMap::<String, bool>::new());
|
||||
|
||||
|
@ -141,8 +143,6 @@ pub fn ItemsList(
|
|||
|
||||
// State to manage property cache
|
||||
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")]
|
||||
fn get_current_url() -> String {
|
||||
use leptos::use_context;
|
||||
|
@ -814,52 +814,108 @@ pub fn ItemsList(
|
|||
<td>
|
||||
{match property {
|
||||
"Name" => view! {
|
||||
<div class="typeahead-container">
|
||||
<TypeaheadInput
|
||||
<div class="editable-cell">
|
||||
<EditableCell
|
||||
value=item.name.clone()
|
||||
fetch_suggestions=Callback::new(move |query: String| -> Vec<WikidataSuggestion> {
|
||||
let key = format!("name-{}", index);
|
||||
fetch_wikidata_suggestions(key.clone(), query.clone());
|
||||
|
||||
// Add a small delay to ensure state is updated
|
||||
let suggestions = wikidata_suggestions.get();
|
||||
suggestions.get(&key).cloned().unwrap_or_default()
|
||||
})
|
||||
on_select=Callback::new(move |suggestion: WikidataSuggestion| {
|
||||
on_input=move |value| {
|
||||
update_item_clone(index, "name", value.clone());
|
||||
fetch_wikidata_suggestions(format!("name-{}", index), value);
|
||||
}
|
||||
key=Arc::new(format!("name-{}", index))
|
||||
focused_cell=focused_cell
|
||||
set_focused_cell=set_focused_cell.clone()
|
||||
on_focus=Some(Callback::new(move |_| {
|
||||
log!("Input focused, showing suggestions");
|
||||
set_show_suggestions.update(|suggestions| {
|
||||
suggestions.insert(format!("name-{}", index), true);
|
||||
});
|
||||
}))
|
||||
on_blur=Some(Callback::new(move |_| {
|
||||
log!("Input blurred, delaying hiding suggestions");
|
||||
spawn_local(async move {
|
||||
gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
|
||||
log!("Hiding suggestions after delay");
|
||||
set_show_suggestions.update(|suggestions| {
|
||||
suggestions.insert(format!("name-{}", index), false);
|
||||
});
|
||||
});
|
||||
}))
|
||||
input_type=InputType::Text
|
||||
/>
|
||||
<button class="search-icon" on:click=move |_| {
|
||||
log!("Search icon clicked, showing suggestions");
|
||||
set_show_suggestions.update(|suggestions| {
|
||||
suggestions.insert(format!("name-{}", index), true);
|
||||
});
|
||||
}>
|
||||
<i class="fas fa-search"></i> Search Wiki
|
||||
</button>
|
||||
{move || {
|
||||
if *show_suggestions.get().get(&format!("name-{}", index)).unwrap_or(&false) {
|
||||
log!("Rendering suggestions list");
|
||||
view! {
|
||||
<ul class="editable-cell-suggestions">
|
||||
{move || {
|
||||
let suggestions = wikidata_suggestions.get()
|
||||
.get(&format!("name-{}", index))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
log!("Suggestions for cell {}: {:?}", index, suggestions);
|
||||
suggestions.into_iter().map(|suggestion| {
|
||||
let label_for_click = suggestion.label.clone();
|
||||
let label_for_display = suggestion.label.clone();
|
||||
let description_for_click = suggestion.description.clone().unwrap_or_default();
|
||||
let description_for_display = suggestion.description.clone().unwrap_or_default();
|
||||
let id = suggestion.id.clone();
|
||||
view! {
|
||||
<li class="editable-cell-suggestions-li" on:click=move |_| {
|
||||
// Update item with basic suggestion details
|
||||
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;
|
||||
item.description = description_for_click.clone();
|
||||
item.wikidata_id = Some(id.clone());
|
||||
item.name = label_for_click.clone();
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch additional properties from Wikidata
|
||||
let wikidata_id = id.clone();
|
||||
spawn_local(async move {
|
||||
let properties = fetch_item_properties(&wikidata_id, set_property_labels.clone(), property_cache.clone(), set_property_cache.clone(), property_labels.clone()).await;
|
||||
// log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
|
||||
|
||||
// Populate the custom properties for the new item
|
||||
set_items.update(|items| {
|
||||
if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref() == Some(&wikidata_id)) {
|
||||
for (property, value) in properties {
|
||||
item.custom_properties.insert(property, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
node_ref=node_ref
|
||||
/>
|
||||
});
|
||||
|
||||
// Hide the suggestion list
|
||||
set_show_suggestions.update(|suggestions| {
|
||||
suggestions.insert(format!("name-{}", index), false);
|
||||
log!("Updated show_suggestions: {:?}", suggestions);
|
||||
});
|
||||
}>
|
||||
{ format!("{} - {}", label_for_display, description_for_display) }
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}}
|
||||
</ul>
|
||||
}
|
||||
} else {
|
||||
log!("Suggestions list hidden");
|
||||
view! {
|
||||
<ul></ul>
|
||||
}
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
}.into_view(),
|
||||
|
||||
"Description" => view! {
|
||||
<EditableCell
|
||||
value=item.description.clone()
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
pub mod items_list;
|
||||
pub mod editable_cell;
|
||||
pub mod typeahead_input;
|
|
@ -1,245 +0,0 @@
|
|||
use leptos::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use crate::models::item::WikidataSuggestion;
|
||||
use js_sys::{Object, Array, Function, Reflect};
|
||||
use leptos::html::Input;
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
use leptos::logging::log;
|
||||
use std::time::Duration;
|
||||
|
||||
#[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);
|
||||
|
||||
spawn_local(async move {
|
||||
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());
|
||||
|
||||
// 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;
|
||||
}
|
||||
gloo_timers::future::sleep(Duration::from_millis(100)).await;
|
||||
retries += 1;
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<input
|
||||
type="text"
|
||||
class="typeahead"
|
||||
prop:value=value
|
||||
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);
|
||||
let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)");
|
||||
let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')");
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
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_or_default();
|
||||
Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap_or_default();
|
||||
array.push(&obj);
|
||||
}
|
||||
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(),
|
||||
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(),
|
||||
&JsValue::from_str("%QUERY")
|
||||
).unwrap();
|
||||
|
||||
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).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,
|
||||
on_select: Callback<WikidataSuggestion>,
|
||||
node_ref: NodeRef<Input>,
|
||||
) {
|
||||
log!("[TYPEAHEAD] Initializing for input: {}", input.id());
|
||||
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
|
||||
input.set_id(&input_id);
|
||||
|
||||
let dataset = Object::new();
|
||||
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| {
|
||||
log!("[TYPEAHEAD] Selection made");
|
||||
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)>);
|
||||
|
||||
let handler_name = format!("handler_{}", input_id);
|
||||
js_sys::Reflect::set(
|
||||
&js_sys::global(),
|
||||
&handler_name.clone().into(),
|
||||
closure.as_ref(),
|
||||
).unwrap();
|
||||
closure.forget();
|
||||
|
||||
// Corrected initialization script using bracket notation for handler
|
||||
let init_script = format!(
|
||||
r#"
|
||||
console.log('[JS] Starting Typeahead init for #{id}');
|
||||
try {{
|
||||
var bloodhound = window.bloodhoundInstance;
|
||||
$('#{id}').typeahead(
|
||||
{{
|
||||
hint: true,
|
||||
highlight: true,
|
||||
minLength: 1
|
||||
}},
|
||||
{{
|
||||
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('[JS] Selection event received');
|
||||
window['{handler}'](ev, suggestion);
|
||||
}});
|
||||
console.log('[JS] Typeahead initialized successfully');
|
||||
}} catch (e) {{
|
||||
console.error('[JS] Typeahead init error:', e);
|
||||
}}
|
||||
"#,
|
||||
id = input_id,
|
||||
handler = handler_name.replace('-', "_") // Replace hyphens to avoid JS issues
|
||||
);
|
||||
|
||||
log!("[RUST] Initialization script: {}", init_script);
|
||||
if let Err(e) = js_sys::eval(&init_script) {
|
||||
log!("[RUST] Eval error: {:?}", e);
|
||||
}
|
||||
}
|
|
@ -10,10 +10,3 @@ pub struct Item {
|
|||
pub wikidata_id: Option<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