Skip to content

Commit

Permalink
feat(blocks): copy, cut, paste for inline textarea (#333)
Browse files Browse the repository at this point in the history
* feat(blocks): copy, cut, paste for inline textarea

* pasting blocks actually works

* do buggy copy and cut

* lint

* fix: add head and tail to bold and italics selection
  • Loading branch information
tangjeff0 authored Aug 13, 2020
1 parent b570f6c commit 15de32e
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 41 deletions.
2 changes: 2 additions & 0 deletions .carve_ignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ athens.views.right-sidebar/sidebar-section-heading-style

;; for future use
athens.db/v-by-ea
athens.keybindings/is-character-key?
athens.keybindings/write-char
8 changes: 7 additions & 1 deletion src/cljs/athens/db.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,13 @@
[(inc ?o) ?new-o]]
[(dec-after ?p ?at ?ch ?new-o)
(after ?p ?at ?ch ?o)
[(dec ?o) ?new-o]]])
[(dec ?o) ?new-o]]
[(plus-after ?p ?at ?ch ?new-o ?x)
(after ?p ?at ?ch ?o)
[(+ ?o ?x) ?new-o]]
[(minus-after ?p ?at ?ch ?new-o ?x)
(after ?p ?at ?ch ?o)
[(- ?o ?x) ?new-o]]])


(defn sort-block-children
Expand Down
37 changes: 37 additions & 0 deletions src/cljs/athens/events.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
(assoc db :selected/items new-vec))))


;; TODO: minus-after to reindex but what about nested blocks?
(reg-event-fx
:selected/delete
(fn [{:keys [db]} [_ selected-items]]
Expand Down Expand Up @@ -340,6 +341,15 @@
@db/dsdb rules eid order)))


(defn plus-after
[eid order x]
(->> (d/q '[:find ?ch ?new-o
:keys db/id block/order
:in $ % ?p ?at ?x
:where (plus-after ?p ?at ?ch ?new-o ?x)]
@db/dsdb rules eid order x)))


(reg-event-fx
:up
(fn [_ [_ uid]]
Expand Down Expand Up @@ -600,6 +610,33 @@
(drop-bullet source-uid target-uid kind)))


;; TODO: convert to tree instead of flat map (handling indentation), write tests for markdown list parsing
(reg-event-fx
:paste
(fn [_ [_ uid text]]
(let [lines (clojure.string/split-lines text)
block (db/get-block [:block/uid uid])
{b-order :block/order} block
parent (db/get-parent [:block/uid uid])
{p-id :db/id} parent
now (now-ts)
new-datoms (map-indexed (fn [i x]
(let [start (subs x 0 2)
s (if (or (= start "- ")
(= start "* "))
(subs x 2)
x)]
{:block/uid (gen-block-uid)
:create/time now
:edit/time now
:block/order (+ 1 i b-order)
:block/string s}))
lines)
reindex (plus-after p-id b-order (count lines))
children (concat new-datoms reindex)]
{:dispatch [:transact [{:db/id p-id :block/children children}]]})))


(defn left-sidebar-drop-above
[s-order t-order]
(let [source-eid (d/q '[:find ?e .
Expand Down
34 changes: 10 additions & 24 deletions src/cljs/athens/keybindings.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -295,29 +295,13 @@


;; TODO: it's ctrl for windows and linux right?
(defn handle-system-shortcuts
"Assumes meta is selected"
(defn handle-shortcuts
[e _ state]
(let [{:keys [key-code target end selection]} (destruct-event e)]
(let [{:keys [key-code head tail selection]} (destruct-event e)]
(cond
(= key-code KeyCodes.A) (do (setStart target 0)
(setEnd target end))

;; TODO: undo. conflicts with datascript undo
(= key-code KeyCodes.Z) (prn "undo")

;; TODO: cut
(= key-code KeyCodes.X) (prn "cut")

;; TODO: paste. magical
(= key-code KeyCodes.V) (prn "paste")

;; TODO: bold
(= key-code KeyCodes.B) (let [new-str (surround selection "**")]
(= key-code KeyCodes.B) (let [new-str (str head (surround selection "**") tail)]
(swap! state assoc :atom-string new-str))

;; TODO: italicize
(= key-code KeyCodes.I) (let [new-str (surround selection "__")]
(= key-code KeyCodes.I) (let [new-str (str head (surround selection "__") tail)]
(swap! state assoc :atom-string new-str)))))


Expand Down Expand Up @@ -426,18 +410,20 @@
;; XXX: what happens here when we have multi-block selection? In this case we pass in `uids` instead of `uid`
(defn block-key-down
[e uid state]
(let [{:keys [meta key-code]} (destruct-event e)]
(let [d-event (destruct-event e)
{:keys [meta ctrl key-code]} d-event]
(swap! state assoc :last-keydown d-event)
(cond
(arrow-key-direction e) (handle-arrow-key e uid state)
(pair-char? e) (handle-pair-char e uid state)
(= key-code KeyCodes.TAB) (handle-tab e uid)
(= key-code KeyCodes.ENTER) (handle-enter e uid state)
(= key-code KeyCodes.BACKSPACE) (handle-backspace e uid state)
(= key-code KeyCodes.ESC) (handle-escape e state)
meta (handle-system-shortcuts e uid state)
(or meta ctrl) (handle-shortcuts e uid state))))

;; -- Default: Add new character -----------------------------------------
(is-character-key? e) (write-char e uid state))))
;; -- Default: Add new character -----------------------------------------
;(is-character-key? e) (write-char e uid state))))


;;:else (prn "non-event" key key-code))))
Expand Down
40 changes: 38 additions & 2 deletions src/cljs/athens/listeners.cljs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
(ns athens.listeners
(:require
;;[athens.util :refer [get-day]]
[athens.db :refer [dsdb]]
[athens.keybindings :refer [arrow-key-direction]]
[cljsjs.react]
[cljsjs.react.dom]
[clojure.string :as string]
[datascript.core :as d]
[goog.events :as events]
[re-frame.core :refer [dispatch subscribe]])
(:import
Expand Down Expand Up @@ -138,11 +140,45 @@
(dispatch [:left-sidebar/toggle]))))


