2018-06-08 13:00:49 +00:00
|
|
|
|
(ns reitit.frontend.history
|
2018-12-04 12:22:59 +00:00
|
|
|
|
"Provides integration to hash-change or HTML5 History
|
|
|
|
|
|
events."
|
2022-02-14 14:59:20 +00:00
|
|
|
|
(:require [goog.events :as gevents]
|
|
|
|
|
|
[reitit.core :as reitit]
|
2023-03-24 09:32:22 +00:00
|
|
|
|
[reitit.frontend :as rf]
|
|
|
|
|
|
goog.Uri))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
|
|
|
|
|
|
(defprotocol History
|
|
|
|
|
|
(-init [this] "Create event listeners")
|
|
|
|
|
|
(-stop [this] "Remove event listeners")
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(-on-navigate [this path] "Find a match for current routing path and call on-navigate callback")
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(-get-path [this] "Get the current routing path, including query string and fragment")
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(-href [this path] "Converts given routing path to browser location"))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
|
|
|
|
|
|
;; This version listens for both pop-state and hash-change for
|
|
|
|
|
|
;; compatibility for old browsers not supporting History API.
|
2018-08-27 11:22:38 +00:00
|
|
|
|
(defrecord FragmentHistory [on-navigate router popstate-listener hashchange-listener last-fragment]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
History
|
|
|
|
|
|
(-init [this]
|
|
|
|
|
|
;; Link clicks and e.g. back button trigger both events, if fragment is same as previous ignore second event.
|
|
|
|
|
|
;; For old browsers only the hash-change event is triggered.
|
|
|
|
|
|
(let [last-fragment (atom nil)
|
|
|
|
|
|
this (assoc this :last-fragment last-fragment)
|
|
|
|
|
|
handler (fn [e]
|
|
|
|
|
|
(let [path (-get-path this)]
|
|
|
|
|
|
(when (or (= goog.events.EventType.POPSTATE (.-type e))
|
|
|
|
|
|
(not= @last-fragment path))
|
2020-09-23 10:25:11 +00:00
|
|
|
|
(-on-navigate this path))))
|
|
|
|
|
|
;; rfe start! uses first on-navigate call to store the
|
|
|
|
|
|
;; instance so it has to see the instance with listeners.
|
|
|
|
|
|
this (assoc this
|
|
|
|
|
|
:popstate-listener (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
|
|
|
|
|
|
:hashchange-listener (gevents/listen js/window goog.events.EventType.HASHCHANGE handler false))]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate this (-get-path this))
|
2020-09-23 10:25:11 +00:00
|
|
|
|
this))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-stop [this]
|
2018-08-27 11:22:38 +00:00
|
|
|
|
(gevents/unlistenByKey popstate-listener)
|
2020-03-05 13:29:34 +00:00
|
|
|
|
(gevents/unlistenByKey hashchange-listener)
|
|
|
|
|
|
nil)
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate [this path]
|
|
|
|
|
|
(reset! last-fragment path)
|
2022-04-05 14:33:25 +00:00
|
|
|
|
(on-navigate (rf/match-by-path router path this) this))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-get-path [this]
|
|
|
|
|
|
;; Remove #
|
|
|
|
|
|
;; "" or "#" should be same as "#/"
|
|
|
|
|
|
(let [fragment (subs (.. js/window -location -hash) 1)]
|
|
|
|
|
|
(if (= "" fragment)
|
|
|
|
|
|
"/"
|
|
|
|
|
|
fragment)))
|
|
|
|
|
|
(-href [this path]
|
|
|
|
|
|
(if path
|
|
|
|
|
|
(str "#" path))))
|
|
|
|
|
|
|
2019-02-08 12:44:28 +00:00
|
|
|
|
(defn- closest-by-tag [el tag]
|
|
|
|
|
|
;; nodeName is upper case for HTML always,
|
|
|
|
|
|
;; for XML or XHTML it would be in the original case.
|
|
|
|
|
|
(let [tag (.toUpperCase tag)]
|
|
|
|
|
|
(loop [el el]
|
2019-02-08 12:47:05 +00:00
|
|
|
|
(if el
|
|
|
|
|
|
(if (= tag (.-nodeName el))
|
|
|
|
|
|
el
|
|
|
|
|
|
(recur (.-parentNode el)))))))
|
2019-02-08 12:44:28 +00:00
|
|
|
|
|
2020-03-05 11:32:48 +00:00
|
|
|
|
(defn- event-target
|
2019-04-10 09:14:21 +00:00
|
|
|
|
"Read event's target from composed path to get shadow dom working,
|
2020-03-05 11:32:48 +00:00
|
|
|
|
fallback to target property if not available"
|
2025-01-22 10:01:10 +00:00
|
|
|
|
[^goog.events.BrowserEvent event]
|
2019-04-12 04:12:46 +00:00
|
|
|
|
(let [original-event (.getBrowserEvent event)]
|
2019-04-10 05:42:09 +00:00
|
|
|
|
(if (exists? (.-composedPath original-event))
|
2019-07-11 08:04:06 +00:00
|
|
|
|
(aget (.composedPath original-event) 0)
|
2019-04-10 05:42:09 +00:00
|
|
|
|
(.-target event))))
|
|
|
|
|
|
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(defn ignore-anchor-click?
|
|
|
|
|
|
"Precicate to check if the anchor click event default action
|
|
|
|
|
|
should be ignored. This logic will ignore the event
|
|
|
|
|
|
if anchor href matches the route tree, and in this case
|
|
|
|
|
|
the page location is updated using History API."
|
2025-01-22 10:01:10 +00:00
|
|
|
|
[router e el ^goog.Uri uri]
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(let [current-domain (if (exists? js/location)
|
2025-01-22 10:01:10 +00:00
|
|
|
|
(.getDomain ^goog.Uri (.parse goog.Uri js/location)))]
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(and (or (and (not (.hasScheme uri)) (not (.hasDomain uri)))
|
|
|
|
|
|
(= current-domain (.getDomain uri)))
|
|
|
|
|
|
(not (.-altKey e))
|
|
|
|
|
|
(not (.-ctrlKey e))
|
|
|
|
|
|
(not (.-metaKey e))
|
|
|
|
|
|
(not (.-shiftKey e))
|
|
|
|
|
|
(or (not (.hasAttribute el "target"))
|
|
|
|
|
|
(contains? #{"" "_self"} (.getAttribute el "target")))
|
|
|
|
|
|
;; Left button
|
|
|
|
|
|
(= 0 (.-button e))
|
|
|
|
|
|
;; isContentEditable property is inherited from parents,
|
|
|
|
|
|
;; so if the anchor is inside contenteditable div, the property will be true.
|
|
|
|
|
|
(not (.-isContentEditable el))
|
2023-03-24 12:29:36 +00:00
|
|
|
|
;; NOTE: Why doesn't this use frontend variant instead of core?
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(reitit/match-by-path router (.getPath uri)))))
|
|
|
|
|
|
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(defrecord Html5History [on-navigate router listen-key click-listen-key]
|
|
|
|
|
|
History
|
|
|
|
|
|
(-init [this]
|
|
|
|
|
|
(let [handler
|
|
|
|
|
|
(fn [e]
|
|
|
|
|
|
(-on-navigate this (-get-path this)))
|
|
|
|
|
|
|
2019-04-26 12:10:17 +00:00
|
|
|
|
ignore-anchor-click-predicate (or (:ignore-anchor-click? this)
|
|
|
|
|
|
ignore-anchor-click?)
|
2018-07-24 10:52:09 +00:00
|
|
|
|
|
|
|
|
|
|
;; Prevent document load when clicking a elements, if the href points to URL that is part
|
|
|
|
|
|
;; of the routing tree."
|
2019-04-26 12:10:17 +00:00
|
|
|
|
ignore-anchor-click (fn [e]
|
2019-08-21 10:43:01 +00:00
|
|
|
|
;; Returns the next matching ancestor of event target
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(when-let [el (closest-by-tag (event-target e) "a")]
|
2025-01-22 10:01:10 +00:00
|
|
|
|
(let [^goog.Uri uri (.parse goog.Uri (.-href el))]
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(when (ignore-anchor-click-predicate router e el uri)
|
|
|
|
|
|
(.preventDefault e)
|
|
|
|
|
|
(let [path (str (.getPath uri)
|
2019-09-20 07:36:08 +00:00
|
|
|
|
(when (.hasQuery uri)
|
|
|
|
|
|
(str "?" (.getQuery uri)))
|
|
|
|
|
|
(when (.hasFragment uri)
|
|
|
|
|
|
(str "#" (.getFragment uri))))]
|
2019-04-26 12:10:17 +00:00
|
|
|
|
(.pushState js/window.history nil "" path)
|
2020-09-23 10:25:11 +00:00
|
|
|
|
(-on-navigate this path))))))
|
|
|
|
|
|
this (assoc this
|
|
|
|
|
|
:listen-key (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
|
|
|
|
|
|
:click-listen-key (gevents/listen js/document goog.events.EventType.CLICK ignore-anchor-click))]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate this (-get-path this))
|
2020-09-23 10:25:11 +00:00
|
|
|
|
this))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate [this path]
|
2022-04-05 14:33:25 +00:00
|
|
|
|
(on-navigate (rf/match-by-path router path this) this))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-stop [this]
|
|
|
|
|
|
(gevents/unlistenByKey listen-key)
|
2020-03-05 13:29:34 +00:00
|
|
|
|
(gevents/unlistenByKey click-listen-key)
|
|
|
|
|
|
nil)
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-get-path [this]
|
2018-08-23 06:54:36 +00:00
|
|
|
|
(str (.. js/window -location -pathname)
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(.. js/window -location -search)
|
|
|
|
|
|
(.. js/window -location -hash)))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-href [this path]
|
|
|
|
|
|
path))
|
2018-06-08 13:00:49 +00:00
|
|
|
|
|
|
|
|
|
|
(defn start!
|
2018-07-24 10:52:09 +00:00
|
|
|
|
"This registers event listeners on HTML5 history and hashchange events.
|
2018-08-27 11:22:21 +00:00
|
|
|
|
|
|
|
|
|
|
Returns History object.
|
|
|
|
|
|
|
2019-05-22 16:58:03 +00:00
|
|
|
|
When using with development workflow like Figwheel, remember to
|
2018-07-11 10:19:24 +00:00
|
|
|
|
remove listeners using stop! call before calling start! again.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters:
|
2018-08-27 11:22:21 +00:00
|
|
|
|
- router The Reitit router.
|
2018-07-24 10:52:09 +00:00
|
|
|
|
- on-navigate Function to be called when route changes. Takes two parameters, ´match´ and ´history´ object.
|
2018-06-08 13:00:49 +00:00
|
|
|
|
|
|
|
|
|
|
Options:
|
2019-04-15 08:40:59 +00:00
|
|
|
|
- :use-fragment (default true) If true, onhashchange and location hash are used to store current route.
|
|
|
|
|
|
|
|
|
|
|
|
Options (Html5History):
|
2019-04-26 12:10:17 +00:00
|
|
|
|
- :ignore-anchor-click? Function (router, event, anchor element, uri) which will be called to
|
|
|
|
|
|
check if the anchor click event should be ignored.
|
|
|
|
|
|
To extend built-in check, you can call `reitit.frontend.history/ignore-anchor-click?`
|
|
|
|
|
|
function, which will ignore clicks if the href matches route tree."
|
2018-07-24 10:52:09 +00:00
|
|
|
|
([router on-navigate]
|
|
|
|
|
|
(start! router on-navigate nil))
|
|
|
|
|
|
([router
|
|
|
|
|
|
on-navigate
|
|
|
|
|
|
{:keys [use-fragment]
|
2019-04-15 08:40:59 +00:00
|
|
|
|
:or {use-fragment true}
|
|
|
|
|
|
:as opts}]
|
|
|
|
|
|
(let [opts (-> opts
|
|
|
|
|
|
(dissoc :use-fragment)
|
|
|
|
|
|
(assoc :router router
|
|
|
|
|
|
:on-navigate on-navigate))]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-init (if use-fragment
|
|
|
|
|
|
(map->FragmentHistory opts)
|
|
|
|
|
|
(map->Html5History opts))))))
|
|
|
|
|
|
|
2021-09-07 11:38:42 +00:00
|
|
|
|
(defn stop!
|
|
|
|
|
|
"Stops the given history handler, removing the event handlers."
|
|
|
|
|
|
[history]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(if history
|
|
|
|
|
|
(-stop history)))
|
2018-06-08 13:00:49 +00:00
|
|
|
|
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(defn
|
|
|
|
|
|
^{:see-also ["reitit.core/match->path"]}
|
|
|
|
|
|
href
|
2021-11-03 10:52:45 +00:00
|
|
|
|
"Generate a URL for a route defined by name, with given path-params and query-params.
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
2021-11-03 10:52:45 +00:00
|
|
|
|
The URL is formatted using Reitit frontend history handler, so using it with
|
|
|
|
|
|
anchor element href will correctly trigger route change event.
|
|
|
|
|
|
|
2025-01-28 13:46:37 +00:00
|
|
|
|
By default currently collections in query parameters are encoded as field-value
|
|
|
|
|
|
pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can
|
|
|
|
|
|
either use Malli coercion to encode values, or just turn the values to strings
|
|
|
|
|
|
before calling the function."
|
2021-09-07 11:38:42 +00:00
|
|
|
|
([history name]
|
|
|
|
|
|
(href history name nil))
|
|
|
|
|
|
([history name path-params]
|
|
|
|
|
|
(href history name path-params nil))
|
|
|
|
|
|
([history name path-params query-params]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(href history name path-params query-params nil))
|
|
|
|
|
|
([history name path-params query-params fragment]
|
2021-09-07 11:38:42 +00:00
|
|
|
|
(let [match (rf/match-by-name! (:router history) name path-params)]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(-href history (rf/match->path match query-params fragment)))))
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
|
|
|
|
|
(defn
|
|
|
|
|
|
^{:see-also ["reitit.core/match->path"]}
|
|
|
|
|
|
push-state
|
2021-11-03 10:52:45 +00:00
|
|
|
|
"Updates the browser URL and pushes new entry to the history stack using
|
|
|
|
|
|
a route defined by name, with given path-params and query-params.
|
|
|
|
|
|
|
|
|
|
|
|
Will also trigger on-navigate callback on Reitit frontend History handler.
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
2025-01-28 13:46:37 +00:00
|
|
|
|
By default currently collections in query parameters are encoded as field-value
|
|
|
|
|
|
pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can
|
|
|
|
|
|
either use Malli coercion to encode values, or just turn the values to strings
|
|
|
|
|
|
before calling the function.
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
|
|
|
|
|
See also:
|
|
|
|
|
|
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
|
|
|
|
|
|
([history name]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(push-state history name nil nil nil))
|
2021-09-07 11:38:42 +00:00
|
|
|
|
([history name path-params]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(push-state history name path-params nil nil))
|
2021-09-07 11:38:42 +00:00
|
|
|
|
([history name path-params query-params]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(push-state history name path-params query-params nil))
|
|
|
|
|
|
([history name path-params query-params fragment]
|
2021-09-07 11:38:42 +00:00
|
|
|
|
(let [match (rf/match-by-name! (:router history) name path-params)
|
2023-03-24 12:29:36 +00:00
|
|
|
|
path (rf/match->path match query-params fragment)]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
;; pushState and replaceState don't trigger popstate event so call on-navigate manually
|
|
|
|
|
|
(.pushState js/window.history nil "" (-href history path))
|
|
|
|
|
|
(-on-navigate history path))))
|
|
|
|
|
|
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(defn
|
|
|
|
|
|
^{:see-also ["reitit.core/match->path"]}
|
|
|
|
|
|
replace-state
|
2021-11-03 10:52:45 +00:00
|
|
|
|
"Updates the browser location and replaces latest entry in the history stack
|
|
|
|
|
|
using URL built from a route defined by name, with given path-params and
|
|
|
|
|
|
query-params.
|
|
|
|
|
|
|
|
|
|
|
|
Will also trigger on-navigate callback on Reitit frontend History handler.
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
2025-01-28 13:46:37 +00:00
|
|
|
|
By default currently collections in query parameters are encoded as field-value
|
|
|
|
|
|
pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can
|
|
|
|
|
|
either use Malli coercion to encode values, or just turn the values to strings
|
|
|
|
|
|
before calling the function.
|
2021-09-07 11:38:42 +00:00
|
|
|
|
|
|
|
|
|
|
See also:
|
|
|
|
|
|
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
|
|
|
|
|
|
([history name]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(replace-state history name nil nil nil))
|
2021-09-07 11:38:42 +00:00
|
|
|
|
([history name path-params]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(replace-state history name path-params nil nil))
|
2021-09-07 11:38:42 +00:00
|
|
|
|
([history name path-params query-params]
|
2023-03-24 12:29:36 +00:00
|
|
|
|
(replace-state history name path-params query-params nil))
|
|
|
|
|
|
([history name path-params query-params fragment]
|
2021-09-07 11:38:42 +00:00
|
|
|
|
(let [match (rf/match-by-name! (:router history) name path-params)
|
2023-03-24 12:29:36 +00:00
|
|
|
|
path (rf/match->path match query-params fragment)]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(.replaceState js/window.history nil "" (-href history path))
|
|
|
|
|
|
(-on-navigate history path))))
|
2023-03-24 09:16:09 +00:00
|
|
|
|
|
|
|
|
|
|
(defn
|
|
|
|
|
|
^{:see-also ["reitit.core/match->path"]}
|
|
|
|
|
|
navigate
|
|
|
|
|
|
"Updates the browser location and either pushes new entry to the history stack
|
|
|
|
|
|
or replaces the latest entry in the the history stack (controlled by
|
|
|
|
|
|
`replace` option) using URL built from a route defined by name given
|
|
|
|
|
|
parameters.
|
|
|
|
|
|
|
|
|
|
|
|
Will also trigger on-navigate callback on Reitit frontend History handler.
|
|
|
|
|
|
|
2025-01-28 13:46:37 +00:00
|
|
|
|
By default currently collections in query parameters are encoded as field-value
|
|
|
|
|
|
pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can
|
|
|
|
|
|
either use Malli coercion to encode values, or just turn the values to strings
|
|
|
|
|
|
before calling the function.
|
2023-03-24 09:16:09 +00:00
|
|
|
|
|
|
|
|
|
|
See also:
|
|
|
|
|
|
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
|
|
|
|
|
|
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
|
|
|
|
|
|
([history name]
|
|
|
|
|
|
(navigate history name nil))
|
2023-03-24 12:29:36 +00:00
|
|
|
|
([history name {:keys [path-params query-params fragment replace] :as opts}]
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(let [match (rf/match-by-name! (:router history) name path-params)
|
2023-03-24 12:29:36 +00:00
|
|
|
|
path (rf/match->path match query-params fragment)]
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(if replace
|
|
|
|
|
|
(.replaceState js/window.history nil "" (-href history path))
|
|
|
|
|
|
(.pushState js/window.history nil "" (-href history path)))
|
|
|
|
|
|
(-on-navigate history path))))
|
|
|
|
|
|
|
|
|
|
|
|
(defn
|
|
|
|
|
|
^{:see-also ["reitit.frontend/set-query-params"]}
|
|
|
|
|
|
set-query
|
|
|
|
|
|
"Update query parameters for the current route.
|
|
|
|
|
|
|
|
|
|
|
|
New query params can be given as a map, or a function taking
|
|
|
|
|
|
the old params and returning the new modified params.
|
|
|
|
|
|
|
2025-01-22 09:10:39 +00:00
|
|
|
|
The current path is matched against the routing tree, and the match data
|
2025-01-22 12:18:54 +00:00
|
|
|
|
(schema, coercion) is used to encode the query parameters.
|
|
|
|
|
|
If the current path doesn't match any route, the query parameters
|
|
|
|
|
|
are parsed from the path without coercion and new values
|
|
|
|
|
|
are also stored without coercion encoding."
|
2023-03-24 09:16:09 +00:00
|
|
|
|
([history new-query-or-update-fn]
|
|
|
|
|
|
(set-query history new-query-or-update-fn nil))
|
|
|
|
|
|
([history new-query-or-update-fn {:keys [replace] :as opts}]
|
|
|
|
|
|
(let [current-path (-get-path history)
|
2025-01-22 12:18:54 +00:00
|
|
|
|
match (rf/match-by-path (:router history) current-path)
|
|
|
|
|
|
new-path (if match
|
|
|
|
|
|
(let [query-params (if (fn? new-query-or-update-fn)
|
|
|
|
|
|
(new-query-or-update-fn (:query (:parameters match)))
|
|
|
|
|
|
new-query-or-update-fn)]
|
|
|
|
|
|
(rf/match->path match query-params (:fragment (:parameters match))))
|
|
|
|
|
|
(rf/set-query-params current-path new-query-or-update-fn))]
|
2023-03-24 09:16:09 +00:00
|
|
|
|
(if replace
|
|
|
|
|
|
(.replaceState js/window.history nil "" (-href history new-path))
|
|
|
|
|
|
(.pushState js/window.history nil "" (-href history new-path)))
|
|
|
|
|
|
(-on-navigate history new-path))))
|