gloo_history/
hash.rs

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/// A [`History`] that is implemented with [`web_sys::History`] and stores path in `#`(fragment).
16///
17/// # Panics
18///
19/// HashHistory does not support relative paths and will panic if routes are not starting with `/`.
20#[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        // We strip # from hash.
199        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    /// Creates a new [`HashHistory`]
224    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                // Hash needs to start with #/.
248                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}