yggdrasil/components/post/
post_toc.rs1use leptos::ev::{KeyboardEvent, PointerEvent};
14use leptos::prelude::*;
15
16#[cfg(target_arch = "wasm32")]
17use crate::bridges::globals;
18use crate::components::enhance_assets::EnhanceScript;
19use crate::components::ui::{Icon, IconName};
20
21const DEFAULT_NAV_ID: &str = "post-toc";
23
24#[component]
35pub fn PostToc(
36 #[prop(into)] toc_html: String,
37 #[prop(default = "目录")] title: &'static str,
38 #[prop(optional)] nav_id: Option<&'static str>,
39 #[prop(optional)] content_id: Option<&'static str>,
40 #[prop(optional)] scroll_id: Option<&'static str>,
41) -> impl IntoView {
42 let open = RwSignal::new(false);
43 let id = nav_id.unwrap_or(DEFAULT_NAV_ID);
44 let pill_id = format!("{id}-pill");
45
46 #[cfg(target_arch = "wasm32")]
48 Effect::new(move |_| {
49 if let (Some(nav_id), Some(content_id), Some(scroll_id)) = (nav_id, content_id, scroll_id) {
50 globals::init_toc_sidebar(
51 Some(&format!("#{nav_id}")),
52 Some(&format!("#{content_id}")),
53 Some(&format!("#{scroll_id}")),
54 );
55 } else {
56 globals::init_toc_sidebar(None, None, None);
57 }
58 });
59 #[cfg(not(target_arch = "wasm32"))]
60 let _ = (content_id, scroll_id);
61
62 #[cfg(target_arch = "wasm32")]
63 on_cleanup(move || {
64 globals::dispose_toc_sidebar(nav_id.map(|nav_id| format!("#{nav_id}")).as_deref());
65 });
66
67 Effect::new(move |_| {
69 if open.get() {
70 request_animation_frame(move || focus_by_id(id));
71 }
72 });
73 let pill_focus_id = pill_id.clone();
74 let close = Callback::new(move |restore: bool| {
76 if !open.get_untracked() {
77 return;
78 }
79 open.set(false);
80 if restore {
81 let pill_id = pill_focus_id.clone();
82 request_animation_frame(move || focus_by_id(&pill_id));
83 }
84 });
85
86 view! {
87 <EnhanceScript/>
90
91 <div
92 class=move || if open.get() { "post-toc is-open" } else { "post-toc" }
93 data-toc-root="true"
94 on:keydown=move |event: KeyboardEvent| {
95 if open.get_untracked() && event.key() == "Escape" {
96 event.prevent_default();
97 close.run(true);
98 }
99 }
100 >
101 <div class="toc-progress-line" aria-hidden="true"></div>
102 <button
103 id=pill_id
104 class="toc-pill"
105 type="button"
106 aria-expanded=move || open.get().to_string()
107 aria-controls=id
108 on:click=move |_| open.set(true)
109 >
110 <svg class="toc-pill-ring" viewBox="0 0 22 22" aria-hidden="true">
111 <circle cx="11" cy="11" r="8.5"></circle>
112 <circle class="toc-pill-ring-value" cx="11" cy="11" r="8.5" pathLength="100"></circle>
113 </svg>
114 <span class="toc-pill-label">
115 <span class="toc-pill-title">{title}</span>
116 <span class="toc-pill-current" data-toc-current="true"></span>
117 </span>
118 <span class="toc-pill-percent tabular-nums" data-toc-percent="true"></span>
119 </button>
120 <div class="toc-scrim" aria-hidden="true" on:click=move |_| close.run(true)></div>
121 <nav id=id class="toc-sidebar" aria-label=title tabindex="-1">
122 <div class="toc-sidebar-head">
123 <span class="toc-sidebar-title">{title}</span>
124 <button
125 class="toc-sidebar-close btn btn-xs btn-icon btn-ghost"
126 type="button"
127 aria-label="关闭目录"
128 on:click=move |_| close.run(true)
129 >
130 <Icon name=IconName::X size=16/>
131 </button>
132 </div>
133 <div
137 class="toc-sidebar-body scroll-fade-y"
138 on:pointerup=move |_: PointerEvent| close.run(false)
139 on:keydown=move |event: KeyboardEvent| {
140 if event.key() == "Enter" {
141 close.run(false);
142 }
143 }
144 inner_html=toc_html
145 ></div>
146 <div class="toc-sidebar-progress">
147 <div class="toc-sidebar-progress-text">
148 <span>"已读"</span>
149 <span class="tabular-nums" data-toc-percent="true"></span>
150 </div>
151 <div class="toc-sidebar-progress-track" aria-hidden="true">
152 <div class="toc-sidebar-progress-bar"></div>
153 </div>
154 </div>
155 </nav>
156 </div>
157 }
158}
159
160#[cfg(target_arch = "wasm32")]
161fn focus_by_id(id: &str) {
162 use wasm_bindgen::JsCast;
163 if let Some(el) = web_sys::window()
164 .and_then(|w| w.document())
165 .and_then(|d| d.get_element_by_id(id))
166 .and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok())
167 {
168 let _ = el.focus();
169 }
170}
171
172#[cfg(not(target_arch = "wasm32"))]
173fn focus_by_id(_id: &str) {}