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."
|
2018-06-08 13:00:49 +00:00
|
|
|
|
(:require [reitit.core :as reitit]
|
2018-07-11 06:52:35 +00:00
|
|
|
|
[reitit.core :as r]
|
2018-07-12 19:46:25 +00:00
|
|
|
|
[reitit.frontend :as rf]
|
2019-02-08 11:57:03 +00:00
|
|
|
|
[goog.events :as gevents])
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(:import goog.Uri))
|
|
|
|
|
|
|
|
|
|
|
|
(defprotocol History
|
|
|
|
|
|
(-init [this] "Create event listeners")
|
|
|
|
|
|
(-stop [this] "Remove event listeners")
|
|
|
|
|
|
(-on-navigate [this path])
|
|
|
|
|
|
(-get-path [this])
|
|
|
|
|
|
(-href [this path]))
|
|
|
|
|
|
|
|
|
|
|
|
;; 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))
|
|
|
|
|
|
(-on-navigate this path))))]
|
|
|
|
|
|
(-on-navigate this (-get-path this))
|
|
|
|
|
|
(assoc this
|
2018-08-27 11:22:38 +00:00
|
|
|
|
: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
|
|
|
|
(-stop [this]
|
2018-08-27 11:22:38 +00:00
|
|
|
|
(gevents/unlistenByKey popstate-listener)
|
|
|
|
|
|
(gevents/unlistenByKey hashchange-listener))
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate [this path]
|
|
|
|
|
|
(reset! last-fragment path)
|
|
|
|
|
|
(on-navigate (rf/match-by-path router path) this))
|
|
|
|
|
|
(-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
|
|
|
|
|
2019-04-10 05:42:09 +00:00
|
|
|
|
(defn- event-target [event]
|
2019-04-10 09:14:21 +00:00
|
|
|
|
"Read event's target from composed path to get shadow dom working,
|
|
|
|
|
|
fallback to target property if not available"
|
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."
|
|
|
|
|
|
[router e el uri]
|
|
|
|
|
|
(let [current-domain (if (exists? js/location)
|
|
|
|
|
|
(.getDomain (.parse Uri js/location)))]
|
|
|
|
|
|
(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))
|
|
|
|
|
|
(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")]
|
|
|
|
|
|
(let [uri (.parse Uri (.-href el))]
|
|
|
|
|
|
(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)
|
|
|
|
|
|
(-on-navigate this path))))))]
|
2018-07-24 10:52:09 +00:00
|
|
|
|
(-on-navigate this (-get-path this))
|
|
|
|
|
|
(assoc this
|
|
|
|
|
|
:listen-key (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
|
2018-08-23 06:55:14 +00:00
|
|
|
|
: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 path]
|
|
|
|
|
|
(on-navigate (rf/match-by-path router path) this))
|
|
|
|
|
|
(-stop [this]
|
|
|
|
|
|
(gevents/unlistenByKey listen-key)
|
|
|
|
|
|
(gevents/unlistenByKey click-listen-key))
|
|
|
|
|
|
(-get-path [this]
|
2018-08-23 06:54:36 +00:00
|
|
|
|
(str (.. js/window -location -pathname)
|
|
|
|
|
|
(.. js/window -location -search)))
|
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))))))
|
|
|
|
|
|
|
|
|
|
|
|
(defn stop! [history]
|
|
|
|
|
|
(if history
|
|
|
|
|
|
(-stop history)))
|
2018-06-08 13:00:49 +00:00
|
|
|
|
|
|
|
|
|
|
(defn href
|
2018-07-24 10:52:09 +00:00
|
|
|
|
([history k]
|
|
|
|
|
|
(href history k nil))
|
|
|
|
|
|
([history k params]
|
|
|
|
|
|
(href history k params nil))
|
|
|
|
|
|
([history k params query]
|
|
|
|
|
|
(let [match (rf/match-by-name! (:router history) k params)]
|
|
|
|
|
|
(-href history (r/match->path match query)))))
|
|
|
|
|
|
|
|
|
|
|
|
(defn push-state
|
2018-07-12 19:48:57 +00:00
|
|
|
|
"Sets the new route, leaving previous route in history."
|
2018-07-24 10:52:09 +00:00
|
|
|
|
([history k]
|
|
|
|
|
|
(push-state history k nil nil))
|
|
|
|
|
|
([history k params]
|
|
|
|
|
|
(push-state history k params nil))
|
|
|
|
|
|
([history k params query]
|
|
|
|
|
|
(let [match (rf/match-by-name! (:router history) k params)
|
|
|
|
|
|
path (r/match->path match query)]
|
|
|
|
|
|
;; 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))))
|
|
|
|
|
|
|
|
|
|
|
|
(defn replace-state
|
2018-07-12 19:48:57 +00:00
|
|
|
|
"Replaces current route. I.e. current route is not left on history."
|
2018-07-24 10:52:09 +00:00
|
|
|
|
([history k]
|
|
|
|
|
|
(replace-state history k nil nil))
|
|
|
|
|
|
([history k params]
|
|
|
|
|
|
(replace-state history k params nil))
|
|
|
|
|
|
([history k params query]
|
|
|
|
|
|
(let [match (rf/match-by-name! (:router history) k params)
|
|
|
|
|
|
path (r/match->path match query)]
|
|
|
|
|
|
(.replaceState js/window.history nil "" (-href history path))
|
|
|
|
|
|
(-on-navigate history path))))
|