diff --git a/src/cljs/athens/db.cljs b/src/cljs/athens/db.cljs index 1851adc22d..a7f0e07820 100644 --- a/src/cljs/athens/db.cljs +++ b/src/cljs/athens/db.cljs @@ -50,7 +50,7 @@ :right-sidebar/width 32 :mouse-down false :daily-notes/items [] - :selected/items [] + :selected/items #{} :theme/dark false :graph-conf default-graph-conf}) diff --git a/src/cljs/athens/events.cljs b/src/cljs/athens/events.cljs index 770714e10d..303a6f95ea 100644 --- a/src/cljs/athens/events.cljs +++ b/src/cljs/athens/events.cljs @@ -300,26 +300,31 @@ (reg-event-db :selected/add-item (fn [db [_ uid]] - (update db :selected/items conj uid))) + (update db :selected/items (fnil conj #{}) uid))) (reg-event-db :selected/remove-item (fn [db [_ uid]] - (let [items (:selected/items db)] - (assoc db :selected/items (filterv #(not= % uid) items))))) + (update db :selected/items disj uid))) + + +(reg-event-db + :selected/remove-items + (fn [db [_ uids]] + (update db :selected/items #(apply disj %1 %2) uids))) (reg-event-db :selected/add-items (fn [db [_ uids]] - (update db :selected/items concat uids))) + (update db :selected/items #(apply conj %1 %2) uids))) (reg-event-db :selected/clear-items (fn [db _] - (assoc db :selected/items []))) + (assoc db :selected/items #{}))) (defn select-up diff --git a/src/cljs/athens/util.cljs b/src/cljs/athens/util.cljs index 360078b956..a53e971481 100644 --- a/src/cljs/athens/util.cljs +++ b/src/cljs/athens/util.cljs @@ -102,6 +102,18 @@ uid)) +(defn get-dataset-children-uids + [el] + (let [block (when el (.. el (closest ".block-container"))) + children-uids (when block + (let [dom-children-uids ^String (.-childrenuids (.-dataset block))] + (when-not (string/blank? dom-children-uids) + (-> dom-children-uids + (string/split #",") + set))))] + children-uids)) + + (defn get-caret-position [target] (let [selectionEnd (.. target -selectionEnd)] diff --git a/src/cljs/athens/views/blocks/content.cljs b/src/cljs/athens/views/blocks/content.cljs index 4b4fbd584e..8299beaa68 100644 --- a/src/cljs/athens/views/blocks/content.cljs +++ b/src/cljs/athens/views/blocks/content.cljs @@ -1,17 +1,18 @@ (ns athens.views.blocks.content (:require - [athens.db :as db] - [athens.electron :as electron] - [athens.events :as events] - [athens.parse-renderer :refer [parse-and-render]] - [athens.style :as style] - [athens.util :as util] + [athens.config :as config] + [athens.db :as db] + [athens.electron :as electron] + [athens.parse-renderer :refer [parse-and-render]] + [athens.style :as style] + [athens.util :as util] [athens.views.blocks.textarea-keydown :as textarea-keydown] - [garden.selectors :as selectors] - [goog.events :as goog-events] - [komponentit.autosize :as autosize] - [re-frame.core :as rf] - [stylefy.core :as stylefy]) + [clojure.set :as set] + [garden.selectors :as selectors] + [goog.events :as goog-events] + [komponentit.autosize :as autosize] + [re-frame.core :as rf] + [stylefy.core :as stylefy]) (:import (goog.events EventType))) @@ -187,33 +188,54 @@ • 3 Because of this bug, add additional exit cases to prevent stack overflow." [e source-uid target-uid] - (let [target (.. e -target) - page (or (.. target (closest ".node-page")) (.. target (closest ".block-page"))) - blocks (->> (.. page (querySelectorAll ".block-container")) - array-seq - vec) - uids (map util/get-dataset-uid blocks) - start-idx (first (keep-indexed (fn [i uid] (when (= uid source-uid) i)) uids)) - end-idx (first (keep-indexed (fn [i uid] (when (= uid target-uid) i)) uids))] - (when (and start-idx end-idx) - (let [up? (> start-idx end-idx) - delta (js/Math.abs (- start-idx end-idx)) - select-fn (if up? events/select-up events/select-down) - start-uid (nth uids start-idx) - end-uid (nth uids end-idx) - new-items (loop [new-items [source-uid] - prev-items []] - (cond - (= prev-items new-items) new-items - (> (count new-items) delta) new-items - (nil? new-items) [] - (or (and (= (first new-items) start-uid) - (= (last new-items) end-uid)) - (and (= (last new-items) start-uid) - (= (first new-items) end-uid))) new-items - :else (recur (select-fn new-items) - new-items)))] - (rf/dispatch [:selected/add-items new-items]))))) + (let [target (.. e -target) + page (or (.. target (closest ".node-page")) + (.. target (closest ".block-page"))) + blocks (->> (.. page (querySelectorAll ".block-container")) + array-seq + vec) + uids (map util/get-dataset-uid blocks) + uids->children-uids (->> (zipmap uids + (map util/get-dataset-children-uids blocks)) + (remove #(-> % second empty?)) + (into {})) + indexed-uids (map-indexed vector uids) + start-index (->> indexed-uids + (filter (fn [[_idx uid]] + (= source-uid uid))) + ffirst) + end-index (->> indexed-uids + (filter (fn [[_idx uid]] + (= target-uid uid))) + ffirst) + selected-uids @(rf/subscribe [:selected/items]) + candidate-uids (->> indexed-uids + (filter (fn [[idx _uid]] + (<= (min start-index end-index) + idx + (max start-index end-index)))) + (map second) + (into (or selected-uids #{}))) + descendants-uids (loop [descendants #{} + ancestors-uids candidate-uids] + (if (seq ancestors-uids) + (let [ancestors-children (->> ancestors-uids + (mapcat #(get uids->children-uids %)) + (into #{}))] + (recur (apply conj descendants ancestors-children) + ancestors-children)) + descendants)) + to-remove-uids (set/intersection selected-uids descendants-uids) + selection-new-uids (set/difference candidate-uids descendants-uids)] + (when config/debug? + (js/console.debug (str "selection: " (pr-str selected-uids) + ", candidates: " (pr-str candidate-uids) + ", descendants: " (pr-str descendants-uids) + ", rm: " (pr-str to-remove-uids) + ", add: " (pr-str selection-new-uids)))) + (when (and start-index end-index) + (rf/dispatch [:selected/remove-items to-remove-uids]) + (rf/dispatch [:selected/add-items selection-new-uids])))) ;; Event Handlers @@ -279,9 +301,14 @@ "If shift key is held when user clicks across multiple blocks, select the blocks." [e target-uid _state] (let [[target-uid _] (db/uid-and-embed-id target-uid) - source-uid @(rf/subscribe [:editing/uid])] - (when (and source-uid target-uid (not= source-uid target-uid) (.. e -shiftKey)) - (find-selected-items e source-uid target-uid)))) + source-uid @(rf/subscribe [:editing/uid]) + shift? (.-shiftKey e)] + (if (and shift? + source-uid + target-uid + (not= source-uid target-uid)) + (find-selected-items e source-uid target-uid) + (rf/dispatch [:selected/clear-items])))) (defn global-mouseup diff --git a/src/cljs/athens/views/blocks/core.cljs b/src/cljs/athens/views/blocks/core.cljs index 635a166e4a..4dc9223dbd 100644 --- a/src/cljs/athens/views/blocks/core.cljs +++ b/src/cljs/athens/views/blocks/core.cljs @@ -197,7 +197,8 @@ (let [{:keys [linked-ref initial-open linked-ref-uid parent-uids]} linked-ref-data state (r/atom {:string/local nil :string/previous nil - :search/type nil ; one of #{:page :block :slash :hashtag} + ;; one of #{:page :block :slash :hashtag} + :search/type nil :search/results nil :search/query nil :search/index nil @@ -208,19 +209,24 @@ :context-menu/y nil :context-menu/show false :caret-position nil - :show-editable-dom false - :linked-ref/open (or (false? linked-ref) initial-open)})] + :show-editable-dom false + :linked-ref/open (or (false? linked-ref) initial-open)})] (fn [block linked-ref-data opts] - (let [{:block/keys [uid string open children _refs]} block - uid-sanitized-block (s/transform - (specter-recursive-path #(contains? % :block/uid)) - (fn [{:block/keys [original-uid uid] :as block}] - (assoc block :block/uid (or original-uid uid))) - block) - {:search/keys [] :keys [dragging]} @state - is-editing @(rf/subscribe [:editing/is-editing uid]) - is-selected @(rf/subscribe [:selected/is-selected uid])] + (let [{:block/keys [uid + string + open + children + _refs]} block + children-uids (set (map :block/uid children)) + uid-sanitized-block (s/transform + (specter-recursive-path #(contains? % :block/uid)) + (fn [{:block/keys [original-uid uid] :as block}] + (assoc block :block/uid (or original-uid uid))) + block) + {:keys [dragging]} @state + is-editing @(rf/subscribe [:editing/is-editing uid]) + is-selected @(rf/subscribe [:selected/is-selected uid])] ;; (prn uid is-selected) @@ -231,23 +237,25 @@ (swap! state assoc :string/previous string :string/local string)) [:div - {:class ["block-container" - (when (and dragging (not is-selected)) "dragging") - (when is-editing "is-editing") - (when is-selected "is-selected") - (when (and (seq children) open) "show-tree-indicator") - (when (and (false? initial-open) (= uid linked-ref-uid)) "is-linked-ref")] - :data-uid uid + {:class ["block-container" + (when (and dragging (not is-selected)) "dragging") + (when is-editing "is-editing") + (when is-selected "is-selected") + (when (and (seq children) open) "show-tree-indicator") + (when (and (false? initial-open) (= uid linked-ref-uid)) "is-linked-ref")] + :data-uid uid + ;; need to know children for selection resolution + :data-childrenuids children-uids ;; :show-editable-dom allows us to render the editing elements (like the textarea) ;; even when not editing this block. When true, clicking the block content will pass ;; the clicks down to the underlying textarea. The textarea is expensive to render, ;; so we avoid rendering it when it's not needed. - :on-mouse-enter #(swap! state assoc :show-editable-dom true) - :on-mouse-leave #(swap! state assoc :show-editable-dom false) - :on-click (fn [e] (doall (.. e stopPropagation) (rf/dispatch [:editing/uid uid]))) - :on-drag-over (fn [e] (block-drag-over e block state)) - :on-drag-leave (fn [e] (block-drag-leave e block state)) - :on-drop (fn [e] (block-drop e block state))} + :on-mouse-enter #(swap! state assoc :show-editable-dom true) + :on-mouse-leave #(swap! state assoc :show-editable-dom false) + :on-click (fn [e] (doall (.. e stopPropagation) (rf/dispatch [:editing/uid uid]))) + :on-drag-over (fn [e] (block-drag-over e block state)) + :on-drag-leave (fn [e] (block-drag-leave e block state)) + :on-drop (fn [e] (block-drop e block state))} [presence/presence-popover-info uid {:inline? true}] diff --git a/src/cljs/athens/views/devtool.cljs b/src/cljs/athens/views/devtool.cljs index 084d2dffb5..60f278a15e 100644 --- a/src/cljs/athens/views/devtool.cljs +++ b/src/cljs/athens/views/devtool.cljs @@ -395,9 +395,9 @@ (eval-box!))) -; Only run the listener in dev mode, not in prod. The listener slows things -; down a lot. For example it makes the enter key take ~300ms rather than ~100ms, -; according to the Chrome devtools flamegraph. +;; Only run the listener in dev mode, not in prod. The listener slows things +;; down a lot. For example it makes the enter key take ~300ms rather than ~100ms, +;; according to the Chrome devtools flamegraph. (when config/debug? (d/listen! dsdb :devtool/open listener))