use super::*;
#[component]
pub(crate) fn docs_main(node: VirtualNode<DocsPageProps>) -> VirtualNode {
let DocsPageProps { route_signal }: DocsPageProps = node.try_get_props().unwrap_or_default();
let (path, _anchor) = parse_route(&route_signal.get());
match find_page(&path) {
Some(page) if page.home => html! {
div {
key: path.clone()
class: c_euv_display_contents()
docs_home_page {
route_signal
}
}
},
Some(page) if page.private && !is_unlocked(&path) => html! {
div {
key: path.clone()
class: c_euv_display_contents()
docs_password_gate {
route: page.route
expected_hash: page.password_hash
title: page.title
}
}
},
Some(_) => html! {
div {
key: path.clone()
class: c_euv_display_contents()
docs_doc_page {
route_signal
}
}
},
None => html! {
div {
key: path.clone()
class: c_euv_display_contents()
docs_not_found {
route_signal
}
}
},
}
}
#[component]
pub(crate) fn docs_doc_page(node: VirtualNode<DocsPageProps>) -> VirtualNode {
let DocsPageProps { route_signal }: DocsPageProps = node.try_get_props().unwrap_or_default();
let toc_active: Signal<String> = App::use_signal(String::new);
use_toc_scroll_spy(toc_active);
let (path, _anchor) = parse_route(&route_signal.get());
let locale: &DocsLocale = locale_of(&path);
let Some(page) = find_page(&path) else {
return html! {
""
};
};
let (prev, next) = prev_next(locale, page.route);
let footer_text: &str = if page.footer.is_empty() {
locale.footer
} else {
page.footer
};
html! {
euv_doc_layout {
toc_title: locale.toc_label
toc_items: page.headings
toc_active: toc_active
prev_label: locale.prev_label
next_label: locale.next_label
prev: prev
next: next
footer: footer_text
if { !page.title.is_empty() } {
h1 {
class: CLASS_DOCS_PAGE_TITLE
{
page.title
}
}
}
euv_markdown {
blocks: page.blocks
}
}
}
}
fn use_toc_scroll_spy(active: Signal<String>) {
let seen_hash: Signal<String> = App::use_signal(|| {
window()
.and_then(|window: Window| window.location().hash().ok())
.unwrap_or_default()
});
let scroll_at_hash: Signal<i32> = App::use_signal(|| 0);
App::use_interval(TOC_SPY_INTERVAL_MILLIS, move || {
let Some(window) = window() else {
return;
};
let Some(document) = window.document() else {
return;
};
let Ok(Some(container)) = document.query_selector(TOC_SPY_CONTAINER_SELECTOR) else {
return;
};
let hash: String = window.location().hash().unwrap_or_default();
let scroll_top: i32 = container.scroll_top();
if hash != seen_hash.get() {
seen_hash.set(hash.clone());
scroll_at_hash.set(scroll_top);
}
let Some(entries) = toc_entry_hrefs(&document) else {
return;
};
let fragment_pick: Option<String> = if scroll_top == scroll_at_hash.get() {
fragment_target(&window, &entries)
} else {
None
};
let Some(next) = fragment_pick.or_else(|| scroll_target(&container, &document, &entries))
else {
return;
};
if next != active.get() {
active.set(next.clone());
}
if hash != next {
sync_hash(&window, &next);
seen_hash.set(next);
scroll_at_hash.set(scroll_top);
}
});
}
fn toc_entry_hrefs(document: &Document) -> Option<Vec<String>> {
let Ok(links) = document.query_selector_all(TOC_SPY_LINK_SELECTOR) else {
return None;
};
let mut entries: Vec<String> = Vec::new();
for index in 0..links.length() {
let Some(node) = links.item(index) else {
continue;
};
let element: Element = node.unchecked_into();
let Some(href) = element.get_attribute(HREF_ATTR) else {
continue;
};
entries.push(href);
}
if entries.is_empty() {
None
} else {
Some(entries)
}
}
fn sync_hash(window: &Window, href: &str) {
let Ok(history) = window.history() else {
return;
};
let _: Result<(), JsValue> = history.replace_state_with_url(&JsValue::NULL, "", Some(href));
}
fn href_anchor(href: &str) -> Option<String> {
let index: Option<usize> = href.rfind('#').filter(|index: &usize| *index > 0);
index
.map(|index: usize| href[index + 1..].to_string())
.filter(|anchor: &String| !anchor.is_empty())
}
fn fragment_target(window: &Window, entries: &[String]) -> Option<String> {
let Ok(hash) = window.location().hash() else {
return None;
};
let anchor_start: Option<usize> = hash.rfind('#').filter(|index: &usize| *index > 0);
let index: usize = anchor_start?;
let raw: &str = &hash[index + 1..];
if raw.is_empty() {
return None;
}
let anchor: String = decode_anchor(raw);
let mut best: Option<String> = None;
let mut best_len: i32 = -1;
for href in entries {
let Some(slug) = href_anchor(href) else {
continue;
};
if slug == anchor {
best = Some(href.clone());
break;
}
if anchor.starts_with(&slug) && slug.len() as i32 > best_len {
best_len = slug.len() as i32;
best = Some(href.clone());
}
}
best
}
fn scroll_target(container: &Element, document: &Document, entries: &[String]) -> Option<String> {
let rect: DomRect = container.get_bounding_client_rect();
let line: f64 = rect.top() + TOC_SPY_READING_LINE_PX;
let at_end: bool = container.scroll_top() + container.client_height()
>= container.scroll_height() - TOC_SPY_END_TOLERANCE_PX as i32;
if at_end && entries.len() > 1 {
return entries.last().cloned();
}
let mut best: Option<String> = None;
let mut best_top: f64 = f64::NEG_INFINITY;
for href in entries {
let Some(slug) = href_anchor(href) else {
continue;
};
let Some(heading) = document.get_element_by_id(slug.as_str()) else {
continue;
};
let top: f64 = heading.get_bounding_client_rect().top();
if top <= line && top > best_top {
best_top = top;
best = Some(href.clone());
}
}
best.or_else(|| entries.first().cloned())
}
fn prev_next(
locale: &'static DocsLocale,
route: &str,
) -> (Option<EuvPaginationItem>, Option<EuvPaginationItem>) {
let pool: Vec<&'static EuvSidebarItem> = flatten_links(locale.sidebar);
let pool_index: Option<usize> = pool
.iter()
.position(|item: &&'static EuvSidebarItem| item.link == Some(route));
let Some(pool_index) = pool_index else {
return (None, None);
};
let to_item: fn(Option<&&'static EuvSidebarItem>) -> Option<EuvPaginationItem> =
|item: Option<&&'static EuvSidebarItem>| {
item.and_then(|item: &&'static EuvSidebarItem| {
item.link.map(|link: &'static str| EuvPaginationItem {
text: item.text,
link,
})
})
};
let prev: Option<EuvPaginationItem> =
to_item(pool_index.checked_sub(1).and_then(|i: usize| pool.get(i)));
let next: Option<EuvPaginationItem> = to_item(pool.get(pool_index + 1));
(prev, next)
}