Om keymap

Om is a thin React wrapper for Clojure. Let’s design a keymap component today. A keymap component allows us to add some useful key binding as shortcut.

At first, we need a global core.async channel to dispatch events. UsuallyU we define some keymap groups like ctrl+b ctrl+v, we actually need a core.async sliding-buffer.

(def key-chan
  (chan (sliding-buffer 1000)))

(defn start-listen-keydown-event [key-chan]
  (dommy/listen! js/document :keydown
    #(when-let [key %]
      (put! key-chan key))))

(start-listen-keydown-event key-chan)

This channel do listen all js.document keydown events, and then put them into self.

We then define an Om component:

(defn handler
  [_ owner {:keys [keymap]}]
  (reify
    om/IDisplayName
    (display-name [_] "KeyboardHandler")
    om/IRender
    (render [_] (dom/span)))

(def state (atom {}))
(def keymap (atom {}))
(om/root handler @state {:opts {:keymap keymap}})

For now, it do nothing but insert a <span></span> tag into html. The component mounts to the span dom before rendering.

Next, we will add a go-loop to this component. It will waiting key-chan inputs and try to compose a meaningful keymap if we pre-defined. Here we simply print event key into console.

om/IDidMount
(did-mount [_]
  (let [ch (chan)]
    (om/set-state! owner :ch ch)
    (tap key-mult ch)
    (go-loop [waiting-keys []
              t-chan nil]
      (let [t-ch (or t-chan (chan))
            [e read-ch] (alts! [ch t-ch])]
        (if (= read-ch ch)
          (let [all-keys (conj waiting-keys e)]
            (if-let [key-fn (match-keys @keymap all-keys)]
             (do
               (key-fn e)
               (print (event->key e))
               (recur [] nil))
             (recur all-keys (timeout 1000))))
          (recur [] nil))))))

om/IWillUnmount
(will-unmount [_]
  (untap key-mult (om/get-state owner :ch)))

we create a channel waiting keys

Before running it, add some helper functions:

(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."
  {9 "tab"
  13 "enter"
  27 "esc"
  37 "left"
  38 "up"
  39 "right"
  40 "down"
  46 "del"
  186 ";"
  191 "slash"})

(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)))))

(defn match-keys [keymap keys]
  identity)

Om Keyboard