ftml_viewer_components/components/
navigation.rs1use flams_ontology::uris::DocumentURI;
2use flams_utils::prelude::HMap;
3use leptos::prelude::*;
4use web_sys::Element;
5
6#[derive(Debug, Clone)]
7pub enum SectionOrInputref {
8 Section,
9 Inputref(RwSignal<bool>, RwSignal<bool>),
10}
11
12pub struct NavElems {
13 pub initialized: RwSignal<bool>,
14 pub ids: HMap<String, SectionOrInputref>,
15 pub titles: HMap<DocumentURI, RwSignal<String>>,
16}
17impl NavElems {
18 pub fn get_title(&mut self, uri: DocumentURI) -> RwSignal<String> {
19 match self.titles.entry(uri) {
20 std::collections::hash_map::Entry::Occupied(e) => *e.get(),
21 std::collections::hash_map::Entry::Vacant(e) => {
22 let name = e.key().name().to_string();
23 *e.insert(RwSignal::new(name))
24 }
25 }
26 }
27 pub fn set_title(&mut self, uri: DocumentURI, title: String) {
28 match self.titles.entry(uri) {
29 std::collections::hash_map::Entry::Occupied(e) => e.get().set(title),
30 std::collections::hash_map::Entry::Vacant(e) => {
31 e.insert(RwSignal::new(title));
32 }
33 }
34 }
35 pub fn update_untracked<R>(f: impl FnOnce(&mut Self) -> R) -> R {
36 expect_context::<RwSignal<Self>>().update_untracked(f)
37 }
38 pub fn with_untracked<R>(f: impl FnOnce(&Self) -> R) -> R {
39 expect_context::<RwSignal<Self>>()
40 .try_with_untracked(f)
41 .expect("this should not happen")
42 }
43 pub fn navigate_to(&self, id: &str) {
44 #[cfg(any(feature = "csr", feature = "hydrate"))]
45 {
46 tracing::trace!("Looking for #{id}");
47 let mut curr = id;
48 loop {
49 match self.ids.get(curr) {
50 None => (),
51 Some(SectionOrInputref::Section) => {
52 tracing::trace!("Navigating to #{curr}");
53 if let Some(e) = document().get_element_by_id(curr) {
54 let options = leptos::web_sys::ScrollIntoViewOptions::new();
55 options.set_behavior(leptos::web_sys::ScrollBehavior::Smooth);
56 options.set_block(leptos::web_sys::ScrollLogicalPosition::Start);
57 e.scroll_into_view_with_scroll_into_view_options(&options);
58 }
59 return;
60 }
61 Some(SectionOrInputref::Inputref(s1, s2)) => {
62 if !s2.get_untracked() {
63 s1.set(true);
64 if s2.get() {
65 return self.navigate_to(id);
66 }
67 }
68 }
69 }
70 if let Some((a, _)) = curr.rsplit_once('/') {
71 curr = a;
72 } else {
73 return;
74 }
75 }
76 }
77 }
78}
79
80fn get_anchor(e: Element) -> Option<Element> {
81 let mut curr = e;
82 loop {
83 if curr.tag_name().to_uppercase() == "A" {
84 return Some(curr);
85 }
86 if curr.tag_name().to_uppercase() == "BODY" {
87 return None;
88 }
89 if let Some(parent) = curr.parent_element() {
90 curr = parent;
91 } else {
92 return None;
93 }
94 }
95}
96
97#[derive(Copy, Clone, Debug)]
98pub struct URLFragment(RwSignal<String>);
99impl URLFragment {
100 pub fn new() -> Self {
101 use leptos::wasm_bindgen::JsCast;
102 #[cfg(any(feature = "csr", feature = "hydrate"))]
103 let signal = RwSignal::new(
104 window()
105 .location()
106 .hash()
107 .ok()
108 .map(|s| s.strip_prefix('#').map(ToString::to_string).unwrap_or(s))
109 .unwrap_or_default(),
110 );
111 #[cfg(not(any(feature = "csr", feature = "hydrate")))]
112 let signal = RwSignal::new(String::new());
113
114 #[cfg(feature = "csr")]
115 {
116 let on_hash_change =
117 wasm_bindgen::prelude::Closure::wrap(Box::new(move |_e: leptos::web_sys::Event| {
118 let s = window()
119 .location()
120 .hash()
121 .ok()
122 .map(|s| s.strip_prefix('#').map(ToString::to_string).unwrap_or(s))
123 .unwrap_or_default();
124 tracing::trace!("Updating URL fragment to {s}");
125 signal.set(s);
126 }) as Box<dyn FnMut(_)>);
127
128 let on_anchor_click = wasm_bindgen::prelude::Closure::wrap(Box::new(
129 move |e: leptos::web_sys::MouseEvent| {
130 if let Some(e) = e.target().and_then(|t| t.dyn_into::<Element>().ok()) {
131 if let Some(e) = get_anchor(e) {
132 if let Some(href) = e.get_attribute("href") {
133 if let Some(href) = href.strip_prefix('#') {
134 tracing::trace!("Updating URL fragment as {href}");
135 signal.set(href.to_string());
136 }
137 }
138 }
139 }
140 },
141 )
142 as Box<dyn FnMut(_)>);
143
144 let _ = window().add_event_listener_with_callback(
145 "hashchange",
146 on_hash_change.as_ref().unchecked_ref(),
147 );
148 let _ = window().add_event_listener_with_callback(
149 "popstate",
150 on_hash_change.as_ref().unchecked_ref(),
151 );
152 let _ = window().add_event_listener_with_callback(
153 "click",
154 on_anchor_click.as_ref().unchecked_ref(),
155 );
156 on_hash_change.forget();
157 on_anchor_click.forget();
158 }
159 Self(signal)
160 }
161}
162
163#[component]
164pub fn Nav() -> impl IntoView {
165 use flams_web_utils::components::ClientOnly;
166 view!(<ClientOnly>{move || {
167 tracing::trace!("Checking URL fragment");
168 let fragment = expect_context::<URLFragment>();
169 let s = fragment.0.get();
170 if !s.is_empty() {
171 NavElems::with_untracked(|e| e.navigate_to(&s));
172 }
173 }}</ClientOnly>)
174}