;; -- Clipboard ----------------------------------------------------------

;; TODO: once :selected/items is a nested tree instead of flat list, walk tree and add hyphens instead of mapping
(defn to-markdown-list
[blocks]
(->> blocks
(map (fn [x] [:block/uid x]))
(d/pull-many @dsdb '[:block/string])
(map #(str "- " (:block/string %) "\n"))
(string/join "")))


(defn copy
"If blocks are selected, copy blocks as markdown list."
[e]
(let [blocks @(subscribe [:selected/items])]
(when (not-empty blocks)
(.. e preventDefault)
;; Use -event_ because goog events quirk
(.. e -event_ -clipboardData (setData "text/plain" (to-markdown-list blocks))))))


;; do same as copy AND delete selected blocks
(defn cut
[e]
(let [blocks @(subscribe [:selected/items])]
(when (not-empty blocks)
(.. e preventDefault)
(.. e -event_ -clipboardData (setData "text/plain" (to-markdown-list blocks)))
(dispatch [:selected/delete blocks]))))


(defn init
[]
;; (events/listen js/window EventType.MOUSEDOWN edit-block)
(events/listen js/window EventType.MOUSEDOWN unfocus)
(events/listen js/window EventType.MOUSEDOWN mouse-down-outside-athena)
(events/listen js/window EventType.KEYDOWN multi-block-selection)
(events/listen js/window EventType.KEYDOWN key-down))
(events/listen js/window EventType.KEYDOWN key-down)
(events/listen js/window EventType.COPY copy)
(events/listen js/window EventType.CUT cut))

50 changes: 36 additions & 14 deletions src/cljs/athens/views/blocks.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[athens.views.dropdown :refer [menu-style dropdown-style]]
[cljsjs.react]
[cljsjs.react.dom]
[clojure.string :as str]
[garden.selectors :as selectors]
[goog.dom.classlist :refer [contains]]
[goog.events :as events]
Expand Down Expand Up @@ -450,6 +451,24 @@
[:<> [(r/adapt-react-class icon)] [:span text] (when kbd [:kbd kbd])]])]]))


(defn paste
"if user does typical copy and paste, meta+v, and "
[e uid state]
(let [data (.. e -clipboardData (getData "text"))
is-block (re-find #"\r?\n" data)
last-keydown (:last-keydown @state)
{:keys [shift]} last-keydown]
;; if `not shift`, do normal plain-text paste
(when (and is-block (not shift))
(.. e preventDefault)
(dispatch [:paste uid data]))))


(defn block-on-change
[e _uid state]
(swap! state assoc :atom-string (.. e -target -value)))


;; Actual string contents - two elements, one for reading and one for writing
;; seems hacky, but so far no better way to click into the correct position with one conditional element
(defn block-content-el
Expand All @@ -464,26 +483,28 @@
:class [(when is-editing "is-editing") "textarea"]
:auto-focus true
:id (str "editable-uid-" uid)
;; never actually use on-change. rather, use :string-listener to update datascript. necessary to make react happy
:on-change (fn [_])
;; use a combination of on-change and on-key-down. imperfect, but good enough until we rewrite keybindings
:on-change (fn [e] (block-on-change e uid state))
:on-paste (fn [e] (paste e uid state))
:on-key-down (fn [e] (block-key-down e uid state))
:on-mouse-down (fn [e]
;; TODO: allow user to select multiple times while holding shift
(if (.. e -shiftKey)
(let [target (.. e -target)
;; TODO: implement for block-page
node-page (.. target (closest ".node-page"))
source-uid @(subscribe [:editing/uid])
target-block (.. target (closest ".block-container"))
blocks (vec (array-seq (.. node-page (querySelectorAll ".block-container"))))
(let [target (.. e -target)
page (or (.. target (closest ".node-page")) (.. target (closest ".block-page")))
source-uid @(subscribe [:editing/uid])
target-block (.. target (closest ".block-container"))
blocks (vec (array-seq (.. page (querySelectorAll ".block-container"))))
[start end] (-> (keep-indexed (fn [i el]
(when (or (= el target-block)
(= source-uid (.. el -dataset -uid)))
i))
blocks)
sort)
selected-blocks (subvec blocks start (inc end))
selected-uids (mapv #(.. % -dataset -uid) selected-blocks)]
(dispatch [:selected/add-items selected-uids]))
blocks))]
(when (and start end)
(let [selected-blocks (subvec blocks start (inc end))
selected-uids (mapv #(.. % -dataset -uid) selected-blocks)]
(dispatch [:editing/uid nil])
(dispatch [:selected/add-items selected-uids]))))
(do
(events/listen js/window EventType.MOUSEOVER multi-block-select-over)
(events/listen js/window EventType.MOUSEUP multi-block-select-up))))}]
Expand Down Expand Up @@ -527,7 +548,8 @@
:search/index 0
:dragging false
:drag-target nil
:edit/time (:edit/time block)})]
:edit/time (:edit/time block)
:last-keydown nil})]
(add-watch state :string-listener
(fn [_context _atom old new]
(let [{:keys [atom-string]} new]
Expand Down

0 comments on commit 15de32e

Please sign in to comment.