Grubs can be edited by long-pressing

This commit is contained in:
Nicholas Kariniemi 2013-09-01 13:39:35 +03:00
parent f8abd291ef
commit f4eaf7cef9
11 changed files with 372 additions and 195 deletions

View file

@ -1,3 +1,11 @@
html, body {
height: 100%;
min-height: 100%;
width: 100%;
min-width: 100%;
margin: 0px;
}
h3 {
margin-top: 10px;
}
@ -27,16 +35,47 @@ tr:hover .grub-close {
user-select: none;
}
.grub-item:active {
.grub-item.grub-active {
color: #ffffff;
background-color: rgb(71, 73, 73);
}
.grub-item .input-span {
display: block;
overflow: hidden;
}
.grub-item .grub-static {
display: inline;
}
.grub-item .grub-input {
display: none;
}
.grub-item.edit .grub-static {
display: none;
}
.grub-item.edit .grub-input {
display: inline;
width: 100%;
}
.grub-item .glyphicon {
margin-right: 5px;
}
.completed {
text-decoration: line-through;
color: #a9a9a9;
}
.completed.edit {
text-decoration: none;
color: default;
}
#clear-all-btn {
float: right;
}

View file

@ -51,6 +51,18 @@
{:_id (:_id test-grub) :completed false}
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Update"
(it "should update grub info when an update event comes"
(let [test-grub {:_id 123456 :grub "original"}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :update
:_id (:_id test-grub)
:grub "updated"})
(short-delay)
(should=
{:_id (:_id test-grub) :grub "updated"}
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Delete"
(it "should delete a grub when a delete event comes"
(let [test-grub {:_id 123456 :completed true}]

View file

@ -56,22 +56,4 @@
clear-all-event {:event :clear-all}]
(reset! state/grubs [test-grub])
(state/handle-event clear-all-event)
(should= [] @state/grubs)))))
(describe
"view event handling"
(describe "Create"
(it "should add a new grub to the state when a create event comes"
(let [test-grub {:grub "testgrub"}
create-event (assoc test-grub :event :create)]
(state/handle-view-event create-event)
(js/setTimeout (fn [] (let [created-grub (first @state/grubs)]
(should= (:grub test-grub) (:grub created-grub)))))))
(it "should generate an _id for the new grub"
(let [test-grub {:grub "testgrub"}
create-event (assoc test-grub :event :create)]
(state/handle-view-event create-event)
(js/setTimeout (fn []
(let [added-grub (first (filter #(= (:grub %) (:grub test-grub))
@state/grubs))]
(should-not-be-nil (:_id added-grub))))))))))
(should= [] @state/grubs))))))

View file

@ -29,6 +29,11 @@
{:_id (:_id event)}
{mo/$set {:completed false}}))
(defmethod handle-event :update [event]
(mc/update grub-collection
{:_id (:_id event)}
{mo/$set {:grub (:grub event)}}))
(defmethod handle-event :delete [event]
(mc/remove grub-collection {:_id (:_id event)}))

View file

