diff --git a/web/src/main/frontend/components/hiccup.cljs b/web/src/main/frontend/components/hiccup.cljs index fdecac024..043aa9ba0 100644 --- a/web/src/main/frontend/components/hiccup.cljs +++ b/web/src/main/frontend/components/hiccup.cljs @@ -29,9 +29,6 @@ (defonce *heading-children (atom {})) -(defonce *mouse - (atom {})) - (defonce *dragging? (atom false)) (defonce *dragging-heading @@ -390,16 +387,13 @@ (defonce *control-show? (atom {})) (rum/defcs heading-control < rum/reactive - (rum/local false ::collapsed?) - [state config uuid heading-id level start-level body children heading dummy?] - (let [collapsed-atom? (get state ::collapsed?) - toggle-collapsed? (state/sub [:ui/collapsed-headings heading-id]) - has-child? (or (seq children) - (seq body)) - collapsed? (or (and - toggle-collapsed? - has-child?) - @collapsed-atom?) + [state config heading uuid heading-id level start-level body children dummy? collapsed-atom] + (let [has-child? (and + (not (:pre-heading? heading)) + (or (seq children) + (seq body))) + collapsed? (and has-child? + (rum/react collapsed-atom)) control-show (util/react (rum/cursor *control-show? heading-id)) dark? (= "dark" (state/sub :ui/theme))] [:div.hd-control.mr-2.flex.flex-row.items-center @@ -414,11 +408,10 @@ :class "transition ease-in-out duration-150" :on-click (fn [e] (util/stop e) - (let [id (str "ls-heading-" uuid)] - (if collapsed? - (expand/expand! (:id config) id) - (expand/collapse! (:id config) id)) - (reset! collapsed-atom? (not collapsed?))))} + (if collapsed? + (expand/expand! heading) + (expand/collapse! heading)) + (reset! collapsed-atom (not collapsed?)))} (cond (and control-show collapsed?) (svg/caret-right) @@ -428,59 +421,33 @@ :else [:span ""])] - [:a - (cond-> - {:id (str "dot-" uuid) - :draggable true - :on-drag-start (fn [event] - (handler/highlight-heading! uuid) - (.setData (gobj/get event "dataTransfer") - "heading-uuid" - uuid) - (.setData (gobj/get event "dataTransfer") - "heading-dom-id" - heading-id) - (reset! *dragging? true) - (reset! *dragging-heading heading)) - ;; :on-drag-end (fn [event] - ;; (reset! *dragging? false) - ;; (reset! *mouse {})) - - :style {:width 16 - :height 16} - :headingid (str uuid)} - (not dummy?) - (assoc :href (str "/page/" uuid) - :on-click (fn [e] - (util/stop e) - (when (gobj/get e "shiftKey") - (state/sidebar-add-block! - (state/get-current-repo) - (:db/id heading) - :heading - heading) - (handler/show-right-sidebar))))) - (if collapsed? - [:svg {:height 16 - :width 16 - :fill "currentColor" - :display "inline-block"} - [:circle {:cx 8 - :cy 8 - :r 5 - :stroke (if dark? "#1d6577" "#cbd7de") - :stroke-width 5 - :fill "none"}] - [:circle {:cx 8 - :cy 8 - :r 3}]] - [:svg {:height 16 - :width 16 - :fill "currentColor" - :display "inline-block"} - [:circle {:cx 8 - :cy 8 - :r 3}]])]])) + [:a (if (not dummy?) + {:href (str "/page/" uuid) + :on-click (fn [e] + (util/stop e) + (when (gobj/get e "shiftKey") + (state/sidebar-add-block! + (state/get-current-repo) + (:db/id heading) + :heading + heading) + (handler/show-right-sidebar)))}) + [:span.bullet-container.cursor + {:id (str "dot-" uuid) + :draggable true + :on-drag-start (fn [event] + (handler/highlight-heading! uuid) + (.setData (gobj/get event "dataTransfer") + "heading-uuid" + uuid) + (.setData (gobj/get event "dataTransfer") + "heading-dom-id" + heading-id) + (reset! *dragging? true) + (reset! *dragging-heading heading)) + :headingid (str uuid) + :class (if collapsed? "bullet-closed")} + [:span.bullet]]]])) (defn- build-id [config ref? sidebar?] @@ -507,10 +474,7 @@ :width (- 700 margin-left)} (if top? {:top 0} - {:bottom bottom})) - :on-mouse-move (fn [event] - (let [client-x (gobj/get event "clientX")] - (reset! *mouse {:client-x client-x})))}])) + {:bottom bottom}))}])) (declare heading-container) (rum/defc heading-checkbox @@ -544,24 +508,24 @@ (heading-checkbox t (str "mr-1 cursor"))) marker-cp (when-not pre-heading? (if (contains? #{"DOING" "IN-PROGRESS" "WAIT" "WAITING"} marker) - [:span {:class (str "task-status " (string/lower-case marker)) - :style {:margin-right 3.5}} - (string/upper-case marker)])) + [:span {:class (str "task-status " (string/lower-case marker)) + :style {:margin-right 3.5}} + (string/upper-case marker)])) priority (when-not pre-heading? (if priority - [:span {:class "priority" - :style {:margin-right 3.5}} - (util/format "[#%s]" (str priority))])) + [:span {:class "priority" + :style {:margin-right 3.5}} + (util/format "[#%s]" (str priority))])) tags (when-not pre-heading? (when-not (empty? tags) - (->elem - :span - {:class "heading-tags"} - (mapv (fn [{:keys [db/id tag/name]}] - [:a.tag.mx-1 {:key (str "tag-" id) - :href (str "/tag/" name)} - (str "#" name)]) - tags))))] + (->elem + :span + {:class "heading-tags"} + (mapv (fn [{:keys [db/id tag/name]}] + [:a.tag.mx-1 {:key (str "tag-" id) + :href (str "/tag/" name)} + (str "#" name)]) + tags))))] (when level (let [element (if (<= level 6) (keyword (str "h" level)) @@ -602,7 +566,7 @@ (.getData (gobj/get event "dataTransfer") attr)) (rum/defc heading-content-or-editor < rum/reactive - [config {:heading/keys [uuid title level body meta content dummy? page format repo children pre-heading? idx] :as heading} edit-input-id heading-id slide?] + [config {:heading/keys [uuid title level body meta content dummy? page format repo children pre-heading? collapsed? idx] :as heading} edit-input-id heading-id slide?] (let [edit? (state/sub [:editor/editing? edit-input-id])] (if edit? [:div {:id (str "editor-" edit-input-id)} @@ -666,7 +630,7 @@ (dnd-separator heading 0 -4 false true)) (when (and (not pre-heading?) (seq body)) - [:div.heading-body + [:div.heading-body {:style {:display (if collapsed? "none" "")}} (for [child body] (let [block (block config child)] (rum/with-key (heading-child block) @@ -688,17 +652,20 @@ {:did-update (fn [state] (util/code-highlight!) state)} - - [config {:heading/keys [uuid title level body meta content dummy? page format repo children idx] :as heading}] + [config {:heading/keys [uuid title level body meta content dummy? page format repo children collapsed? pre-heading? idx] :as heading}] (let [ref? (boolean (:ref? config)) sidebar? (boolean (:sidebar? config)) slide? (boolean (:slide? config)) unique-dom-id (build-id config ref? sidebar?) edit-input-id (str "edit-heading-" unique-dom-id uuid) heading-id (str "ls-heading-" unique-dom-id uuid) - has-child? (or (seq children) - (seq body)) + has-child? (boolean + (and + (not pre-heading?) + (or (seq children) + (seq body)))) start-level (or (:start-level config) 1) + collapsed-atom (atom collapsed?) drag-attrs {:on-drag-over (fn [event] (util/stop event) (when-not (dnd-same-heading? uuid) @@ -734,7 +701,11 @@ :on-mouse-over (fn [e] (util/stop e) (when has-child? - (swap! *control-show? assoc heading-id true))) + (swap! *control-show? assoc heading-id true)) + (when-let [parent (gdom/getElement heading-id)] + (let [node (.querySelector parent ".bullet-container") + closed? (d/has-class? node "bullet-closed")] + (reset! collapsed-atom closed?)))) :on-mouse-out (fn [e] (util/stop e) (when has-child? @@ -745,10 +716,13 @@ {:id heading-id :style {:position "relative"} :class (str uuid - (if dummy? " dummy")) + (when dummy? " dummy") + (when (and collapsed? has-child?) " collapsed") + (when pre-heading? " pre-heading")) :headingid (str uuid) :repo repo - :level level} + :level level + :haschild (str has-child?)} (not slide?) (merge drag-attrs)) @@ -756,12 +730,13 @@ [:div.flex-1.flex-row.py-1 (when-not slide? - (heading-control config uuid heading-id level start-level body children heading dummy?)) + (heading-control config heading uuid heading-id level start-level body children dummy? collapsed-atom)) (heading-content-or-editor config heading edit-input-id heading-id slide?)] (when (seq children) - [:div.heading-children {:style {:margin-left 33}} + [:div.heading-children {:style {:margin-left 31 + :display (if collapsed? "none" "")}} (for [child children] (let [child (dissoc child :heading/meta)] (rum/with-key (heading-container config child) diff --git a/web/src/main/frontend/components/page.cljs b/web/src/main/frontend/components/page.cljs index 86d76d3bf..ae0cb4310 100644 --- a/web/src/main/frontend/components/page.cljs +++ b/web/src/main/frontend/components/page.cljs @@ -68,7 +68,8 @@ (util/stop e) (let [encoded-page-name (get-page-name state) id encoded-page-name] - (expand/toggle-all! id))))) + (expand/cycle!) + (handler/re-render-root!))))) ;; (mixins/perf-measure-mixin "Page") [state {:keys [repo] :as option}] (let [repo (or repo (state/get-current-repo)) diff --git a/web/src/main/frontend/db.cljs b/web/src/main/frontend/db.cljs index e1d399b14..10f16c51b 100644 --- a/web/src/main/frontend/db.cljs +++ b/web/src/main/frontend/db.cljs @@ -168,6 +168,7 @@ :heading/last-modified-at {} :heading/body {} :heading/pre-heading? {} + :heading/collapsed? {} ;; :heading/children {:db/valueType :db.type/ref ;; :db/cardinality :db.cardinality/many} @@ -346,7 +347,8 @@ (let [kv? (and (vector? k) (= :kv (first k))) k (vec (cons repo k)) ;; TODO: refactor - use-cache? false] + use-cache? false + ] (when-let [conn (if files-db? (deref (get-files-conn repo)) (get-conn repo))] @@ -505,23 +507,16 @@ (let [new-result (-> (if (keyword? query) (get-key-value repo-url query) - ;; TODO: Datascript query performance - ;; (if files-db? - ;; (apply d/q query (d/db (get-conn)) inputs) - ;; (profile - ;; "Query" - ;; (doall (apply d/q query (d/db (get-conn)) inputs)))) - - (apply d/q query (d/db (get-conn)) inputs) - ) + ;; TODO: Improve Datascript query performance + (if files-db? + (apply d/q query (d/db (get-conn)) inputs) + (profile + "Query" + (doall (apply d/q query (d/db (get-conn)) inputs))))) transform-fn)] - (set-new-result! handler-key new-result) - ;; (if files-db? - ;; (set-new-result! handler-key new-result) - ;; (profile - ;; (str "set new result " handler-key) - ;; (set-new-result! handler-key new-result))) - )))))))))) + (profile + (str "set new result " handler-key) + (set-new-result! handler-key new-result)))))))))))) (defn pull-heading [id] @@ -1452,7 +1447,64 @@ (when-let [heading (entity repo [:heading/uuid (:heading/uuid heading)])] (get-in heading [:heading/meta :end-pos])))))) +(defn get-heading-ids + [heading] + (let [ids (atom []) + _ (walk/prewalk + (fn [form] + (when (map? form) + (when-let [id (:heading/uuid form)] + (swap! ids conj id))) + form) + heading)] + @ids)) + +(defn collapse-heading! + [heading] + (let [repo (:heading/repo heading)] + (transact! repo + [{:heading/uuid (:heading/uuid heading) + :heading/collapsed? true}]))) + +(defn collapse-headings! + [heading-ids] + (let [repo (state/get-current-repo)] + (transact! repo + (map + (fn [id] + {:heading/uuid id + :heading/collapsed? true}) + heading-ids)))) + +(defn expand-heading! + [heading] + (let [repo (:heading/repo heading)] + (transact! repo + [{:heading/uuid (:heading/uuid heading) + :heading/collapsed? false}]))) + +(defn expand-headings! + [heading-ids] + (let [repo (state/get-current-repo)] + (transact! repo + (map + (fn [id] + {:heading/uuid id + :heading/collapsed? false}) + heading-ids)))) + +(defn get-collapsed-headings + [] + (d/q + '[:find ?content + :where + [?h :heading/content ?content] + [?h :heading/collapsed? true]] + (get-conn))) + (comment + + (defn debug! [] (let [repos (->> (get-in @state/state [:me :repos]) diff --git a/web/src/main/frontend/expand.cljs b/web/src/main/frontend/expand.cljs index a4f7dc6ca..0c1491c94 100644 --- a/web/src/main/frontend/expand.cljs +++ b/web/src/main/frontend/expand.cljs @@ -5,103 +5,95 @@ [frontend.util :as util] [clojure.string :as string] [medley.core :as medley] - [frontend.state :as state])) + [frontend.state :as state] + [frontend.db :as db])) -(defn get-headings - [id] - ;; TODO: dommy/by-id will fail if id includes `=` - (when-let [node (gdom/getElement id)] - (some-> (d/sel node [".ls-heading"]) - (array-seq)))) +(defn- hide! + [element] + (d/set-style! element :display "none")) -(defn- get-level - [node] - (-> (d/attr node "level") - (util/parse-int))) - -(defn get-heading-children - [headings heading] - (let [heading-id (gobj/get heading "id") - level (get-level heading) - nodes (->> headings - ;; drop preceding nodes - (drop-while (fn [node] - (not= heading-id (gobj/get node "id")))) - ;; drop self - (next) - ;; take the children - (take-while (fn [node] - (> (get-level node) level))))] - nodes)) - -(defn collapse-non-heading! - [id] - (when-let [node (gdom/getElement id)] - (doseq [node (d/sel node [".heading-body"])] - (d/hide! node)))) - -(defn expand-non-heading! - [id] - (when-let [node (gdom/getElement id)] - (doseq [node (d/sel node [".heading-body"])] - (d/show! node)))) +(defn- show! + [element] + (d/set-style! element :display "")) (defn collapse! - [headings-id heading-id] - (let [all-headings (get-headings headings-id)] - (collapse-non-heading! heading-id) + [heading] + (let [heading-id (str "ls-heading-" (:heading/uuid heading))] (when-let [node (gdom/getElement heading-id)] - (let [root-level (d/attr node "level")] - (let [children (get-heading-children all-headings node)] - (doseq [node children] - (let [child-level (d/attr node "level")] - (when (and root-level - child-level - (= 1 (- (util/parse-int child-level) - (util/parse-int root-level)))) - (d/add-class! node "is-collapsed")) - (d/hide! node)))))))) + (d/add-class! node "collapsed") + (when-let [e (.querySelector node ".heading-body")] + (hide! e)) + (when-let [e (.querySelector node ".heading-children")] + (hide! e)) + (db/collapse-heading! heading)))) (defn expand! - [headings-id heading-id] - (let [all-headings (get-headings headings-id)] - (state/expand-heading! heading-id) - (expand-non-heading! heading-id) + [heading] + (let [heading-id (str "ls-heading-" (:heading/uuid heading))] (when-let [node (gdom/getElement heading-id)] - (d/remove-class! node "is-collapsed") - (let [root-level (d/attr node "level")] - (let [children (get-heading-children all-headings node)] - (doseq [node children] - (let [child-level (d/attr node "level") - collapsed? (d/has-class? node "is-collapsed") - next-child? (and collapsed? - root-level - child-level - (= 1 (- (util/parse-int child-level) - (util/parse-int root-level))))] - (when next-child? - (d/remove-class! node "is-collapsed")) - (when (or (not collapsed?) - next-child?) - (d/show! node))))))))) + ;; (d/remove-class! node "collapsed") + (when-let [e (.querySelector node ".heading-body")] + (show! e)) + (when-let [e (.querySelector node ".heading-children")] + (show! e)) + (db/expand-heading! heading) + ;; (doseq [element (d/by-class node "cycle-collapsed")] + ;; (d/remove-class! element "cycle-collapsed") + ;; (show! element)) + ))) +(defn set-bullet-closed! + [element] + (when element + (when-let [node (.querySelector element ".bullet-container")] + (d/add-class! node "bullet-closed")))) -;; ;; Collapse acts like TOC -(defn toggle-all! - [id] - ;; default to level 2 - (let [all-headings (get-headings id) - headings all-headings] - (when (seq headings) - (let [toggle-state (:ui/toggle-state @state/state)] - (doseq [heading headings] - (let [heading-id (gobj/get heading "id") - level (util/parse-int (d/attr heading "level"))] - (if toggle-state - (expand! id heading-id) - (when (= level 2) - (collapse! id heading-id) - (state/collapse-heading! heading-id))))) - (when toggle-state - (state/clear-collapsed-headings!)) - (state/ui-toggle-state!))))) +;; Collapse acts like TOC +;; There are three modes to cycle: +;; 1. Collapse all headings which levels are greater than 2 +;; 2. Hide all heading's body (user can still see the heading title) +;; 3. Show everything +(defn cycle! + [] + (let [mode (state/next-collapse-mode) + get-headings (fn [] + (let [elements (d/by-class "ls-heading") + result (group-by (fn [e] + (let [level (d/attr e "level")] + (and level + (> (util/parse-int level) 2)))) elements)] + [(get result true) (get result false)]))] + (case mode + :show-all + (do + (doseq [element (d/by-class "ls-heading")] + (show! element)) + (let [elements (d/by-class "heading-body")] + (doseq [element elements] + (show! element))) + (doseq [element (d/by-class "bullet-closed")] + (d/remove-class! element "bullet-closed")) + (doseq [element (d/by-class "heading-children")] + (show! element))) + + :hide-heading-body + (let [elements (d/by-class "heading-body")] + (doseq [element elements] + (d/set-style! element :display "none") + (when-let [parent (util/rec-get-heading-node element)] + (set-bullet-closed! parent)))) + + :hide-heading-children + (let [[elements top-level-elements] (get-headings) + level-2-elements (filter (fn [e] + (let [level (d/attr e "level")] + (and level + (= (util/parse-int level) 2) + (not (d/has-class? e "pre-heading"))))) + top-level-elements)] + (doseq [element elements] + (hide! element)) + (doseq [element level-2-elements] + (when (= "true" (d/attr element "haschild")) + (set-bullet-closed! element))))) + (state/cycle-collapse!))) diff --git a/web/src/main/frontend/handler/dnd.cljs b/web/src/main/frontend/handler/dnd.cljs index bb57e3909..0cff04b16 100644 --- a/web/src/main/frontend/handler/dnd.cljs +++ b/web/src/main/frontend/handler/dnd.cljs @@ -9,21 +9,9 @@ [cljs-time.coerce :as tc] [cljs-time.core :as t])) -(defn- get-heading-ids - [heading] - (let [ids (atom []) - _ (walk/prewalk - (fn [form] - (when (map? form) - (when-let [id (:heading/uuid form)] - (swap! ids conj id))) - form) - heading)] - @ids)) - (defn- remove-heading-child! [target-heading parent-heading] - (let [child-ids (set (get-heading-ids target-heading))] + (let [child-ids (set (db/get-heading-ids target-heading))] (db/get-heading-content-rec parent-heading (fn [{:heading/keys [uuid level content]}] @@ -195,7 +183,7 @@ (= direction :up) (let [offset-heading-id (if nested? (:heading/uuid to-heading) - (last (get-heading-ids to-heading))) + (last (db/get-heading-ids to-heading))) offset-end-pos (get-end-pos (db/entity repo [:heading/uuid offset-heading-id]))] (rebuild-dnd-headings repo target-file target-child? @@ -207,7 +195,7 @@ (= direction :down) (let [offset-heading-id (if nested? (:heading/uuid to-heading) - (last (get-heading-ids to-heading))) + (last (db/get-heading-ids to-heading))) target-start-pos (get-start-pos target-heading)] (rebuild-dnd-headings repo target-file target-child? target-start-pos @@ -358,7 +346,7 @@ :else (let [offset-heading-id (if nested? (:heading/uuid to-heading) - (last (get-heading-ids to-heading))) + (last (db/get-heading-ids to-heading))) offset-end-pos (get-end-pos (db/entity repo [:heading/uuid offset-heading-id]))] (rebuild-dnd-headings repo to-file target-child? @@ -406,7 +394,7 @@ (utf8/substring to-file-content separate-pos)))) target-delete-tx (map (fn [id] [:db.fn/retractEntity [:heading/uuid id]]) - (get-heading-ids target-heading)) + (db/get-heading-ids target-heading)) [target-modified-time to-modified-time] (let [modified-at (tc/to-long (t/now))] [[[:db/add (:db/id (:heading/page target-heading)) :page/last-modified-at modified-at] @@ -427,7 +415,7 @@ :else (let [offset-heading-id (if nested? (:heading/uuid to-heading) - (last (get-heading-ids to-heading))) + (last (db/get-heading-ids to-heading))) offset-end-pos (get-end-pos (db/entity to-heading-repo [:heading/uuid offset-heading-id]))] (rebuild-dnd-headings to-heading-repo to-file target-child? diff --git a/web/src/main/frontend/state.cljs b/web/src/main/frontend/state.cljs index d5441bf6f..e650d1ac8 100644 --- a/web/src/main/frontend/state.cljs +++ b/web/src/main/frontend/state.cljs @@ -29,7 +29,8 @@ :search/result nil :ui/theme (or (storage/get :ui/theme) "black") - :ui/toggle-state false + ;; :show-all, :hide-heading-body, :hide-heading-children + :ui/cycle-collapse :show-all :ui/collapsed-headings {} :ui/sidebar-collapsed-blocks {} :ui/root-component nil @@ -110,9 +111,21 @@ (when (= (get-current-repo) (:url repo)) (set-current-repo! (:url (first (get-repos)))))) -(defn ui-toggle-state! +(defn next-collapse-mode [] - (update-state! :ui/toggle-state not)) + (case (:ui/cycle-collapse @state) + :show-all + :hide-heading-body + + :hide-heading-body + :hide-heading-children + + :hide-heading-children + :show-all)) + +(defn cycle-collapse! + [] + (set-state! :ui/cycle-collapse (next-collapse-mode))) (defn get-edit-heading [] @@ -149,11 +162,15 @@ (defn collapse-heading! [heading-id] - (set-state! [:ui/collapsed-headings heading-id] true)) + (set-state! [:ui/collapsed-headings heading-id] (atom true))) + +(defn get-heading-collapsed-state + [heading-id] + (get-in @state [:ui/collapsed-headings heading-id])) (defn expand-heading! [heading-id] - (set-state! [:ui/collapsed-headings heading-id] false)) + (set-state! [:ui/collapsed-headings heading-id] (atom false))) (defn clear-collapsed-headings! [] diff --git a/web/src/main/frontend/util.cljs b/web/src/main/frontend/util.cljs index 281dc50ff..d45cfb8ba 100644 --- a/web/src/main/frontend/util.cljs +++ b/web/src/main/frontend/util.cljs @@ -514,7 +514,7 @@ (.getDate local-date-time) 0 0 0 0))) -(defn- rec-get-heading-node +(defn rec-get-heading-node [node] (if (and node (d/has-class? node "ls-heading")) node