ftml_viewer_components/components/
navigation.rs

1use 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}