Skip to content

Instantly share code, notes, and snippets.

@tomconnors
Created January 16, 2014 18:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomconnors/8460406 to your computer and use it in GitHub Desktop.
Save tomconnors/8460406 to your computer and use it in GitHub Desktop.
Handling keyboard input with React and clojurescript
;;; I'm not using Om yet because I've got a lot of code using Pump and just haven't made the transition,
;;; but the principles are all the same. So you'll see Pump-specific code here.
(def code->key
"map from a character code (read from events with event.which)
to a string representation of it.
Only need to add 'special' things here."
{13 "enter"
37 "left"
38 "up"
39 "right"
40 "down"
46 "del"
186 ";"})
(defn event-modifiers
"Given a keydown event, return the modifier keys that were being held."
[e]
(into [] (filter identity [(if (.-shiftKey e) "shift")
(if (.-altKey e) "alt")
(if (.-ctrlKey e) "ctrl")
(if (.-metaKey e) "meta")])))
(def mod-keys
"A vector of the modifier keys that we use to compare against to make
sure that we don't report things like pressing the shift key as independent events.
This may not be desirable behavior, depending on the use case, but it works for
what I need."
[;; shift
(js/String.fromCharCode 16)
;; ctrl
(js/String.fromCharCode 17)
;; alt
(js/String.fromCharCode 18)
])
(defn event->key
"Given an event, return a string like 'up' or 'shift+l' or 'ctrl+;'
describing the key that was pressed.
This fn will never return just 'shift' or any other lone modifier key."
[event]
(let [mods (event-modifiers event)
which (.-which event)
key (or (code->key which) (.toLowerCase (js/String.fromCharCode which)))]
(if (and key (not (empty? key)) (not (some #{key} mod-keys)))
(join "+" (conj mods key)))))
(def keyboard-q
"An observable queue that gets all keydown events put'ed onto it.
Observable queues are implemented elsewhere, but they're pretty simple.
If you want to see an implementation, ask me about it."
(queue/observable-queue (chan)))
;; listen for keydown events and put them onto the queue.
(listen! dom/body "keydown" (fn [e] (queue/put! keyboard-q e)))
(defn match-keys
"Given the props of the component and the most recent series of keys that
were pressed (not the codes, but strings like 'shift+r' and stuff)
return a handler fn associated with a key combo in the keys list
or nil."
[props keys]
(loop [;; :shortcuts of props is a vector that looks like
;; ["keycombo" handler-fn "another-key-combo" another-handler]
;; or
;; [["several" "combos" "with" "same" "handler"] the-handler]
;; Partition it to get a list like [[combo handler][combo handler]]
combos (partition 2 (:shortcuts props))]
(if-not (empty? combos)
(let [[combo handler] (first combos)
;; make all the combos vectors so we can treat them all
;; the same.
combo (if (coll? combo) combo [combo])]
;; loop over the keys that have been pressed and check whether
;; the list or any tail of the list contains the a key combo from the shortcuts
(if (loop [keys keys]
(if (empty? keys)
false
(if (some #(= keys (split % #" ")) combo)
true
(recur (rest keys)))))
handler
(recur (rest combos)))))))
;;; should eventually be a mixin, probably.
;;; defr means define react component
(defr KeyboardHandler
:component-did-mount
(fn [c]
(let [ch (chan)]
;; set the channel as a prop of this component so we can refer
;; to it at clean up time.
(set! (.-chan c) ch)
(queue/subscribe keyboard-q ch)
(go
(loop [;; waiting keys are keys that were recently pressed
waiting-keys []
;; t-chan is either nil or a timeout channel that closes
;; when there's no keyboard input for some amount of time.
t-chan nil]
(let [t-chan (or t-chan (chan))
;; read from either the keyboard input channel or the
;; timeout channel.
[e read-chan] (alts! [ch t-chan])]
;; if we read from the keyboard input channel, handle the
;; keyboard event (or do nothing if we read nil - that
;; indicates that the channel closed, which means the
;; component is unmounting.)
(if (= read-chan ch)
(if e
(if-let [;; get the key for the keydown, or nil if it
;; was a lone midifier key.
e-key (event->key e)]
(let [;; make a vec of all the recently pressed keys,
;; including the new one.
all-keys (conj waiting-keys e-key)]
(if-let [;; if there are any handlers registered
;; for a key combo in all-keys, call it.
matching-fn (match-keys (.-props c) all-keys)]
(do (matching-fn e)
;; after calling the matching fn, recur so
;; we're ready for the next key event.
(recur [] nil))
;; if there was no matching fn, recur with the
;; list of recently struck keys and a timeout.
;; If the timeout channel closes before another
;; key is struck, all previously struck keys are forgotten.
(recur all-keys (async/timeout 1000))))
;; no key matches the event (it was probably a
;; modifier key, or some other thing I didn't think
;; of.)
;; Recur without changing anything.
(recur waiting-keys t-chan)))
;; we read from the timeout-chan. Forget all waiting keys.
(recur [] nil)))))))
:component-will-unmount (fn [c]
;; cleanup.
(let [ch (.-chan c)]
(queue/unsubscribe! keyboard-q ch)
(async/close! ch)))
;; this is the render function.
;; it just returns a hidden span.
[component properties state] [:span.hidden])
;;;elsewhere:
(defr SomeGreatComponent [c p s]
[:div
[KeyboardHandler {:shortcuts ["shift+right" (fn [e] (log "got a shift+right key combo event" e))]]])
@afhammad
Copy link

Any reason you aren't using goog.ui.KeyboardShortcutHandler? Any idea if it would it play nicely with React's event management system?

@sudodoki
Copy link

sudodoki commented Sep 8, 2014

Any reason you didn't put require with 'queue' stuff into the gist? Cannot really run this =(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment