Skip to content

Commit 5369271

Browse files
committed
chore: fixes and toc rewrite
1 parent 97ab6e2 commit 5369271

3 files changed

Lines changed: 223 additions & 46 deletions

File tree

yew-rs/docs-ja/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ macro_rules! doc_page {
2424
sidebar={docs_sidebar()}
2525
active_sidebar_path={$path}
2626
active_nav="Docs"
27-
doc_version="next"
27+
doc_version="Next"
2828
lang="ja"
2929
markdown={markdown}
3030
toc={toc}

yew-rs/lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ssr = ["yew/ssr", "stylist/ssr"]
1313
yew = "0.23"
1414
stylist = { version = "0.15", features = ["yew_integration", "parser"] }
1515
syntect = { git = "https://github.com/Madoshakalaka/syntect.git", branch = "line-highlighting", default-features = false, features = ["parsing", "html", "default-syntaxes", "dump-load", "regex-fancy"] }
16-
web-sys = { workspace = true, features = ["Window", "Document", "Element", "EventTarget", "HtmlElement", "HtmlHeadElement", "MediaQueryList", "Navigator", "Node", "Clipboard"], optional = true }
16+
web-sys = { workspace = true, features = ["Window", "Document", "DomRect", "Element", "EventTarget", "HtmlElement", "HtmlHeadElement", "Location", "MediaQueryList", "Navigator", "Node", "Clipboard"], optional = true }
1717
wasm-bindgen = { workspace = true, optional = true }
1818
wasm-bindgen-futures = { workspace = true, optional = true }
1919
js-sys = { workspace = true, optional = true }

yew-rs/lib/src/components/layout.rs

Lines changed: 221 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use stylist::yew::styled_component;
2+
#[cfg(feature = "csr")]
3+
use wasm_bindgen::JsCast;
24
use yew::prelude::*;
35

46
use crate::components::footer::Footer;
@@ -45,7 +47,14 @@ fn version_slug(doc_version: &str) -> &str {
4547
}
4648

4749
pub fn rewrite_doc_href(href: &str, lang: &str, doc_version: &str) -> String {
48-
if let Some(rest) = href.strip_prefix("/docs/") {
50+
let after_lang = if lang.is_empty() {
51+
href
52+
} else {
53+
let prefix = format!("/{lang}");
54+
href.strip_prefix(&prefix).unwrap_or(href)
55+
};
56+
57+
if let Some(rest) = after_lang.strip_prefix("/docs/") {
4958
let lang_p = if lang.is_empty() {
5059
String::new()
5160
} else {
@@ -62,6 +71,34 @@ pub fn rewrite_doc_href(href: &str, lang: &str, doc_version: &str) -> String {
6271
}
6372
}
6473

74+
fn edit_page_url(active_path: &str, lang: &str) -> String {
75+
const BASE: &str = "https://github.com/yewstack/yew/blob/master/yew-rs";
76+
77+
let crate_dir = if lang.is_empty() {
78+
"docs".to_string()
79+
} else {
80+
format!("docs-{}", lang.to_lowercase())
81+
};
82+
83+
let bare = if lang.is_empty() {
84+
active_path
85+
} else {
86+
let pfx = format!("/{lang}");
87+
active_path.strip_prefix(&pfx).unwrap_or(active_path)
88+
};
89+
90+
let Some(page_path) = bare.strip_prefix("/docs/") else {
91+
return String::new();
92+
};
93+
let file_path: String = page_path
94+
.split('/')
95+
.map(|seg| seg.replace('-', "_"))
96+
.collect::<Vec<_>>()
97+
.join("/");
98+
99+
format!("{BASE}/{crate_dir}/src/pages/{file_path}.rs")
100+
}
101+
65102
fn build_breadcrumbs(
66103
entries: &[SidebarEntry],
67104
active_path: &str,
@@ -306,6 +343,28 @@ pub fn Layout(props: &LayoutProps) -> Html {
306343
color: var(--color-primary);
307344
}
308345
346+
.edit-page {
347+
margin-top: 2rem;
348+
}
349+
350+
.edit-page-link {
351+
display: inline-flex;
352+
align-items: center;
353+
gap: 0.375rem;
354+
color: var(--color-text-secondary);
355+
font-size: 0.875rem;
356+
text-decoration: none;
357+
transition: color 0.2s;
358+
}
359+
360+
.edit-page-link:hover {
361+
color: var(--color-primary);
362+
}
363+
364+
.edit-page-icon {
365+
flex-shrink: 0;
366+
}
367+
309368
.toc-column {
310369
width: 250px;
311370
flex-shrink: 0;
@@ -434,6 +493,12 @@ pub fn Layout(props: &LayoutProps) -> Html {
434493
(prev, next)
435494
});
436495

496+
let edit_url = if has_sidebar {
497+
edit_page_url(props.active_sidebar_path.as_str(), props.lang.as_str())
498+
} else {
499+
String::new()
500+
};
501+
437502
#[cfg(feature = "csr")]
438503
let copy_md_button = if !props.full_width && !props.markdown.is_empty() {
439504
html! {
@@ -529,6 +594,16 @@ pub fn Layout(props: &LayoutProps) -> Html {
529594
<h1 class="page-title">{&props.title}</h1>
530595
}
531596
{props.children.clone()}
597+
if !edit_url.is_empty() {
598+
<div class="edit-page">
599+
<a class="edit-page-link" href={edit_url.clone()} target="_blank" rel="noopener noreferrer">
600+
<svg viewBox="0 0 24 24" width="16" height="16" class="edit-page-icon">
601+
<path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
602+
</svg>
603+
{"Edit this page"}
604+
</a>
605+
</div>
606+
}
532607
if let Some((prev, next)) = &pagination {
533608
<nav class="pagination" aria-label="Docs pages">
534609
if let Some((label, href)) = prev {
@@ -549,25 +624,7 @@ pub fn Layout(props: &LayoutProps) -> Html {
549624
}
550625
</main>
551626
if has_sidebar && !props.toc.is_empty() {
552-
<aside class="toc-column">
553-
<nav class="toc-container">
554-
<ul class="toc-list">
555-
{ for props.toc.iter().map(|entry| {
556-
let pad = match entry.level {
557-
3 => "padding-left:1rem",
558-
4 => "padding-left:2rem",
559-
_ => "",
560-
};
561-
let href = format!("#{}", entry.id);
562-
html! {
563-
<li style={pad}>
564-
<a class="toc-link" href={href}>{&entry.text}</a>
565-
</li>
566-
}
567-
})}
568-
</ul>
569-
</nav>
570-
</aside>
627+
<Toc entries={props.toc.clone()} />
571628
}
572629
</div>
573630
<Footer />
@@ -577,37 +634,157 @@ pub fn Layout(props: &LayoutProps) -> Html {
577634

578635
#[cfg(feature = "csr")]
579636
fn init_page_features() {
580-
let _ = js_sys::eval(PAGE_FEATURES_JS);
637+
let window = match web_sys::window() {
638+
Some(w) => w,
639+
None => return,
640+
};
641+
let hash = window.location().hash().unwrap_or_default();
642+
if !hash.is_empty() {
643+
let document = match window.document() {
644+
Some(d) => d,
645+
None => return,
646+
};
647+
if let Some(el) = document.get_element_by_id(&hash[1..]) {
648+
gloo::timers::callback::Timeout::new(100, move || {
649+
el.scroll_into_view();
650+
})
651+
.forget();
652+
}
653+
}
581654
}
582655

583656
#[cfg(not(feature = "csr"))]
584657
fn init_page_features() {}
585658

659+
#[derive(Clone, PartialEq, Properties)]
660+
struct TocProps {
661+
entries: Vec<TocEntry>,
662+
}
663+
586664
#[cfg(feature = "csr")]
587-
const PAGE_FEATURES_JS: &str = r#"
588-
(function() {
589-
var content = document.querySelector('.doc-content');
590-
if (!content) return;
591-
592-
var headings = content.querySelectorAll('h2.anchor, h3.anchor, h4.anchor');
593-
var tc = document.querySelector('.toc-container');
594-
if (tc && headings.length > 0) {
595-
var links = tc.querySelectorAll('.toc-link');
596-
var obs = new IntersectionObserver(function(entries) {
597-
entries.forEach(function(e) {
598-
if (e.isIntersecting) {
599-
links.forEach(function(l) {
600-
l.classList.toggle('toc-link--active', l.getAttribute('href') === '#' + e.target.id);
601-
});
665+
#[function_component]
666+
fn Toc(props: &TocProps) -> Html {
667+
let active_id = use_state(|| Option::<AttrValue>::None);
668+
669+
{
670+
let active_id = active_id.clone();
671+
let entries = props.entries.clone();
672+
use_effect_with(entries.clone(), move |_| {
673+
let window = web_sys::window().unwrap();
674+
let document = window.document().unwrap();
675+
676+
let navbar_height: f64 = document
677+
.query_selector(".navbar")
678+
.ok()
679+
.flatten()
680+
.map(|el| {
681+
let html: web_sys::HtmlElement = el.unchecked_into();
682+
html.client_height() as f64
683+
})
684+
.unwrap_or(60.0);
685+
686+
let ids: Vec<AttrValue> = entries.iter().map(|e| e.id.clone()).collect();
687+
688+
let compute = {
689+
let ids = ids.clone();
690+
let active_id = active_id.clone();
691+
let document = document.clone();
692+
let window = window.clone();
693+
move || {
694+
let mut active: Option<AttrValue> = None;
695+
let mut next_visible_idx: Option<usize> = None;
696+
697+
for (i, id) in ids.iter().enumerate() {
698+
if let Some(el) = document.get_element_by_id(id.as_str()) {
699+
let rect = el.get_bounding_client_rect();
700+
if rect.top() >= navbar_height {
701+
next_visible_idx = Some(i);
702+
break;
703+
}
704+
}
705+
}
706+
707+
if let Some(idx) = next_visible_idx {
708+
if let Some(el) = document.get_element_by_id(ids[idx].as_str()) {
709+
let rect = el.get_bounding_client_rect();
710+
let vh = window
711+
.inner_height()
712+
.ok()
713+
.and_then(|v| v.as_f64())
714+
.unwrap_or(800.0);
715+
if rect.top() > 0.0 && rect.bottom() < vh / 2.0 {
716+
active = Some(ids[idx].clone());
717+
} else if idx > 0 {
718+
active = Some(ids[idx - 1].clone());
719+
}
720+
}
721+
} else if !ids.is_empty() {
722+
active = Some(ids[ids.len() - 1].clone());
723+
}
724+
725+
active_id.set(active);
602726
}
603-
});
604-
}, { rootMargin: '-60px 0px -66% 0px', threshold: 0 });
605-
headings.forEach(function(h) { obs.observe(h); });
727+
};
728+
729+
compute();
730+
731+
let compute_scroll = compute.clone();
732+
let scroll_listener =
733+
gloo::events::EventListener::new(&document, "scroll", move |_| compute_scroll());
734+
735+
let resize_listener =
736+
gloo::events::EventListener::new(&window, "resize", move |_| compute());
737+
738+
move || drop((scroll_listener, resize_listener))
739+
});
740+
}
741+
742+
html! {
743+
<aside class="toc-column">
744+
<nav class="toc-container">
745+
<ul class="toc-list">
746+
{ for props.entries.iter().map(|entry| {
747+
let pad = match entry.level {
748+
3 => "padding-left:1rem",
749+
4 => "padding-left:2rem",
750+
_ => "",
751+
};
752+
let href = format!("#{}", entry.id);
753+
let is_active = *active_id == Some(entry.id.clone());
754+
html! {
755+
<li style={pad}>
756+
<a class={classes!("toc-link", is_active.then_some("toc-link--active"))} href={href}>{&entry.text}</a>
757+
</li>
758+
}
759+
})}
760+
</ul>
761+
</nav>
762+
</aside>
606763
}
764+
}
607765

608-
if (location.hash) {
609-
var el = document.getElementById(location.hash.slice(1));
610-
if (el) setTimeout(function() { el.scrollIntoView(); }, 100);
766+
#[cfg(not(feature = "csr"))]
767+
#[function_component]
768+
fn Toc(props: &TocProps) -> Html {
769+
html! {
770+
<aside class="toc-column">
771+
<nav class="toc-container">
772+
<ul class="toc-list">
773+
{ for props.entries.iter().map(|entry| {
774+
let pad = match entry.level {
775+
3 => "padding-left:1rem",
776+
4 => "padding-left:2rem",
777+
_ => "",
778+
};
779+
let href = format!("#{}", entry.id);
780+
html! {
781+
<li style={pad}>
782+
<a class="toc-link" href={href}>{&entry.text}</a>
783+
</li>
784+
}
785+
})}
786+
</ul>
787+
</nav>
788+
</aside>
611789
}
612-
})();
613-
"#;
790+
}

0 commit comments

Comments
 (0)