mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 04:34:14 +00:00
518 lines
25 KiB
Clojure
518 lines
25 KiB
Clojure
(ns frontend.handler.dnd
|
|
(:require [frontend.handler.notification :as notification]
|
|
[frontend.handler.repo :as repo-handler]
|
|
[frontend.config :as config]
|
|
[frontend.state :as state]
|
|
[frontend.util :as util :refer-macros [profile]]
|
|
[frontend.db :as db]
|
|
[clojure.walk :as walk]
|
|
[clojure.string :as string]
|
|
[frontend.utf8 :as utf8]
|
|
[cljs-time.coerce :as tc]
|
|
[cljs-time.core :as t]))
|
|
|
|
(defn- remove-block-child!
|
|
[target-block parent-block]
|
|
(let [child-ids (set (db/get-block-ids target-block))]
|
|
(db/get-block-content-rec
|
|
parent-block
|
|
(fn [{:block/keys [uuid level content]}]
|
|
(if (contains? child-ids uuid)
|
|
""
|
|
content)))))
|
|
|
|
(defn- recompute-block-level
|
|
[to-block nested?]
|
|
(+ (:block/level to-block)
|
|
(if nested? 1 0)))
|
|
|
|
(defn- recompute-block-content-and-changes
|
|
[target-block to-block nested? same-repo? same-file?]
|
|
(let [new-level (recompute-block-level to-block nested?)
|
|
target-level (:block/level target-block)
|
|
format (:block/format target-block)
|
|
pattern (config/get-block-pattern format)
|
|
block-changes (atom [])
|
|
all-content (db/get-block-content-rec
|
|
target-block
|
|
(fn [{:block/keys [uuid level content]
|
|
:as block}]
|
|
(let [new-level (+ new-level (- level target-level))
|
|
new-content (string/replace-first content
|
|
(apply str (repeat level pattern))
|
|
(apply str (repeat new-level pattern)))
|
|
block (cond->
|
|
{:block/uuid uuid
|
|
:block/level new-level
|
|
:block/content new-content
|
|
:block/page (:block/page to-block)}
|
|
|
|
(not same-repo?)
|
|
(merge (dissoc block [:block/level :block/content]))
|
|
|
|
(not same-file?)
|
|
(merge {:block/page (:block/page to-block)
|
|
:block/file (:block/file to-block)}))]
|
|
(swap! block-changes conj block)
|
|
new-content)))]
|
|
[all-content @block-changes]))
|
|
|
|
(defn- move-parent-to-child?
|
|
[target-block to-block]
|
|
(let [to-block-id (:block/uuid to-block)
|
|
result (atom false)
|
|
_ (walk/postwalk
|
|
(fn [form]
|
|
(when (map? form)
|
|
(when-let [id (:block/uuid form)]
|
|
(when (= id to-block-id)
|
|
(reset! result true))))
|
|
form)
|
|
target-block)]
|
|
@result))
|
|
|
|
(defn- compute-target-child?
|
|
[target-block to-block]
|
|
(let [target-block-id (:block/uuid target-block)
|
|
result (atom false)
|
|
_ (walk/postwalk
|
|
(fn [form]
|
|
(when (map? form)
|
|
(when-let [id (:block/uuid form)]
|
|
(when (= id target-block-id)
|
|
(reset! result true))))
|
|
form)
|
|
to-block)]
|
|
@result))
|
|
|
|
(defn rebuild-dnd-blocks
|
|
[repo file target-child? start-pos target-blocks offset-block-uuid {:keys [delete? same-file?]
|
|
:or {delete? false
|
|
same-file? true}}]
|
|
(when (seq target-blocks)
|
|
(let [file-id (:db/id file)
|
|
target-block-ids (set (map :block/uuid target-blocks))
|
|
after-blocks (->> (db/get-file-after-blocks repo file-id start-pos)
|
|
(remove (fn [h] (contains? target-block-ids (:block/uuid h)))))
|
|
|
|
after-blocks (cond
|
|
delete?
|
|
after-blocks
|
|
|
|
(and offset-block-uuid
|
|
(not (contains? (set (map :block/uuid after-blocks)) offset-block-uuid)))
|
|
(concat target-blocks after-blocks)
|
|
|
|
offset-block-uuid
|
|
(let [[before after] (split-with (fn [h] (not= (:block/uuid h)
|
|
offset-block-uuid)) after-blocks)]
|
|
(concat (conj (vec before) (first after))
|
|
target-blocks
|
|
(rest after)))
|
|
:else
|
|
(concat target-blocks after-blocks))
|
|
after-blocks (remove nil? after-blocks)
|
|
;; _ (prn {:start-pos start-pos
|
|
;; :target-blocks target-blocks
|
|
;; :after-blocks (map (fn [block]
|
|
;; (:block/content block))
|
|
;; after-blocks)})
|
|
last-start-pos (atom start-pos)
|
|
result (mapv
|
|
(fn [{:block/keys [uuid meta content level page] :as block}]
|
|
(let [content (str (util/trim-safe content) "\n")
|
|
target-block? (contains? target-block-ids uuid)
|
|
content-length (if target-block?
|
|
(utf8/length (utf8/encode content))
|
|
(- (:end-pos meta) (:start-pos meta)))
|
|
new-end-pos (+ @last-start-pos content-length)
|
|
new-meta {:start-pos @last-start-pos
|
|
:end-pos new-end-pos}]
|
|
(reset! last-start-pos new-end-pos)
|
|
(let [data {:block/uuid uuid
|
|
:block/meta new-meta}]
|
|
(cond
|
|
(and target-block? (not same-file?))
|
|
(merge
|
|
(dissoc block :block/idx :block/dummy?)
|
|
data)
|
|
|
|
target-block?
|
|
(merge
|
|
data
|
|
{:block/level level
|
|
:block/content content
|
|
:block/page page})
|
|
|
|
:else
|
|
data))))
|
|
after-blocks)]
|
|
result)))
|
|
|
|
(defn- get-start-pos
|
|
[block]
|
|
(get-in block [:block/meta :start-pos]))
|
|
|
|
(defn- get-end-pos
|
|
[block]
|
|
(get-in block [:block/meta :end-pos]))
|
|
|
|
(defn- compute-direction
|
|
[target-block top-block nested? top? target-child?]
|
|
(cond
|
|
(= top-block target-block)
|
|
:down
|
|
|
|
(and target-child? nested?)
|
|
:up
|
|
|
|
(and target-child? (not top?))
|
|
:down
|
|
|
|
:else
|
|
:up))
|
|
|
|
(defn- compute-after-blocks-in-same-file
|
|
[repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes]
|
|
(cond
|
|
top?
|
|
(rebuild-dnd-blocks repo target-file target-child?
|
|
original-top-block-start-pos
|
|
block-changes
|
|
nil
|
|
{})
|
|
|
|
(= direction :up)
|
|
(let [offset-block-id (if nested?
|
|
(:block/uuid to-block)
|
|
(last (db/get-block-ids to-block)))
|
|
offset-end-pos (get-end-pos
|
|
(db/entity repo [:block/uuid offset-block-id]))]
|
|
(rebuild-dnd-blocks repo target-file target-child?
|
|
offset-end-pos
|
|
block-changes
|
|
nil
|
|
{}))
|
|
|
|
(= direction :down)
|
|
(let [offset-block-id (if nested?
|
|
(:block/uuid to-block)
|
|
(last (db/get-block-ids to-block)))
|
|
target-start-pos (get-start-pos target-block)]
|
|
(rebuild-dnd-blocks repo target-file target-child?
|
|
target-start-pos
|
|
block-changes
|
|
offset-block-id
|
|
{}))))
|
|
|
|
;; TODO: still could be different pages, e.g. move a block from one journal to another journal
|
|
(defn- move-block-in-same-file
|
|
[repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
|
|
(if (move-parent-to-child? target-block to-block)
|
|
nil
|
|
(let [old-file-content (db/get-file (:file/path (db/entity (:db/id (:block/file target-block)))))
|
|
old-file-content (utf8/encode old-file-content)
|
|
subs (fn [start-pos end-pos] (utf8/substring old-file-content start-pos end-pos))
|
|
bottom-content (db/get-block-content-rec bottom-block)
|
|
top-content (remove-block-child! bottom-block top-block)
|
|
top-area (subs 0 (get-start-pos top-block))
|
|
bottom-area (subs
|
|
(cond
|
|
(and nested? (= direction :down))
|
|
(get-end-pos bottom-block)
|
|
target-child?
|
|
(db/get-block-end-pos-rec repo top-block)
|
|
:else
|
|
(db/get-block-end-pos-rec repo bottom-block))
|
|
nil)
|
|
between-area (if (= direction :down)
|
|
(subs (db/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
|
|
(subs (db/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
|
|
up-content (when (= direction :up)
|
|
(cond
|
|
nested?
|
|
(util/join-newline (:block/content top-block)
|
|
target-content
|
|
(if target-child?
|
|
(remove-block-child! target-block (:block/children to-block))
|
|
(db/get-block-content-rec (:block/children top-block))))
|
|
(and top? target-child?)
|
|
(util/join-newline target-content (remove-block-child! target-block to-block))
|
|
|
|
top?
|
|
(util/join-newline target-content top-content)
|
|
|
|
:else
|
|
(let [top-content (if target-child?
|
|
(remove-block-child! target-block to-block)
|
|
top-content)]
|
|
(util/join-newline top-content target-content))))
|
|
down-content (when (= direction :down)
|
|
(cond
|
|
nested?
|
|
(util/join-newline (:block/content bottom-block)
|
|
target-content)
|
|
target-child?
|
|
(util/join-newline top-content target-content)
|
|
|
|
:else
|
|
(util/join-newline bottom-content target-content)))
|
|
;; _ (prn {:direction direction
|
|
;; :nested? nested?
|
|
;; :top? top?
|
|
;; :target-child? target-child?
|
|
;; :top-area top-area
|
|
;; :up-content up-content
|
|
;; :between-area between-area
|
|
;; :down-content down-content
|
|
;; :bottom-area bottom-area
|
|
;; })
|
|
new-file-content (string/trim
|
|
(util/join-newline
|
|
top-area
|
|
up-content
|
|
between-area
|
|
down-content
|
|
bottom-area))
|
|
after-blocks (->> (compute-after-blocks-in-same-file repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes)
|
|
(remove nil?))
|
|
path (:file/path (db/entity repo (:db/id (:block/file to-block))))
|
|
modified-time (let [modified-at (tc/to-long (t/now))]
|
|
(->
|
|
[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
|
|
distinct
|
|
vec))]
|
|
(profile
|
|
"Move block in the same file: "
|
|
(repo-handler/transact-react-and-alter-file!
|
|
repo
|
|
(concat
|
|
after-blocks
|
|
modified-time)
|
|
{:key :block/change
|
|
:data block-changes}
|
|
[[path new-file-content]]))
|
|
;; (alter-file repo
|
|
;; path
|
|
;; new-file-content
|
|
;; {:re-render-root? true})
|
|
)))
|
|
|
|
(defn- move-block-in-different-files
|
|
[repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
|
|
(let [target-file (db/entity repo (:db/id (:block/file target-block)))
|
|
target-file-path (:file/path target-file)
|
|
target-file-content (db/get-file repo target-file-path)
|
|
to-file (db/entity repo (:db/id (:block/file to-block)))
|
|
to-file-path (:file/path to-file)
|
|
target-block-end-pos (db/get-block-end-pos-rec repo target-block)
|
|
to-block-start-pos (get-start-pos to-block)
|
|
to-block-end-pos (db/get-block-end-pos-rec repo to-block)
|
|
new-target-file-content (utf8/delete! target-file-content
|
|
(get-start-pos target-block)
|
|
target-block-end-pos)
|
|
to-file-content (utf8/encode (db/get-file repo to-file-path))
|
|
new-to-file-content (let [separate-pos (cond nested?
|
|
(get-end-pos to-block)
|
|
top?
|
|
to-block-start-pos
|
|
:else
|
|
to-block-end-pos)]
|
|
(string/trim
|
|
(util/join-newline
|
|
(utf8/substring to-file-content 0 separate-pos)
|
|
target-content
|
|
(utf8/substring to-file-content separate-pos))))
|
|
modified-time (let [modified-at (tc/to-long (t/now))]
|
|
(->
|
|
[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
|
|
distinct
|
|
vec))
|
|
target-after-blocks (rebuild-dnd-blocks repo target-file target-child?
|
|
(get-start-pos target-block)
|
|
block-changes nil {:delete? true})
|
|
to-after-blocks (cond
|
|
top?
|
|
(rebuild-dnd-blocks repo to-file target-child?
|
|
(get-start-pos to-block)
|
|
block-changes
|
|
nil
|
|
{:same-file? false})
|
|
|
|
:else
|
|
(let [offset-block-id (if nested?
|
|
(:block/uuid to-block)
|
|
(last (db/get-block-ids to-block)))
|
|
offset-end-pos (get-end-pos
|
|
(db/entity repo [:block/uuid offset-block-id]))]
|
|
(rebuild-dnd-blocks repo to-file target-child?
|
|
offset-end-pos
|
|
block-changes
|
|
nil
|
|
{:same-file? false})))]
|
|
(profile
|
|
"Move block between different files: "
|
|
(repo-handler/transact-react-and-alter-file!
|
|
repo
|
|
(concat
|
|
target-after-blocks
|
|
to-after-blocks
|
|
modified-time)
|
|
{:key :block/change
|
|
:data (conj block-changes target-block)}
|
|
[[target-file-path new-target-file-content]
|
|
[to-file-path new-to-file-content]]))))
|
|
|
|
(defn- move-block-in-different-repos
|
|
[target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
|
|
(let [target-file (db/entity target-block-repo (:db/id (:block/file target-block)))
|
|
target-file-path (:file/path target-file)
|
|
target-file-content (db/get-file target-block-repo target-file-path)
|
|
to-file (db/entity to-block-repo (:db/id (:block/file to-block)))
|
|
to-file-path (:file/path to-file)
|
|
target-block-end-pos (db/get-block-end-pos-rec target-block-repo target-block)
|
|
to-block-start-pos (get-start-pos to-block)
|
|
to-block-end-pos (db/get-block-end-pos-rec to-block-repo to-block)
|
|
new-target-file-content (utf8/delete! target-file-content
|
|
(get-start-pos target-block)
|
|
target-block-end-pos)
|
|
to-file-content (utf8/encode (db/get-file to-block-repo to-file-path))
|
|
new-to-file-content (let [separate-pos (cond nested?
|
|
(get-end-pos to-block)
|
|
top?
|
|
to-block-start-pos
|
|
:else
|
|
to-block-end-pos)]
|
|
(string/trim
|
|
(util/join-newline
|
|
(utf8/substring to-file-content 0 separate-pos)
|
|
target-content
|
|
(utf8/substring to-file-content separate-pos))))
|
|
target-delete-tx (map (fn [id]
|
|
[:db.fn/retractEntity [:block/uuid id]])
|
|
(db/get-block-ids target-block))
|
|
[target-modified-time to-modified-time]
|
|
(let [modified-at (tc/to-long (t/now))]
|
|
[[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]]
|
|
[[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
|
|
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]])
|
|
target-after-blocks (rebuild-dnd-blocks target-block-repo target-file target-child?
|
|
(get-start-pos target-block)
|
|
block-changes nil {:delete? true})
|
|
to-after-blocks (cond
|
|
top?
|
|
(rebuild-dnd-blocks to-block-repo to-file target-child?
|
|
(get-start-pos to-block)
|
|
block-changes
|
|
nil
|
|
{:same-file? false})
|
|
|
|
:else
|
|
(let [offset-block-id (if nested?
|
|
(:block/uuid to-block)
|
|
(last (db/get-block-ids to-block)))
|
|
offset-end-pos (get-end-pos
|
|
(db/entity to-block-repo [:block/uuid offset-block-id]))]
|
|
(rebuild-dnd-blocks to-block-repo to-file target-child?
|
|
offset-end-pos
|
|
block-changes
|
|
nil
|
|
{:same-file? false})))]
|
|
(profile
|
|
"[Target file] Move block between different files: "
|
|
(repo-handler/transact-react-and-alter-file!
|
|
target-block-repo
|
|
(concat
|
|
target-delete-tx
|
|
target-after-blocks
|
|
target-modified-time)
|
|
{:key :block/change
|
|
:data [(dissoc target-block :block/children)]}
|
|
[[target-file-path new-target-file-content]]))
|
|
|
|
(profile
|
|
"[Destination file] Move block between different files: "
|
|
(repo-handler/transact-react-and-alter-file!
|
|
to-block-repo
|
|
(concat
|
|
to-after-blocks
|
|
to-modified-time)
|
|
{:key :block/change
|
|
:data [block-changes]}
|
|
[[to-file-path new-to-file-content]]))))
|
|
|
|
(defn move-block
|
|
"There can be at least 3 possible situations:
|
|
1. Move a block in the same file (either top-to-bottom or bottom-to-top).
|
|
2. Move a block between two different files.
|
|
3. Move a block between two files in different repos.
|
|
|
|
Notes:
|
|
1. Those two blocks might have different formats, e.g. one is `org` and another is `markdown`,
|
|
we don't handle this now. TODO: transform between different formats in mldoc.
|
|
2. Sometimes we might need to move a parent block to it's own child.
|
|
"
|
|
[target-block to-block target-dom-id top? nested?]
|
|
(when (and target-block to-block (:block/format target-block) (:block/format to-block))
|
|
(cond
|
|
(not= (:block/format target-block)
|
|
(:block/format to-block))
|
|
(notification/show!
|
|
(util/format "Sorry, you can't move a block of format %s to another file of format %s."
|
|
(:block/format target-block)
|
|
(:block/format to-block))
|
|
:error)
|
|
|
|
(= (:block/uuid target-block) (:block/uuid to-block))
|
|
nil
|
|
|
|
:else
|
|
(let [pattern (config/get-block-pattern (:block/format to-block))
|
|
target-block-repo (:block/repo target-block)
|
|
to-block-repo (:block/repo to-block)
|
|
target-block (assoc target-block
|
|
:block/meta
|
|
(:block/meta (db/entity target-block-repo [:block/uuid (:block/uuid target-block)])))
|
|
to-block (assoc to-block
|
|
:block/meta
|
|
(:block/meta (db/entity [:block/uuid (:block/uuid to-block)])))
|
|
same-repo? (= target-block-repo to-block-repo)
|
|
target-file (:block/file target-block)
|
|
same-file? (and
|
|
same-repo?
|
|
(= (:db/id target-file)
|
|
(:db/id (:block/file to-block))))
|
|
[top-block bottom-block] (if same-file?
|
|
(if (< (get-start-pos target-block)
|
|
(get-start-pos to-block))
|
|
[target-block to-block]
|
|
[to-block target-block])
|
|
[nil nil])
|
|
target-child? (compute-target-child? target-block to-block)
|
|
direction (compute-direction target-block top-block nested? top? target-child?)
|
|
original-top-block-start-pos (get-start-pos top-block)
|
|
[target-content block-changes] (recompute-block-content-and-changes target-block to-block nested? same-repo? same-file?)]
|
|
(cond
|
|
same-file?
|
|
(move-block-in-same-file target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
|
|
|
|
;; same repo but different files
|
|
same-repo?
|
|
(move-block-in-different-files target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
|
|
|
|
;; different repos
|
|
:else
|
|
(move-block-in-different-repos target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes))
|
|
|
|
(when (state/git-auto-push?)
|
|
(doseq [repo (->> #{target-block-repo to-block-repo}
|
|
(remove nil?))]
|
|
(repo-handler/push repo nil)))))))
|