1use std::{borrow::Cow, fmt};
2
3use gloo_utils::window;
4use wasm_bindgen::UnwrapThrowExt;
5use web_sys::Url;
6
7use crate::browser::BrowserHistory;
8use crate::history::History;
9use crate::listener::HistoryListener;
10use crate::location::Location;
11use crate::utils::{assert_absolute_path, assert_no_query};
12#[cfg(feature = "query")]
13use crate::{error::HistoryResult, query::ToQuery};
14
15#[derive(Clone, PartialEq)]
21pub struct HashHistory {
22 inner: BrowserHistory,
23}
24
25impl fmt::Debug for HashHistory {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.debug_struct("HashHistory").finish()
28 }
29}
30
31impl History for HashHistory {
32 fn len(&self) -> usize {
33 self.inner.len()
34 }
35
36 fn go(&self, delta: isize) {
37 self.inner.go(delta)
38 }
39
40 fn push<'a>(&self, route: impl Into<Cow<'a, str>>) {
41 let route = route.into();
42
43 assert_absolute_path(&route);
44 assert_no_query(&route);
45
46 let url = Self::get_url();
47 url.set_hash(&route);
48
49 self.inner.push(url.href());
50 }
51
52 fn replace<'a>(&self, route: impl Into<Cow<'a, str>>) {
53 let route = route.into();
54
55 assert_absolute_path(&route);
56 assert_no_query(&route);
57
58 let url = Self::get_url();
59 url.set_hash(&route);
60
61 self.inner.replace(url.href());
62 }
63
64 fn push_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
65 where
66 T: 'static,
67 {
68 let route = route.into();
69
70 assert_absolute_path(&route);
71 assert_no_query(&route);
72
73 let url = Self::get_url();
74 url.set_hash(&route);
75
76 self.inner.push_with_state(url.href(), state)
77 }
78
79 fn replace_with_state<'a, T>(&self, route: impl Into<Cow<'a, str>>, state: T)
80 where
81 T: 'static,
82 {
83 let route = route.into();
84
85 assert_absolute_path(&route);
86 assert_no_query(&route);
87
88 let url = Self::get_url();
89 url.set_hash(&route);
90
91 self.inner.replace_with_state(url.href(), state)
92 }
93
94 #[cfg(feature = "query")]
95 fn push_with_query<'a, Q>(
96 &self,
97 route: impl Into<Cow<'a, str>>,
98 query: Q,
99 ) -> HistoryResult<(), Q::Error>
100 where
101 Q: ToQuery,
102 {
103 let query = query.to_query()?;
104 let route = route.into();
105
106 assert_absolute_path(&route);
107 assert_no_query(&route);
108
109 let url = Self::get_url();
110 url.set_hash(&format!("{route}?{query}"));
111
112 self.inner.push(url.href());
113 Ok(())
114 }
115 #[cfg(feature = "query")]
116 fn replace_with_query<'a, Q>(
117 &self,
118 route: impl Into<Cow<'a, str>>,
119 query: Q,
120 ) -> HistoryResult<(), Q::Error>
121 where
122 Q: ToQuery,
123 {
124 let query = query.to_query()?;
125 let route = route.into();
126
127 assert_absolute_path(&route);
128 assert_no_query(&route);
129
130 let url = Self::get_url();
131 url.set_hash(&format!("{route}?{query}"));
132
133 self.inner.replace(url.href());
134 Ok(())
135 }
136
137 #[cfg(feature = "query")]
138 fn push_with_query_and_state<'a, Q, T>(
139 &self,
140 route: impl Into<Cow<'a, str>>,
141 query: Q,
142 state: T,
143 ) -> HistoryResult<(), Q::Error>
144 where
145 Q: ToQuery,
146 T: 'static,
147 {
148 let route = route.into();
149
150 assert_absolute_path(&route);
151 assert_no_query(&route);
152
153 let url = Self::get_url();
154
155 let query = query.to_query()?;
156 url.set_hash(&format!("{route}?{query}"));
157
158 self.inner.push_with_state(url.href(), state);
159
160 Ok(())
161 }
162
163 #[cfg(feature = "query")]
164 fn replace_with_query_and_state<'a, Q, T>(
165 &self,
166 route: impl Into<Cow<'a, str>>,
167 query: Q,
168 state: T,
169 ) -> HistoryResult<(), Q::Error>
170 where
171 Q: ToQuery,
172 T: 'static,
173 {
174 let route = route.into();
175
176 assert_absolute_path(&route);
177 assert_no_query(&route);
178
179 let url = Self::get_url();
180
181 let query = query.to_query()?;
182 url.set_hash(&format!("{route}?{query}"));
183
184 self.inner.replace_with_state(url.href(), state);
185
186 Ok(())
187 }
188
189 fn listen<CB>(&self, callback: CB) -> HistoryListener
190 where
191 CB: Fn() + 'static,
192 {
193 self.inner.listen(callback)
194 }
195
196 fn location(&self) -> Location {
197 let inner_loc = self.inner.location();
198 let hash_url = inner_loc.hash().chars().skip(1).collect::<String>();
200
201 assert_absolute_path(&hash_url);
202
203 let hash_url = Url::new_with_base(
204 &hash_url,
205 &window()
206 .location()
207 .href()
208 .expect_throw("failed to get location href."),
209 )
210 .expect_throw("failed to get make url");
211
212 Location {
213 path: hash_url.pathname().into(),
214 query_str: hash_url.search().into(),
215 hash: hash_url.hash().into(),
216 id: inner_loc.id,
217 state: inner_loc.state,
218 }
219 }
220}
221
222impl HashHistory {
223 pub fn new() -> Self {
225 Self::default()
226 }
227
228 fn get_url() -> Url {
229 let href = window()
230 .location()
231 .href()
232 .expect_throw("Failed to read location href");
233
234 Url::new(&href).expect_throw("current url is not valid.")
235 }
236}
237
238impl Default for HashHistory {
239 fn default() -> Self {
240 thread_local! {
241 static HASH_HISTORY: HashHistory = {
242 let browser_history = BrowserHistory::new();
243 let browser_location = browser_history.location();
244
245 let current_hash = browser_location.hash();
246
247 if current_hash.is_empty() || !current_hash.starts_with("#/") {
249 let url = HashHistory::get_url();
250 url.set_hash("#/");
251
252 browser_history.replace(url.href());
253 }
254
255 HashHistory {
256 inner: browser_history,
257 }
258 };
259 }
260
261 HASH_HISTORY.with(|s| s.clone())
262 }
263}