//! `/artifact/:slug` — closing document of a cycle. use leptos::prelude::*; use leptos_router::hooks::{use_params, use_query_map}; use leptos_router::params::Params; use crate::api::{fetch_artifact, ArtifactPageData}; use crate::corpus::Lang; use crate::pages::inspector::{Anchor, Inspector, PopoverPos, SelectionPopover, Tab}; use crate::pages::shared::LangToggle; #[derive(Params, PartialEq, Clone, Debug)] struct SlugParam { slug: Option, } #[component] pub fn ArtifactPage() -> impl IntoView { let params = use_params::(); let slug = Memo::new(move |_| { params.get().ok().and_then(|p| p.slug).unwrap_or_default() }); let query = use_query_map(); let lang = Memo::new(move |_| { Lang::from_query(query.read().get("lang").as_deref()) }); let (anchor, set_anchor) = signal::>(None); let (tab, set_tab) = signal(Tab::Note); let (popover_pos, set_popover_pos) = signal::>(None); let notes_tick = RwSignal::new(0u32); let slug_sig: Signal = Signal::derive(move || slug.get()); let lang_sig: Signal = Signal::derive(move || lang.get()); Effect::new(move |_| { let _ = slug.get(); set_anchor.set(None); set_popover_pos.set(None); }); let data = Resource::new( move || (slug.get(), lang.get().as_str().to_string()), |(s, l)| fetch_artifact(s, l), ); #[cfg(feature = "hydrate")] { let slug_for_fx = slug_sig; Effect::new(move |_| { let _ = notes_tick.get(); let current_slug = slug_for_fx.get(); if current_slug.is_empty() { return; } leptos::task::spawn_local(async move { if let Ok(entries) = crate::api::fetch_notes(current_slug).await { let ids: std::collections::HashSet = entries.into_iter().map(|e| e.para_id).collect(); annotate_notes(&ids); } }); }); } view! {
"…"

}> {move || { data.get().map(|res| { let lang_val = lang.get(); let home_href = format!("/?lang={}", lang_val.as_str()); let label_back = match lang_val { Lang::Ru => "все источники", Lang::En => "all sources", }; match res { Ok(Some(ArtifactPageData { artifact })) => view! {
}.into_any(), Ok(None) => view! {

"artifact not found"

"← back"
}.into_any(), Err(e) => view! {

{format!("{e}")}

}.into_any(), } }) }}
} } #[cfg(feature = "hydrate")] fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option { use wasm_bindgen::JsCast; let target = ev.target()?; let mut el = target.dyn_into::().ok()?; loop { if el.has_attribute("data-para-id") { let id = el.get_attribute("data-para-id")?; let text = el.text_content().unwrap_or_default(); let excerpt = text.trim().to_string(); return Some(Anchor { para_id: id, excerpt, is_selection: false, }); } el = el.parent_element()?; } } #[cfg(not(feature = "hydrate"))] fn paragraph_from_event(_ev: &leptos::ev::MouseEvent) -> Option { None } #[cfg(feature = "hydrate")] fn handle_mouseup( _ev: &leptos::ev::MouseEvent, set_pos: &WriteSignal>, ) { let Some(win) = web_sys::window() else { return }; let Ok(Some(sel)) = win.get_selection() else { set_pos.set(None); return; }; if sel.is_collapsed() { set_pos.set(None); return; } let text = match sel.to_string().as_string() { Some(s) => s.trim().to_string(), None => { set_pos.set(None); return; } }; if text.len() < 2 { set_pos.set(None); return; } let Ok(range) = sel.get_range_at(0) else { set_pos.set(None); return; }; let rect = range.get_bounding_client_rect(); let start_node = range.start_container().ok(); let para_id = start_node.as_ref().and_then(find_para_id_from_node); let Some(para_id) = para_id else { set_pos.set(None); return; }; set_pos.set(Some(PopoverPos { x: rect.left() + rect.width() / 2.0, y: rect.top() - 8.0, anchor: Anchor { para_id, excerpt: text, is_selection: true }, })); } #[cfg(not(feature = "hydrate"))] fn handle_mouseup( _ev: &leptos::ev::MouseEvent, _set_pos: &WriteSignal>, ) {} #[cfg(feature = "hydrate")] fn find_para_id_from_node(node: &web_sys::Node) -> Option { use wasm_bindgen::JsCast; let mut current: Option = node .dyn_ref::() .cloned() .or_else(|| node.parent_element()); while let Some(el) = current { if el.has_attribute("data-para-id") { return el.get_attribute("data-para-id"); } current = el.parent_element(); } None } #[cfg(feature = "hydrate")] fn annotate_notes(ids: &std::collections::HashSet) { use wasm_bindgen::JsCast; let Some(win) = web_sys::window() else { return }; let Some(doc) = win.document() else { return }; let Ok(nodes) = doc.query_selector_all("[data-para-id]") else { return }; for i in 0..nodes.length() { let Some(node) = nodes.item(i) else { continue }; let Ok(el) = node.dyn_into::() else { continue }; let id = el.get_attribute("data-para-id").unwrap_or_default(); let classes = el.class_list(); if ids.contains(&id) { let _ = classes.add_1("has-notes"); } else { let _ = classes.remove_1("has-notes"); } } }