Skip to content

Instantly share code, notes, and snippets.

@mathieulegrand
Last active August 29, 2015 14:21
Show Gist options
  • Save mathieulegrand/b9da9e102dff3c454e30 to your computer and use it in GitHub Desktop.
Save mathieulegrand/b9da9e102dff3c454e30 to your computer and use it in GitHub Desktop.
Editable DIV as a IRC style input bar (textarea replacement) in ClojureScript
(ns potato.keyboard
(:require [clojure.string]
[goog.dom]
[goog.style]
[goog.events]
[goog.dom.xml]
[goog.dom.classlist]
[goog.dom.selection]
[goog.testing.events]
[goog.events.EventTarget]
[goog.events.KeyHandler]
[goog.events.PasteHandler]
[goog.events.BrowserEvent]
[goog.dom.Range]
[goog.editor.node]
[goog.editor.range]
[goog.Uri]))
(def ENTER 13)
(def ESC 27)
(defn has-keyboard []
"test whether ?keyboard=1 is set as an URL parameter"
(let [param (clojure.string/lower-case (or (.getParameterValue (goog.Uri/parse js/location) "keyboard") ""))]
(or (= param "1") (= param "yes") (= param "true"))))
(defn content [editable]
(goog.dom/getRawTextContent (:div @editable)))
(defn empty! [editable]
(goog.dom/setTextContent (:div @editable) "")
true)
(defn destroy! [editable]
(swap! editable assoc :active false)
(when (:placeholder @editable)
(goog.dom/removeNode (:placeholder @editable)))
(goog.dom/removeNode (:div @editable))
(swap! (:keyhandler @editable) update-in [:editables-list] (fn [alist] (remove #(= editable %) alist))))
(defn set-placeholder! [editable placeholder-text]
(when (:active @editable)
(when (and (not (:placeholder @editable)) placeholder-text)
(let [editable-div (:div @editable)
placeholder (goog.dom/createDom "span" #js {:className "placeholder"
:onClick #(.focus editable-div)} placeholder-text)]
(when-not (> (count (content editable)) 0)
(goog.dom/insertSiblingBefore placeholder editable-div)
(swap! editable assoc :placeholder placeholder)))
(js/setTimeout #(set-placeholder! editable placeholder-text) 10000))))
(defn reset-cursor! [keyhandler & [editable]]
"Place cursor in editable (or default) at the end of the content"
(let [editable-div (or editable (:div (deref (:default-editable @keyhandler))))
cursor-position (goog.editor.node/getRightMostLeaf editable-div)]
(if (= cursor-position editable-div)
(.select (goog.dom.Range/createCaret editable-div 0))
(goog.editor.range/placeCursorNextTo (goog.editor.node/getRightMostLeaf editable-div) false))))
(defn really-focus [mydiv & {:keys [timeout] :or [timeout 100]}]
"Really focus a given DIV by calling the Javascript focus now and after a timeout"
(.focus mydiv)
(js/setTimeout #(.focus mydiv) timeout))
(defn append-editable-div [opts]
"append an editable div channel-input within the :parent-node DOM element"
;; e.g. (append-editable-div {:id "channel-input"
;; :keyboard (potato.keyboard/init dom-body)
;; :text-content "Default content"
;; :parent-node dom-node})
(let [keyhandler (:keyboard opts)
editable-div (goog.dom/createDom "div" #js {:id (or (:id opts) "channel-input")
:spellcheck (or (:spellcheck opts) true)
:contentEditable true
:role "textbox"} (:text-content opts))]
(goog.dom/append (:parent-node opts) editable-div)
(let [paste-event-handler (goog.events.PasteHandler. editable-div)]
(goog.events/listen paste-event-handler (.-PASTE goog.events.PasteHandler/EventType)
(fn [event] (.preventDefault event)
(let [clipboard-text (.getData (.-clipboardData (.getBrowserEvent event)) "text/plain")]
(.execCommand js/document "insertText" false clipboard-text)))))
(really-focus editable-div)
(reset-cursor! keyhandler editable-div)
(let [editable-definition (atom {:active true
:locked false
:saved-opts opts
:keyhandler keyhandler
:div editable-div
:typing-callback (:typing-callback opts)
:new-size-callback (:new-size-callback opts)
:previous-height (.-height (goog.style/getSize editable-div))})]
(when (:placeholder-text opts)
(set-placeholder! editable-definition (:placeholder-text opts)))
(if (= (count (:editables-list @keyhandler)) 0)
(swap! keyhandler assoc :default-editable editable-definition))
(swap! keyhandler update-in [:editables-list] #(conj % editable-definition))
editable-definition)))
(defn set-callback-for-editable [editable event callback]
"Add a callback for pre-defined event on the given editable"
;; e.g. (set-callback-for-editable editable ENTER #(print "ENTER"))
(swap! editable assoc event callback))
(defn- switch-editable [& {:keys [enable disable] :or {enable false disable false}}]
"unlocking or locking the editable passed as :enable or :disable"
(when-let [dom-node (:div (deref (or enable disable)))]
;(.log js/console (if enable "unlocking" "locking") dom-node)
(goog.dom.xml/setAttributes dom-node #js {:contentEditable (not disable)})
(if disable
(goog.dom.classlist/add dom-node "locked")
(goog.dom.classlist/remove dom-node "locked")))
(swap! (or enable disable) assoc :locked (not enable)))
(defn- switch-editables [keyhandler mode & [editable]]
"proxy function for enable and disable, mode is :enable or :disable"
(if editable
(doseq [elem (:editables-list @keyhandler)] (if (= editable elem) (switch-editable mode elem)))
(doseq [elem (:editables-list @keyhandler)] (switch-editable mode elem))))
(defn disable [keyhandler & [editable]]
"Prevent the editable div from receiving input"
(switch-editables keyhandler :disable editable))
(defn enable [keyhandler & [editable]]
"Enable the editable div to receive input"
(switch-editables keyhandler :enable editable))
(defn- is-platform-special? [event]
"Return TRUE if we detect Ctrl-TAB, Meta-TAB, Ctrl-C, Meta-C"
(and (.-platformModifierKey event)
(or (= (or (and goog.userAgent.MAC (.-META goog.events/KeyCodes))
(.-CTRL goog.events/KeyCodes)) (.-keyCode event))
(= 99 (.-keyCode event)) ;; upper-case C letter
(= 67 (.-keyCode event))))) ;; lower-case c letter
(defn- match-action [event editable])
(defn- act-on-editable-if-matching [event editable]
;; the editable needs to be active, not locked, and to match our target
(when (or (and (:active @editable) (not (:locked @editable))
(= (.-target event) (:div @editable))))
;; a key was typed in this input area, remove the placeholder
(when (:placeholder @editable)
(goog.dom/removeNode (:placeholder @editable))
(swap! editable dissoc :placeholder))
;; send a "is typing..." notification back via the callback if defined
(let [typing-callback (:typing-callback @editable)]
(if typing-callback (typing-callback)))
;; check whether the size has changed and callback when it did
(let [current-height (.-height (goog.style/getSize (:div @editable)))]
(when-not (= current-height (:previous-height @editable))
(swap! editable assoc :previous-height current-height)
(let [new-size-callback (:new-size-callback @editable)]
(if new-size-callback (new-size-callback)))))
;; call the specific keys callback if defined
(condp = (.-keyCode event)
ENTER (if-not (or (.-shiftKey event) (.-repeat event) (.-ctrlKey event))
(let [on-enter (get @editable ENTER)]
(when on-enter (.preventDefault event) (on-enter))))
ESC (let [on-escape (get @editable ESC)]
(when on-escape (.preventDefault event) (on-escape)))
(match-action event editable))
;; return true when we matched the div
true))
(defn- on-key-event [event keyhandler]
(let [current-target-tag (clojure.string/lower-case (.-tagName (.-target event)))
editables (:editables-list @keyhandler)]
;; ignore legacy targets, i.e. the search box and platform specific combos (Ctrl-TAB, Ctrl-V)
(when-not (or (is-platform-special? event) (= current-target-tag "input") (= current-target-tag "textarea"))
;; look for one single editable that's active and targetted
(if-not (some #(act-on-editable-if-matching event %) editables)
;; if we didn't find any, then try to move to the default target instead
(let [default-target (deref (:default-editable @keyhandler))
new-target-div (:div default-target)]
(when (and (:active default-target) (not (:locked default-target)))
(really-focus new-target-div)
(reset-cursor! keyhandler new-target-div)
;; this does not work well on Firefox: the character doesn't get inserted in the div
(goog.testing.events/fireKeySequence new-target-div (.-keyCode event))))))))
(defn init [root-node]
"initialise a keyhandler for the elements contained in the DOM root-node"
(let [listenable (goog.events.KeyHandler. root-node)
keyhandler (atom {:editables-list []
:listenable listenable})]
(goog.events/listen listenable (.-KEY goog.events.KeyHandler/EventType) #(on-key-event % keyhandler) true)
(goog.events/listen root-node (.-BLUR goog.events/EventType) #())
keyhandler))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment