1use std::{any::Any, borrow::Cow, cell::RefCell, fmt, rc::Rc};
2
3use gloo_events::EventListener;
4use gloo_utils::window;
5use wasm_bindgen::{JsValue, UnwrapThrowExt};
6use web_sys::Url;
7
8use crate::history::History;
9use crate::listener::HistoryListener;
10use crate::location::Location;
11use crate::state::{HistoryState, StateMap};
12use crate::utils::WeakCallback;
13#[cfg(feature = "query")]
14use crate::{error::HistoryResult, query::ToQuery};
15
16#[derive(Clone)]
19pub struct BrowserHistory {
20 inner: web_sys::History,
21 states: Rc<RefCell<StateMap>>,
22 callbacks: Rc<RefCell<Vec<WeakCallback>>>,
23}
24
25impl fmt::Debug for BrowserHistory {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.debug_struct("BrowserHistory").finish()
28 }
29}
30
31impl PartialEq for BrowserHistory {
32 fn eq(&self, _rhs: &Self) -> bool {
33 true
35 }
36}
37
38impl History for BrowserHistory {
39 fn len(&self) -> usize {
40 self.inner.length().expect_throw("failed to get length.") as usize
41 }
42
43 fn go(&self, delta: isize) {
44 self.inner
45 .go_with_delta(delta as i32)
46 .expect_throw("failed to call go.")
47 }
48
49 fn push<'a>(&self, route: impl Into<Cow<'a, str>>) {
50 let url = route.into();
51 self.inner
52 .push_state_with_url(&Self::create_history_state().1, "", Some(&url))
53 .expect_throw("failed to push state.");
54
55 self.notify_callbacks();
56 }
57
58 fn replace<'a>(&self, route: impl Into<Cow<'a, str>>) {
59 let url = route.into();
60 self.inner
61 .replace_state_with_url(&Self::create_history_state().1, "", Some(&url))
62 .expect_throw("failed to replace history.");
63
64 self.notify_callbacks();
65 }
66
67 fn push_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
68 where
69 T: 'static,
70 {
71 let url = route.into();
72
73 let (id, history_state) = Self::create_history_state();
74
75 let mut states = self.states.borrow_mut();
76 states.insert(id, Rc::new(state) as Rc<dyn Any>);
77
78 self.inner
79 .push_state_with_url(&history_state, "", Some(&url))
80 .expect_throw("failed to push state.");
81
82 drop(states);
83 self.notify_callbacks();
84 }
85
86 fn replace_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
87 where
88 T: 'static,
89 {
90 let url = route.into();
91
92 let (id, history_state) = Self::create_history_state();
93
94 let mut states = self.states.borrow_mut();
95 states.insert(id, Rc::new(state) as Rc<dyn Any>);
96
97 self.inner
98 .replace_state_with_url(&history_state, "", Some(&url))
99 .expect_throw("failed to replace state.");
100
101 drop(states);
102 self.notify_callbacks();
103 }
104
105 #[cfg(feature = "query")]
106 fn push_with_query<'a, Q>(
107 &self,
108 route: impl Into<Cow<'a, str>>,
109 query: Q,
110 ) -> HistoryResult<(), Q::Error>
111 where
112 Q: ToQuery,
113 {
114 let route = route.into();
115 let query = query.to_query()?;
116
117 let url = Self::combine_url(&route, &query);
118
119 self.inner
120 .push_state_with_url(&Self::create_history_state().1, "", Some(&url))
121 .expect_throw("failed to push history.");
122
123 self.notify_callbacks();
124 Ok(())
125 }
126
127 #[cfg(feature = "query")]
128 fn replace_with_query<'a, Q>(
129 &self,
130 route: impl Into<Cow<'a, str>>,
131 query: Q,
132 ) -> HistoryResult<(), Q::Error>
133 where
134 Q: ToQuery,
135 {
136 let route = route.into();
137 let query = query.to_query()?;
138
139 let url = Self::combine_url(&route, &query);
140
141 self.inner
142 .replace_state_with_url(&Self::create_history_state().1, "", Some(&url))
143 .expect_throw("failed to replace history.");
144
145 self.notify_callbacks();
146 Ok(())
147 }
148
149 #[cfg(feature = "query")]
150 fn push_with_query_and_state<'a, Q, T>(
151 &self,
152 route: impl Into<Cow<'a, str>>,
153 query: Q,
154 state: T,
155 ) -> HistoryResult<(), Q::Error>
156 where
157 Q: ToQuery,
158 T: 'static,
159 {
160 let (id, history_state) = Self::create_history_state();
161
162 let mut states = self.states.borrow_mut();
163 states.insert(id, Rc::new(state) as Rc<dyn Any>);
164
165 let route = route.into();
166 let query = query.to_query()?;
167
168 let url = Self::combine_url(&route, &query);
169
170 self.inner
171 .push_state_with_url(&history_state, "", Some(&url))
172 .expect_throw("failed to push history.");
173
174 drop(states);
175 self.notify_callbacks();
176 Ok(())
177 }
178
179 #[cfg(feature = "query")]
180 fn replace_with_query_and_state<'a, Q, T>(
181 &self,
182 route: impl Into<Cow<'a, str>>,
183 query: Q,
184 state: T,
185 ) -> HistoryResult<(), Q::Error>
186 where
187 Q: ToQuery,
188 T: 'static,
189 {
190 let (id, history_state) = Self::create_history_state();
191
192 let mut states = self.states.borrow_mut();
193 states.insert(id, Rc::new(state) as Rc<dyn Any>);
194
195 let route = route.into();
196 let query = query.to_query()?;
197
198 let url = Self::combine_url(&route, &query);
199
200 self.inner
201 .replace_state_with_url(&history_state, "", Some(&url))
202 .expect_throw("failed to replace history.");
203
204 drop(states);
205 self.notify_callbacks();
206 Ok(())
207 }
208
209 fn listen<CB>(&self, callback: CB) -> HistoryListener
210 where
211 CB: Fn() + 'static,
212 {
213 let cb = Rc::new(callback) as Rc<dyn Fn()>;
215
216 self.callbacks.borrow_mut().push(Rc::downgrade(&cb));
217
218 HistoryListener { _listener: cb }
219 }
220
221 fn location(&self) -> Location {
222 let loc = window().location();
223
224 let history_state = self.inner.state().expect_throw("failed to get state");
225 let history_state = serde_wasm_bindgen::from_value::<HistoryState>(history_state).ok();
226
227 let id = history_state.map(|m| m.id());
228
229 let states = self.states.borrow();
230
231 Location {
232 path: loc.pathname().expect_throw("failed to get pathname").into(),
233 query_str: loc
234 .search()
235 .expect_throw("failed to get location query.")
236 .into(),
237 hash: loc
238 .hash()
239 .expect_throw("failed to get location hash.")
240 .into(),
241 state: id.and_then(|m| states.get(&m).cloned()),
242 id,
243 }
244 }
245}
246
247impl Default for BrowserHistory {
248 fn default() -> Self {
249 thread_local! {
251 static BROWSER_HISTORY: (BrowserHistory, EventListener) = {
252 let window = window();
253
254 let inner = window
255 .history()
256 .expect_throw("Failed to create browser history. Are you using a browser?");
257 let callbacks = Rc::default();
258
259 let history = BrowserHistory {
260 inner,
261 callbacks,
262 states: Rc::default(),
263 };
264
265 let listener = {
266 let history = history.clone();
267
268 EventListener::new(&window, "popstate", move |_| {
270 history.notify_callbacks();
271 })
272 };
273
274 (history, listener)
275 };
276 }
277
278 BROWSER_HISTORY.with(|(history, _)| history.clone())
279 }
280}
281
282impl BrowserHistory {
283 pub fn new() -> Self {
285 Self::default()
286 }
287
288 fn notify_callbacks(&self) {
289 crate::utils::notify_callbacks(self.callbacks.clone());
290 }
291
292 fn create_history_state() -> (u32, JsValue) {
293 let history_state = HistoryState::new();
294
295 (
296 history_state.id(),
297 serde_wasm_bindgen::to_value(&history_state)
298 .expect_throw("fails to create history state."),
299 )
300 }
301
302 pub(crate) fn combine_url(route: &str, query: &str) -> String {
303 let href = window()
304 .location()
305 .href()
306 .expect_throw("Failed to read location href");
307
308 let url = Url::new_with_base(route, &href).expect_throw("current url is not valid.");
309
310 url.set_search(query);
311
312 url.href()
313 }
314}