@ -1,7 +1,15 @@
(ns grub.async-utils
(:require [cljs.core.async :as async :refer [<! >! chan put! alts!]])
(:require-macros [cljs.core.async.macros :as m :refer [go]]
[grub.macros :refer [go-loop]]))
(:refer-clojure :exclude [map filter])
(:require [cljs.core.async :as async :refer [<! >! chan put! alts! close!]])
(:require-macros [cljs.core.async.macros :refer [go]]
[grub.macros :refer [do-chan]]))
(defn log [in]
(let [out (chan)]
(do-chan [e in]
(.log js/console e)
(>! out e))
out))
(defn put-all! [cs x]
(doseq [c cs]
@ -22,53 +30,57 @@
(defn fan-in
([ins] (fan-in (chan) ins))
([c ins]
(go-loop
(let [[x] (alts! ins)]
(>! c x)))
c))
([out ins]
(go (loop [ins (vec ins)]
(when (> (count ins) 0)
(let [[x in] (alts! ins)]
(when x
(>! out x)
(recur ins))
(recur (vec (disj (set ins) in))))))
(close! out))
out))
(defn copy-chan
(defn copy
([c]
(first (fan-out c 1)))
([out c]
(first (fan-out c [out]))))
(defn event-chan
([type] (event-chan js/window type))
([el type] (event-chan (chan) el type))
([c el type]
(let [writer #(put! c %)]
(.addEventListener el type writer)
{:chan c
:unsubscribe #(.removeEventListener el type writer)})))
(defn map-chan
([f source] (map-chan (chan) f source))
([c f source]
(go-loop
(>! c (f (<! source))))
c))
(defn filter-chan
([f source] (filter-chan (chan) f source))
([c f source]
(go-loop
(let [v (<! source)]
(when (f v)
(>! c v))))
c))
(defn do-chan! [f source]
(go-loop
(let [v (<! source)]
(f v))))
(defn do-chan [f source]
(defn map [f in]
(let [out (chan)]
(go-loop
(let [v (<! source)]
(f v)
(>! out v)))
(go (loop []
(if-let [x (<! in)]
(do (>! out (f x))
(recur))
(close! out))))
out))
(defn map-filter [f in]
(let [out (chan)]
(go (loop []
(if-let [x (<! in)]
(do
(when-let [val (f x)]
(>! out val))
(recur))
(close! out))))
out))
(defn filter [pred in]
(let [out (chan)]
(go (loop []
(if-let [x (<! in)]
(do (when (pred x) (>! out x))
(recur))
(close! out))))
out))
(defn siphon
([in] (siphon in []))
([in coll]
(go (loop [coll coll]
(if-let [v (<! in)]
(recur (conj coll v))
coll)))))

View file

@ -9,13 +9,14 @@
[cljs.core.async.macros :refer [go]]))
(defn handle-grub-events []
(a/copy-chan state/incoming-view-events view/outgoing-events)
(a/copy-chan state/incoming-events ws/outgoing-events)
(a/copy-chan ws/incoming-events state/outgoing-events))
(a/fan-out view/outgoing-events [state/incoming-events ws/incoming-events])
(a/copy state/incoming-events ws/outgoing-events)
(a/copy ws/incoming-events state/outgoing-events))
(defn init []
(view/init)
(ws/connect-to-server)
(state/init)
(view/init)
(handle-grub-events))
(init)

120
src/cljs/grub/dom.cljs Normal file
View file

@ -0,0 +1,120 @@
(ns grub.dom
(:require [grub.async-utils :as a]
[dommy.core :as dommy]
[cljs.core.async :refer [<! >! chan timeout close!]])
(:require-macros [grub.macros :refer [log logs go-loop]]
[dommy.macros :refer [deftemplate sel1 node]]
[cljs.core.async.macros :refer [go]]))
(defn listen
([el type] (listen el type nil))
([el type f] (listen el type f (chan)))
([el type f out]
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
unlisten #(do (dommy/unlisten! el type push-fn)
(close! out))]
(dommy/listen! el type push-fn)
{:chan out :unlisten unlisten})))
(defn listen-once
([el type] (listen el type nil))
([el type f] (listen el type f (chan)))
([el type f out]
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
unlisten #(do (dommy/unlisten! el type push-fn)
(close! out))]
(dommy/listen-once! el type push-fn)
{:chan out :unlisten unlisten})))
(def add-grub-text
(node [:input.form-control {:id "add-grub-input" :type "text" :placeholder "2 grubs"}]))
(def add-grub-btn
(node [:button.btn.btn-primary {:id "add-grub-btn" :type "button"} "Add"]))
(def clear-all-btn
(node [:button.btn.hidden {:id "clear-all-btn" :type "button"} "Clear all"]))
(defn make-grub-node [grub]
(node [:li.list-group-item.grub-item
{:id (:_id grub)
:class (when (:completed grub) "completed")}
[:span.grub-static
(if (:completed grub)
[:span.glyphicon.glyphicon-check]
[:span.glyphicon.glyphicon-unchecked])
[:span.grub-text (:grub grub)]]
[:input.grub-input {:type "text" :value (:grub grub)}]]))
(defn grubs-selector []
[(sel1 :#grub-list) :.grub-item])
(deftemplate main-template []
[:div.container
[:div.row.show-grid
[:div.col-lg-4]
[:div.col-lg-4
[:h3 "Grub List"]
[:div.input-group
add-grub-text
[:span.input-group-btn
add-grub-btn]]
[:ul#grub-list.list-group]
clear-all-btn]
[:div.col-lg-4]
[:div.col-lg-2]]])
(defn render-body []
(dommy/prepend! (sel1 :body) (main-template)))
(defn render-grub-list [grubs]
(let [grub-list (sel1 :#grub-list)
sorted-grubs (sort-by (juxt :completed :_id) grubs)]
(aset grub-list "innerHTML" "")
(doseq [grub sorted-grubs]
(let [node (make-grub-node grub)]
(dommy/append! grub-list node)))))
(defn get-add-grub-text []
(dommy/value add-grub-text))
(defn clear-add-grub-text []
(dommy/set-value! add-grub-text ""))
(defprotocol IActivatable
(-activate! [view])
(-deactivate! [view]))
(defprotocol IHideable
(-hide! [view])
(-show! [view]))
(defprotocol IEditable
(-set-editing! [view])
(-unset-editing! [view]))
(extend-type js/HTMLElement
IActivatable
(-activate! [view]
(dommy/add-class! view :grub-active))
(-deactivate! [view]
(dommy/remove-class! view :grub-active)))
(extend-type js/HTMLElement
IHideable
(-hide! [view]
(dommy/add-class! view :hidden))
(-show! [view]
(dommy/remove-class! view :hidden)))
(extend-type js/HTMLLIElement
IEditable
(-set-editing! [view]
(-deactivate! view)
(dommy/add-class! view :edit)
(.focus (sel1 view :input)))
(-unset-editing! [view]
(dommy/remove-class! view :edit)))

View file

@ -11,3 +11,13 @@
`(cljs.core.async.macros/go
(while true
~@body)))
(defmacro do-chan [[binding chan] & body]
`(let [chan# ~chan]
(cljs.core.async.macros/go
(loop []
(if-let [~binding (cljs.core.async/<! chan#)]
(do
~@body
(recur))
:done)))))

View file

@ -1,10 +1,10 @@
(ns grub.state
(:require [cljs.core.async :refer [chan <!]])
(:require [grub.async-utils :as a]
[cljs.core.async :refer [chan <!]])
(:require-macros [grub.macros :refer [log logs go-loop]]
[cljs.core.async.macros :refer [go]]))
(def incoming-events (chan))
(def incoming-view-events (chan))
(def outgoing-events (chan))
(def grubs (atom []))
@ -38,6 +38,13 @@
incomplete-grub (assoc grub :completed false)]
(assoc current grub-index incomplete-grub)))))
(defmethod handle-event :update [event]
(swap! grubs
(fn [current]
(let [[grub-index grub] (get-grub-with-index current (:_id event))
updated-grub (assoc grub :grub (:grub event))]
(assoc current grub-index updated-grub)))))
(defmethod handle-event :delete [event]
(swap! grubs
(fn [current]
@ -49,33 +56,9 @@
(defmethod handle-event :unknown-event [event]
(logs "Cannot handle unknown event:" event))
(defn pass-on-view-event [event]
(go (>! incoming-events event))
(go (>! outgoing-events event)))
(defmulti handle-view-event :event :default :unknown-event)
(defmethod handle-view-event :create [event]
(let [create-event (-> event
(assoc :event :add)
(assoc :_id (str "grub-" (.now js/Date)))
(assoc :completed false))]
(pass-on-view-event create-event)))
(defmethod handle-view-event :unknown-event [event]
(pass-on-view-event event))
(defn handle-incoming-events []
(go-loop (handle-event (<! incoming-events))))
(go-loop (let [event (<! incoming-events)]
(handle-event event))))
(defn handle-incoming-view-events []
(go-loop (handle-view-event (<! incoming-view-events))))
(defn init []
(handle-incoming-events)
(handle-incoming-view-events))
(init)
(handle-incoming-events))

View file

@ -1,122 +1,135 @@
(ns grub.view
(:require [grub.async-utils
:refer [do-chan! do-chan event-chan map-chan fan-in filter-chan]]
(:require [grub.async-utils :as a]
[grub.state :as state]
[grub.dom :as dom]
[dommy.core :as dommy]
[cljs.core.async :refer [<! >! chan]])
(:require-macros [grub.macros :refer [log logs go-loop]]
[cljs.core.async :refer [<! >! chan timeout close!]])
(:require-macros [grub.macros :refer [log logs go-loop do-chan]]
[dommy.macros :refer [deftemplate sel1 node]]
[cljs.core.async.macros :refer [go]]))
(def outgoing-events (chan))
(def add-grub-text
(node [:input.form-control {:id "add-grub-input" :type "text" :placeholder "2 grubs"}]))
(def add-grub-btn
(node [:button.btn.btn-primary {:id "add-grub-btn" :type "button"} "Add"]))
(deftemplate main-template []
[:div.container
[:div.row.show-grid
[:div.col-lg-4]
[:div.col-lg-4
[:h3 "Grub List"]
[:div.input-group
add-grub-text
[:span.input-group-btn
add-grub-btn]]
[:ul#grub-list.list-group]
[:button.btn.hidden {:id "clear-all-btn" :type "button"} "Clear all"]]
[:div.col-lg-4]
[:div.col-lg-2]]])
(defn make-grub-node [grub]
(if (:completed grub)
(node [:li.list-group-item.completed.grub-item {:id (:_id grub)}
[:span.glyphicon.glyphicon-check]
(str " " (:grub grub))])
(node [:li.list-group-item.grub-item {:id (:_id grub)}
[:span.glyphicon.glyphicon-unchecked]
(str " " (:grub grub))])))
(defn render-body []
(dommy/prepend! (sel1 :body) (main-template)))
(defprotocol IHideable
(-hide! [view])
(-show! [view]))
(extend-type js/HTMLElement
IHideable
(-hide! [view]
(dommy/add-class! view :hidden))
(-show! [view]
(dommy/remove-class! view :hidden)))
(defn get-add-grub-text []
(let [text (dommy/value add-grub-text)]
(dommy/set-value! add-grub-text "")
text))
(defn get-grubs-from-clicks []
(let [out (chan)]
(dommy/listen! add-grub-btn :click #(go (>! out (get-add-grub-text))))
out))
(defn put-grubs-if-enter-pressed [out event]
(when (= (.-keyIdentifier event) "Enter")
(go (>! out (get-add-grub-text)))))
(->> (:chan (dom/listen dom/add-grub-btn :click))
(a/map #(dom/get-add-grub-text))))
(defn get-grubs-from-enter []
(let [out (chan)]
(dommy/listen! add-grub-text
:keyup
(partial put-grubs-if-enter-pressed out))
out))
(->> (:chan (dom/listen dom/add-grub-text :keyup))
(a/filter #(= (.-keyIdentifier %) "Enter"))
(a/map #(dom/get-add-grub-text))))
(defn get-added-events []
(let [grubs (fan-in [(get-grubs-from-clicks)
(get-grubs-from-enter)])]
(defn get-created-events []
(let [grubs (a/fan-in [(get-grubs-from-clicks)
(get-grubs-from-enter)])]
(->> grubs
(filter-chan #(not (empty? %)))
(map-chan (fn [g] {:event :create :grub g})))))
(a/filter #(not (empty? %)))
(a/map (fn [g] {:grub g})))))
(defn get-completed-event [event]
(defn get-clear-all-events []
(:chan (dom/listen dom/clear-all-btn :click)))
(defn get-grub-mousedown-events []
(:chan (dom/listen (dom/grubs-selector) :mousedown)))
(defn get-grub-mouseup-events []
(:chan (dom/listen (dom/grubs-selector) :mouseup)))
(defn get-grub-mouseleave-events []
(:chan (dom/listen (dom/grubs-selector) :mouseleave)))
(defn get-body-clicks []
(:chan (dom/listen (sel1 :body) :click)))
(defn get-enters []
(->> (:chan (dom/listen (sel1 :body) :keyup))
(a/filter #(= (.-keyIdentifier %) "Enter"))))
(defn parse-completed-event [event]
(let [target (.-target event)
id (.-id target)
completed (dommy/has-class? target "completed")
event-type (if completed :uncomplete :complete)]
{:_id id :event event-type}))
(defn get-clear-all-events []
(let [events (chan)]
(dommy/listen! (sel1 :#clear-all-btn) :click #(go (>! events {:event :clear-all})))
events))
(defn render-grub-list [grubs]
(let [grub-list (sel1 :#grub-list)
sorted-grubs (sort-by (juxt :completed :_id) grubs)]
(aset grub-list "innerHTML" "")
(doseq [grub sorted-grubs]
(let [node (make-grub-node grub)]
(dommy/listen! node :click #(go (>! outgoing-events (get-completed-event %))))
(dommy/append! grub-list node)))))
(defn push-outgoing-events []
(fan-in outgoing-events [(get-added-events)
(get-clear-all-events)]))
(defn watch-for-state-changes []
(defn re-render-when-state-changes []
(add-watch state/grubs
:grub-add-watch
(fn [key ref old new]
(if (empty? new)
(-hide! (sel1 :#clear-all-btn))
(-show! (sel1 :#clear-all-btn)))
(render-grub-list new))))
(dom/-hide! dom/clear-all-btn)
(dom/-show! dom/clear-all-btn))
(dom/render-grub-list new))))
(defn event-loop []
(let [out (chan)
created (get-created-events)
clear-all (get-clear-all-events)
mousedown (get-grub-mousedown-events)
mouseup (get-grub-mouseup-events)
mouseleave (get-grub-mouseleave-events)
body-click (get-body-clicks)
edit (chan)
enter (get-enters)]
(go (loop [mousedown-time nil
edit-elem nil]
(let [[event c] (alts! [created clear-all mousedown
mouseup mouseleave edit body-click
enter])]
(if edit-elem
(cond
(or (and (= c body-click)
(not (dommy/descendant? (.-target event) edit-elem)))
(= c enter))
(do (dom/-unset-editing! edit-elem)
(let [grub-text (.-value (sel1 edit-elem :.grub-input))
id (.-id edit-elem)]
(>! out {:event :update
:grub grub-text
:_id id}))
(recur nil nil))
:else (recur nil edit-elem))
(cond
(= c created)
(let [add-event (-> event
(assoc :event :add)
(assoc :_id (str "grub-" (.now js/Date)))
(assoc :completed false))]
(>! out add-event)
(dom/clear-add-grub-text)
(recur mousedown-time edit-elem))
(= c clear-all)
(do (>! out {:event :clear-all})
(recur mousedown-time edit-elem))
(= c mousedown)
(do (dom/-activate! (.-selectedTarget event))
(let [now (.now js/Date)]
(go (<! (timeout 500))
(>! edit {:mousedown-time now :elem (.-selectedTarget event)}))
(recur now edit-elem)))
(= c mouseup)
(do (dom/-deactivate! (.-selectedTarget event))
(>! out (parse-completed-event event))
(recur nil edit-elem))
(= c mouseleave)
(do (dom/-deactivate! (.-selectedTarget event))
(recur nil edit-elem))
(= c edit)
(if (and mousedown-time (= (:mousedown-time event) mousedown-time))
(do (dom/-set-editing! (:elem event))
(recur nil (:elem event)))
(recur nil edit-elem))
:else (recur mousedown-time edit-elem))))))
out))
(defn init []
(render-body)
(watch-for-state-changes)
(push-outgoing-events))
(dom/render-body)
(re-render-when-state-changes)
(a/copy outgoing-events (event-loop)))

View file

@ -22,8 +22,8 @@
(go (>! outgoing-events grub-event))))))
(defn connect-to-server []
(let [full-url (str "ws://" (.-host (.-location js/document)) "/ws")]
(reset! websocket* (js/WebSocket. full-url))
(let [server-url (str "ws://" (.-host (.-location js/document)) "/ws")]
(reset! websocket* (js/WebSocket. server-url))
(aset @websocket* "onopen" (fn [event] (log "Connected:" event)))
(aset @websocket* "onclose" (fn [event] (log "Connection closed:" event)))
(aset @websocket* "onerror" (fn [event] (log "Connection error:" event)))