impulse_thaw/anchor/
mod.rs1mod anchor_link;
2
3pub use anchor_link::AnchorLink;
4
5use leptos::{context::Provider, html, prelude::*};
6use thaw_utils::{class_list, mount_style};
7use web_sys::{DomRect, Element};
8
9#[component]
10pub fn Anchor(
11 #[prop(optional, into)] class: MaybeProp<String>,
12 #[prop(into, optional)]
16 offset_target: Option<OffsetTarget>,
17 children: Children,
18) -> impl IntoView {
19 mount_style("anchor", include_str!("./anchor.css"));
20 let anchor_ref = NodeRef::new();
21 let bar_ref = NodeRef::new();
22 let element_ids = RwSignal::new(Vec::<String>::new());
23 let active_id = RwSignal::new(None::<String>);
24
25 #[cfg(any(feature = "csr", feature = "hydrate"))]
26 {
27 use leptos::ev;
28 use std::cmp::Ordering;
29 use thaw_utils::{add_event_listener_with_bool, throttle};
30
31 struct LinkInfo {
32 top: f64,
33 id: String,
34 }
35
36 let offset_target = send_wrapper::SendWrapper::new(offset_target);
37
38 let on_scroll = move || {
39 element_ids.with(|ids| {
40 let offset_target_top = if let Some(offset_target) = offset_target.as_ref() {
41 if let Some(rect) = offset_target.get_bounding_client_rect() {
42 rect.top()
43 } else {
44 return;
45 }
46 } else {
47 0.0
48 };
49
50 let mut links: Vec<LinkInfo> = vec![];
51 for id in ids.iter() {
52 if let Some(link_el) = document().get_element_by_id(id) {
53 let link_rect = link_el.get_bounding_client_rect();
54 links.push(LinkInfo {
55 top: link_rect.top() - offset_target_top,
56 id: id.clone(),
57 });
58 }
59 }
60 links.sort_by(|a, b| {
61 if a.top > b.top {
62 Ordering::Greater
63 } else {
64 Ordering::Less
65 }
66 });
67
68 let mut temp_link = None::<LinkInfo>;
69 for link in links.into_iter() {
70 if link.top >= 0.0 {
71 if link.top <= 12.0 {
72 temp_link = Some(link);
73 break;
74 } else if temp_link.is_some() {
75 break;
76 } else {
77 temp_link = None;
78 }
79 } else {
80 temp_link = Some(link);
81 }
82 }
83 active_id.set(temp_link.map(|link| link.id));
84 });
85 };
86 let cb = throttle(
87 move || {
88 on_scroll();
89 },
90 std::time::Duration::from_millis(200),
91 );
92 let scroll_handle = add_event_listener_with_bool(
93 document(),
94 ev::scroll,
95 move |_| {
96 cb();
97 },
98 true,
99 );
100 on_cleanup(move || {
101 scroll_handle.remove();
102 });
103 }
104 #[cfg(not(any(feature = "csr", feature = "hydrate")))]
105 {
106 let _ = offset_target;
107 }
108
109 view! {
110 <div class=class_list!["thaw-anchor", class] node_ref=anchor_ref>
111 <div class="thaw-anchor-rail">
112 <div
113 class="thaw-anchor-rail__bar"
114 class=(
115 "thaw-anchor-rail__bar--active",
116 move || active_id.with(|id| id.is_some()),
117 )
118
119 node_ref=bar_ref
120 ></div>
121 </div>
122 <Provider value=AnchorInjection::new(
123 anchor_ref,
124 bar_ref,
125 element_ids,
126 active_id,
127 )>{children()}</Provider>
128 </div>
129 }
130}
131
132#[derive(Clone)]
133pub(crate) struct AnchorInjection {
134 anchor_ref: NodeRef<html::Div>,
135 bar_ref: NodeRef<html::Div>,
136 element_ids: RwSignal<Vec<String>>,
137 pub active_id: RwSignal<Option<String>>,
138}
139
140impl Copy for AnchorInjection {}
141
142impl AnchorInjection {
143 pub fn expect_context() -> Self {
144 expect_context()
145 }
146
147 fn new(
148 anchor_ref: NodeRef<html::Div>,
149 bar_ref: NodeRef<html::Div>,
150 element_ids: RwSignal<Vec<String>>,
151 active_id: RwSignal<Option<String>>,
152 ) -> Self {
153 Self {
154 anchor_ref,
155 bar_ref,
156 element_ids,
157 active_id,
158 }
159 }
160
161 pub fn scroll_into_view(&self, id: &String) {
162 let Some(link_el) = document().get_element_by_id(id) else {
163 return;
164 };
165 link_el.scroll_into_view();
166 }
167
168 pub fn append_id(&self, id: String) {
169 self.element_ids.update(|ids| {
170 ids.push(id);
171 });
172 }
173
174 pub fn remove_id(&self, id: &String) {
175 self.element_ids.update(|ids| {
176 if let Some(index) = ids.iter().position(|item_id| item_id == id) {
177 ids.remove(index);
178 }
179 });
180 }
181
182 pub fn update_background_position(&self, title_rect: DomRect) {
183 if let Some(anchor_el) = self.anchor_ref.get_untracked() {
184 let bar_el = self.bar_ref.get_untracked().unwrap();
185 let anchor_rect = anchor_el.get_bounding_client_rect();
186
187 let offset_top = title_rect.top() - anchor_rect.top();
188 bar_el.style(("top", format!("{}px", offset_top)));
191 bar_el.style(("height", format!("{}px", title_rect.height())));
192 }
193 }
194}
195
196pub enum OffsetTarget {
197 Selector(String),
198 Element(Element),
199}
200
201#[cfg(any(feature = "csr", feature = "hydrate"))]
202impl OffsetTarget {
203 fn get_bounding_client_rect(&self) -> Option<DomRect> {
204 match self {
205 OffsetTarget::Selector(selector) => {
206 let el = document().query_selector(selector).ok().flatten()?;
207 Some(el.get_bounding_client_rect())
208 }
209 OffsetTarget::Element(el) => Some(el.get_bounding_client_rect()),
210 }
211 }
212}
213
214impl From<&'static str> for OffsetTarget {
215 fn from(value: &'static str) -> Self {
216 Self::Selector(value.to_string())
217 }
218}
219
220impl From<String> for OffsetTarget {
221 fn from(value: String) -> Self {
222 Self::Selector(value)
223 }
224}
225
226impl From<Element> for OffsetTarget {
227 fn from(value: Element) -> Self {
228 Self::Element(value)
229 }
230}