-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
442 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,151 @@ | ||
(ns domina.events | ||
(:require [domina :as domina] | ||
[goog.object :as gobj] | ||
[goog.events :as events])) | ||
|
||
;; Restatement of the the GClosure Event API. | ||
(defprotocol Event | ||
(prevent-default [evt] "Prevents the default action, for example a link redirecting to a URL") | ||
(stop-propagation [evt] "Stops event propagation") | ||
(target [evt] "Returns the target of the event") | ||
(current-target [evt] "Returns the object that had the listener attached") | ||
(event-type [evt] "Returns the type of the the event") | ||
(raw-event [evt] "Returns the original GClosure event")) | ||
|
||
(def builtin-events (set (map keyword (gobj/getValues events/EventType)))) | ||
|
||
;;;;;;;;;;;;;;;;;;; Event Wrappers ;;;;;;;;;;;;;;;;; | ||
|
||
(defn child-of? | ||
"returns true if the node(child) is a child of parent" | ||
[parent child] | ||
(cond | ||
(not child) false | ||
(identical? parent child) false | ||
(identical? (.-parentNode child) parent) true | ||
:else (recur parent (.-parentNode child)))) | ||
|
||
(defn- mouse-enter-leave | ||
"this is used to build cross browser versions of :mouseenter and :mouseleave events" | ||
[func] | ||
(fn [e] | ||
(let [re (.-relatedTarget e) | ||
this (.-currentTarget e)] | ||
(when (and | ||
(not (identical? re this)) | ||
(not (child-of? this re))) | ||
(func e))))) | ||
|
||
|
||
(defn- gen-wrapper | ||
"Generic event wrapper that handles listening and cleanup of wrapped events" | ||
[event-key wrapped-key wrapper-func] | ||
(let [obj (new js/Object) | ||
wevent (name wrapped-key) | ||
event (name event-key)] | ||
(set! (.-wrapped-event obj) wevent) | ||
(set! (.-event obj) event) | ||
(set! (.-listen obj) | ||
(fn [elm func capture opt-scope opt-handler] | ||
(let [callback (wrapper-func func)] | ||
(set! (.-listen callback) func) | ||
(set! (.-scope callback) opt-scope) | ||
(set! (.-event callback) event) | ||
(set! (.-capture callback) capture) | ||
(if op-handler | ||
(.listen opt-handler elm wevent callback capture) | ||
(events/listen elm wevent callback capture))))) | ||
(set! (.-unlisten obj) | ||
(fn [elm func capture opt-scope opt-handler] | ||
(let [listeners (if (= capture js/undefined) | ||
(concat (events/getListeners elm wevent false) | ||
(events/getListeners elm wevent true)) | ||
(events/getListeners elm wevent capture))] | ||
(dorun | ||
(map (fn [obj] | ||
(let[listener (.-listener obj) | ||
lfunc (.-listen listener) | ||
scope (.-scope listener) | ||
capture (.-capture listener)] | ||
(when (and (or (not func) (= lfunc func)) | ||
(or (not opt-scope) (= scope opt-scope))) | ||
(if opt-handler | ||
(.unlisten opt-handler elm wevent listener capture) | ||
(events/unlisten elm wevent listener capture))))) listeners))))) | ||
obj)) | ||
|
||
(def wrapper-register (atom {})) | ||
|
||
;;;;;;;;;;;;;;;;;;; Public API ;;;;;;;;;;;;;;;;; | ||
|
||
(defn reg-event-wrapper! [event-key wrapped-key wrapper-func] | ||
(swap! wrapper-register assoc event-key (gen-wrapper event-key wrapped-key wrapper-func))) | ||
(def root-element (.. js/window -document -documentElement)) | ||
|
||
(defn- find-builtin-type | ||
[evt-type] | ||
(if (contains? builtin-events evt-type) | ||
(name evt-type) | ||
evt-type)) | ||
|
||
;; The listener function will always return true: we ignore the return value | ||
;; of the user-provided function since we don't want consumers to have to worry | ||
;; about that (user-provided functions are usually just a bunch of side effects) | ||
;; User functions can explicitly take control by calling prevent-default or | ||
;; stop-propegation | ||
(defn- create-listener-function | ||
[f] | ||
(fn [evt] | ||
(f (reify | ||
Event | ||
(prevent-default [_] (.preventDefault evt)) | ||
(stop-propagation [_] (.stopPropagation evt)) | ||
(target [_] (.-target evt)) | ||
(current-target [_] (.-currentTarget evt)) | ||
(event-type [_] (.-type evt)) | ||
(raw-event [_] evt) | ||
ILookup | ||
(-lookup [o k] | ||
(if-let [val (aget evt k)] | ||
val | ||
(aget evt (name k)))) | ||
(-lookup [o k not-found] (or (-lookup o k) | ||
not-found)))) | ||
true)) | ||
|
||
(defn- listen-internal! | ||
[content type listener capture once] | ||
(let [f (create-listener-function listener) | ||
t (find-builtin-type type)] | ||
(doall (for [node (domina/nodes content)] | ||
(if once | ||
(events/listenOnce node t f capture) | ||
(events/listen node t f capture)))))) | ||
|
||
(defn listen! | ||
"adding an event to the selected nodes" | ||
([nds event func] (listen! nds event func false)) | ||
([nds event func capture] | ||
(let [wrapper (@wrapper-register event)] | ||
(doseq [node (domina/nodes nds)] | ||
(if (nil? wrapper) | ||
(events/listen node (name event) func capture) | ||
(events/listenWithWrapper node wrapper func capture)))))) | ||
|
||
(defn unlisten! | ||
"removing a specific event from the selected nodes" | ||
([nds event func] (unlisten! nds event func false)) | ||
([nds event func capture] | ||
(let [wrapper (@wrapper-register event)] | ||
(doseq [node (domina/nodes nds)] | ||
(if (nil? wrapper) | ||
(events/unlisten node (name event) func capture) | ||
(.unlisten wrapper node func capture)))))) | ||
|
||
(defn remove-listeners! | ||
"removes all listeners for a given set of events on the selected nodes" | ||
[nds & event-list] | ||
(doseq [node (domina/nodes nds)] | ||
(let [map-func #(let [wrapper (@wrapper-register %)] | ||
(if wrapper | ||
(.unlisten wrapper node) | ||
(events/removeAll node (name %))))] | ||
(doall (map map-func event-list))))) | ||
|
||
(defn fire-listeners! | ||
"fires the listeners attached to a set of nodes" | ||
[nds event capture event-map] | ||
(let [wrapper (@wrapper-register event) | ||
nevent (if wrapper (.-wrapped-event wrapper) (name event)) | ||
event-obj (goog.events.Event. (event-map :type) (event-map :target))] | ||
(set! (.-relatedTarget event-obj) (event-map :related-target)) | ||
(doseq [node (domina/nodes nds)] | ||
(events/fireListeners node nevent capture event-obj)))) | ||
|
||
;;;;;;;;;;;;;;;;;;; Initialization ;;;;;;;;;;;;;;;;; | ||
(reg-event-wrapper! :mouseenter :mouseover mouse-enter-leave) | ||
(reg-event-wrapper! :mouseleave :mouseout mouse-enter-leave) | ||
"Add an event listener to each node in a DomContent. Listens for events during the bubble phase. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element." | ||
([type listener] (listen! root-element type listener)) | ||
([content type listener] | ||
(listen-internal! content type listener false false))) | ||
|
||
(defn capture! | ||
"Add an event listener to each node in a DomContent. Listens for events during the capture phase. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element." | ||
([type listener] (capture! root-element type listener)) | ||
([content type listener] | ||
(listen-internal! content type listener true false))) | ||
|
||
(defn listen-once! | ||
"Add an event listener to each node in a DomContent. Listens for events during the bubble phase. De-registers the listener after the first time it is invoked. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element." | ||
([type listener] (listen-once! root-element type listener)) | ||
([content type listener] | ||
(listen-internal! content type listener false true))) | ||
|
||
(defn capture-once! | ||
"Add an event listener to each node in a DomContent. Listens for events during the capture phase. De-registers the listener after the first time it is invoked. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element." | ||
([type listener] (capture-once! root-element type listener)) | ||
([content type listener] | ||
(listen-internal! content type listener true true))) | ||
|
||
(defn unlisten! | ||
"Removes event listeners from each node in the content. If a listener type is supplied, removes only listeners of that type. If content is omitted, it will remove listeners from the document's root element." | ||
([] (unlisten! root-element)) | ||
([content] | ||
(doseq [node (domina/nodes content)] | ||
(events/removeAll node))) | ||
([content type] | ||
(let [type (find-builtin-type type)] | ||
(events/removeAll node type)))) | ||
|
||
(defn- ancestor-nodes | ||
"Returns a seq of a node and its ancestors, starting with the document root." | ||
([n] (ancestor-nodes n (cons n))) | ||
([n so-far] | ||
(if-let [parent (.-parentNode n)] | ||
(recur parent (cons parent so-far)) | ||
so-far))) | ||
|
||
;; See closure.goog.testing.events.fireBrowserEvent. This function will give us the same | ||
;; bubbling/capturing functionality as W3C DOM level 2 events, on native browser nodes, | ||
;; so we can dispatch against things that do not inherit from goog.event.EventTarget. | ||
(defn dispatch-browser! | ||
"Intended for internal/testing use only. Clients should prefer dispatch!. Dispatches an event as a simulated browser event from the given source node. Emulates capture/bubble behavior. Returns false if any handlers called prevent-default, otherwise true." | ||
[source evt] | ||
(let [ancestors (ancestor-nodes (domina/single-node source))] | ||
;; Capture phase | ||
(doseq [n ancestors] | ||
(when-not (.-propagationStopped n) | ||
(set! (.-currentTarget evt) n) | ||
(events/fireListeners n (.-type evt) true evt))) | ||
;; Bubble phase | ||
(doseq [n (reverse ancestors)] | ||
(when-not (.-propagationStopped n) | ||
(set! (.-currentTarget evt) n) | ||
(events/fireListeners n (.-type evt) false evt))) | ||
(.-returnValue_ evt))) | ||
|
||
(defn dispatch-event-target! | ||
"Intended for internal/testing use only. Clients should prefer dispatch!. Dispatches an event using GClosure's event handling. The event source must extend goog.event.EventTarget" | ||
[source evt] | ||
(events/dispatchEvent source evt)) | ||
|
||
(defn- is-event-target? | ||
"Tests whether the object is a goog.event.EventTarget" | ||
[o] | ||
(and (.-getParentEventTarget o) | ||
(.-dispatchEvent o))) | ||
|
||
(defn dispatch! | ||
"Dispatches an event of the given type, adding the values in event map to the event object. Optionally takes an event source. If none is provided, dispatches on the document's root element. Returns false if any handlers called prevent-default, otherwise true." | ||
([type evt-map] (dispatch! root-element type evt-map)) | ||
([source type evt-map] | ||
(let [evt (events/Event. (find-builtin-type type))] | ||
(doseq [[k v] evt-map] (aset evt k v)) | ||
(if (is-event-target? source) | ||
(dispatch-event-target! source evt) | ||
(dispatch-browser! source evt))))) | ||
|
||
(defn unlisten-by-key! | ||
"Given a listener key, removes the listener." | ||
[key] | ||
(events/unlistenByKey key)) | ||
|
||
(defn get-listeners | ||
"Returns a sequence of event listeners for all the nodes in the | ||
content of a given type." | ||
[content type] | ||
(let [type (find-builtin-type type)] | ||
(mapcat #(events/getListeners % type false) (domina/nodes content)))) |
Oops, something went wrong.