dnd.cljs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. (ns frontend.handler.dnd
  2. (:require [frontend.handler.notification :as notification]
  3. [frontend.handler.repo :as repo-handler]
  4. [frontend.config :as config]
  5. [frontend.util :as util :refer-macros [profile]]
  6. [frontend.db :as db]
  7. [clojure.walk :as walk]
  8. [clojure.string :as string]
  9. [frontend.utf8 :as utf8]
  10. [cljs-time.coerce :as tc]
  11. [cljs-time.core :as t]))
  12. (defn- remove-block-child!
  13. [target-block parent-block]
  14. (let [child-ids (set (db/get-block-ids target-block))]
  15. (db/get-block-content-rec
  16. parent-block
  17. (fn [{:block/keys [uuid level content]}]
  18. (if (contains? child-ids uuid)
  19. ""
  20. content)))))
  21. (defn- recompute-block-level
  22. [to-block nested?]
  23. (+ (:block/level to-block)
  24. (if nested? 1 0)))
  25. (defn- recompute-block-content-and-changes
  26. [target-block to-block nested? same-repo? same-file?]
  27. (let [new-level (recompute-block-level to-block nested?)
  28. target-level (:block/level target-block)
  29. format (:block/format target-block)
  30. pattern (config/get-block-pattern format)
  31. block-changes (atom [])
  32. all-content (db/get-block-content-rec
  33. target-block
  34. (fn [{:block/keys [uuid level content]
  35. :as block}]
  36. (let [new-level (+ new-level (- level target-level))
  37. new-content (string/replace-first content
  38. (apply str (repeat level pattern))
  39. (apply str (repeat new-level pattern)))
  40. block (cond->
  41. {:block/uuid uuid
  42. :block/level new-level
  43. :block/content new-content
  44. :block/page (:block/page to-block)}
  45. (not same-repo?)
  46. (merge (dissoc block [:block/level :block/content]))
  47. (not same-file?)
  48. (merge {:block/page (:block/page to-block)
  49. :block/file (:block/file to-block)}))]
  50. (swap! block-changes conj block)
  51. new-content)))]
  52. [all-content @block-changes]))
  53. (defn- move-parent-to-child?
  54. [target-block to-block]
  55. (let [to-block-id (:block/uuid to-block)
  56. result (atom false)
  57. _ (walk/postwalk
  58. (fn [form]
  59. (when (map? form)
  60. (when-let [id (:block/uuid form)]
  61. (when (= id to-block-id)
  62. (reset! result true))))
  63. form)
  64. target-block)]
  65. @result))
  66. (defn- compute-target-child?
  67. [target-block to-block]
  68. (let [target-block-id (:block/uuid target-block)
  69. result (atom false)
  70. _ (walk/postwalk
  71. (fn [form]
  72. (when (map? form)
  73. (when-let [id (:block/uuid form)]
  74. (when (= id target-block-id)
  75. (reset! result true))))
  76. form)
  77. to-block)]
  78. @result))
  79. (defn rebuild-dnd-blocks
  80. [repo file target-child? start-pos target-blocks offset-block-uuid {:keys [delete? same-file?]
  81. :or {delete? false
  82. same-file? true}}]
  83. (when (seq target-blocks)
  84. (let [file-id (:db/id file)
  85. target-block-ids (set (map :block/uuid target-blocks))
  86. after-blocks (->> (db/get-file-after-blocks repo file-id start-pos)
  87. (remove (fn [h] (contains? target-block-ids (:block/uuid h)))))
  88. after-blocks (cond
  89. delete?
  90. after-blocks
  91. (and offset-block-uuid
  92. (not (contains? (set (map :block/uuid after-blocks)) offset-block-uuid)))
  93. (concat target-blocks after-blocks)
  94. offset-block-uuid
  95. (let [[before after] (split-with (fn [h] (not= (:block/uuid h)
  96. offset-block-uuid)) after-blocks)]
  97. (concat (conj (vec before) (first after))
  98. target-blocks
  99. (rest after)))
  100. :else
  101. (concat target-blocks after-blocks))
  102. after-blocks (remove nil? after-blocks)
  103. ;; _ (prn {:start-pos start-pos
  104. ;; :target-blocks target-blocks
  105. ;; :after-blocks (map (fn [block]
  106. ;; (:block/content block))
  107. ;; after-blocks)})
  108. last-start-pos (atom start-pos)
  109. result (mapv
  110. (fn [{:block/keys [uuid meta content level page] :as block}]
  111. (let [content (str (util/trim-safe content) "\n")
  112. target-block? (contains? target-block-ids uuid)
  113. content-length (if target-block?
  114. (utf8/length (utf8/encode content))
  115. (- (:end-pos meta) (:start-pos meta)))
  116. new-end-pos (+ @last-start-pos content-length)
  117. new-meta {:start-pos @last-start-pos
  118. :end-pos new-end-pos}]
  119. (reset! last-start-pos new-end-pos)
  120. (let [data {:block/uuid uuid
  121. :block/meta new-meta}]
  122. (cond
  123. (and target-block? (not same-file?))
  124. (merge
  125. (dissoc block :block/idx :block/dummy?)
  126. data)
  127. target-block?
  128. (merge
  129. data
  130. {:block/level level
  131. :block/content content
  132. :block/page page})
  133. :else
  134. data))))
  135. after-blocks)]
  136. result)))
  137. (defn- get-start-pos
  138. [block]
  139. (get-in block [:block/meta :start-pos]))
  140. (defn- get-end-pos
  141. [block]
  142. (get-in block [:block/meta :end-pos]))
  143. (defn- compute-direction
  144. [target-block top-block nested? top? target-child?]
  145. (cond
  146. (= top-block target-block)
  147. :down
  148. (and target-child? nested?)
  149. :up
  150. (and target-child? (not top?))
  151. :down
  152. :else
  153. :up))
  154. (defn- compute-after-blocks-in-same-file
  155. [repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes]
  156. (cond
  157. top?
  158. (rebuild-dnd-blocks repo target-file target-child?
  159. original-top-block-start-pos
  160. block-changes
  161. nil
  162. {})
  163. (= direction :up)
  164. (let [offset-block-id (if nested?
  165. (:block/uuid to-block)
  166. (last (db/get-block-ids to-block)))
  167. offset-end-pos (get-end-pos
  168. (db/entity repo [:block/uuid offset-block-id]))]
  169. (rebuild-dnd-blocks repo target-file target-child?
  170. offset-end-pos
  171. block-changes
  172. nil
  173. {}))
  174. (= direction :down)
  175. (let [offset-block-id (if nested?
  176. (:block/uuid to-block)
  177. (last (db/get-block-ids to-block)))
  178. target-start-pos (get-start-pos target-block)]
  179. (rebuild-dnd-blocks repo target-file target-child?
  180. target-start-pos
  181. block-changes
  182. offset-block-id
  183. {}))))
  184. ;; TODO: still could be different pages, e.g. move a block from one journal to another journal
  185. (defn- move-block-in-same-file
  186. [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]
  187. (if (move-parent-to-child? target-block to-block)
  188. nil
  189. (let [old-file-content (db/get-file (:file/path (db/entity (:db/id (:block/file target-block)))))
  190. old-file-content (utf8/encode old-file-content)
  191. subs (fn [start-pos end-pos] (utf8/substring old-file-content start-pos end-pos))
  192. bottom-content (db/get-block-content-rec bottom-block)
  193. top-content (remove-block-child! bottom-block top-block)
  194. top-area (subs 0 (get-start-pos top-block))
  195. bottom-area (subs
  196. (cond
  197. (and nested? (= direction :down))
  198. (get-end-pos bottom-block)
  199. target-child?
  200. (db/get-block-end-pos-rec repo top-block)
  201. :else
  202. (db/get-block-end-pos-rec repo bottom-block))
  203. nil)
  204. between-area (if (= direction :down)
  205. (subs (db/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
  206. (subs (db/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
  207. up-content (when (= direction :up)
  208. (cond
  209. nested?
  210. (util/join-newline (:block/content top-block)
  211. target-content
  212. (if target-child?
  213. (remove-block-child! target-block (:block/children to-block))
  214. (db/get-block-content-rec (:block/children top-block))))
  215. (and top? target-child?)
  216. (util/join-newline target-content (remove-block-child! target-block to-block))
  217. top?
  218. (util/join-newline target-content top-content)
  219. :else
  220. (let [top-content (if target-child?
  221. (remove-block-child! target-block to-block)
  222. top-content)]
  223. (util/join-newline top-content target-content))))
  224. down-content (when (= direction :down)
  225. (cond
  226. nested?
  227. (util/join-newline (:block/content bottom-block)
  228. target-content)
  229. target-child?
  230. (util/join-newline top-content target-content)
  231. :else
  232. (util/join-newline bottom-content target-content)))
  233. ;; _ (prn {:direction direction
  234. ;; :nested? nested?
  235. ;; :top? top?
  236. ;; :target-child? target-child?
  237. ;; :top-area top-area
  238. ;; :up-content up-content
  239. ;; :between-area between-area
  240. ;; :down-content down-content
  241. ;; :bottom-area bottom-area
  242. ;; })
  243. new-file-content (string/trim
  244. (util/join-newline
  245. top-area
  246. up-content
  247. between-area
  248. down-content
  249. bottom-area))
  250. 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)
  251. (remove nil?))
  252. path (:file/path (db/entity repo (:db/id (:block/file to-block))))
  253. modified-time (let [modified-at (tc/to-long (t/now))]
  254. (->
  255. [[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
  256. [:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
  257. [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
  258. [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
  259. distinct
  260. vec))]
  261. (profile
  262. "Move block in the same file: "
  263. (repo-handler/transact-react-and-alter-file!
  264. repo
  265. (concat
  266. after-blocks
  267. modified-time)
  268. {:key :block/change
  269. :data block-changes}
  270. [[path new-file-content]]))
  271. ;; (alter-file repo
  272. ;; path
  273. ;; new-file-content
  274. ;; {:re-render-root? true})
  275. )))
  276. (defn- move-block-in-different-files
  277. [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]
  278. (let [target-file (db/entity repo (:db/id (:block/file target-block)))
  279. target-file-path (:file/path target-file)
  280. target-file-content (db/get-file repo target-file-path)
  281. to-file (db/entity repo (:db/id (:block/file to-block)))
  282. to-file-path (:file/path to-file)
  283. target-block-end-pos (db/get-block-end-pos-rec repo target-block)
  284. to-block-start-pos (get-start-pos to-block)
  285. to-block-end-pos (db/get-block-end-pos-rec repo to-block)
  286. new-target-file-content (utf8/delete! target-file-content
  287. (get-start-pos target-block)
  288. target-block-end-pos)
  289. to-file-content (utf8/encode (db/get-file repo to-file-path))
  290. new-to-file-content (let [separate-pos (cond nested?
  291. (get-end-pos to-block)
  292. top?
  293. to-block-start-pos
  294. :else
  295. to-block-end-pos)]
  296. (string/trim
  297. (util/join-newline
  298. (utf8/substring to-file-content 0 separate-pos)
  299. target-content
  300. (utf8/substring to-file-content separate-pos))))
  301. modified-time (let [modified-at (tc/to-long (t/now))]
  302. (->
  303. [[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
  304. [:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
  305. [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
  306. [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
  307. distinct
  308. vec))
  309. target-after-blocks (rebuild-dnd-blocks repo target-file target-child?
  310. (get-start-pos target-block)
  311. block-changes nil {:delete? true})
  312. to-after-blocks (cond
  313. top?
  314. (rebuild-dnd-blocks repo to-file target-child?
  315. (get-start-pos to-block)
  316. block-changes
  317. nil
  318. {:same-file? false})
  319. :else
  320. (let [offset-block-id (if nested?
  321. (:block/uuid to-block)
  322. (last (db/get-block-ids to-block)))
  323. offset-end-pos (get-end-pos
  324. (db/entity repo [:block/uuid offset-block-id]))]
  325. (rebuild-dnd-blocks repo to-file target-child?
  326. offset-end-pos
  327. block-changes
  328. nil
  329. {:same-file? false})))]
  330. (profile
  331. "Move block between different files: "
  332. (repo-handler/transact-react-and-alter-file!
  333. repo
  334. (concat
  335. target-after-blocks
  336. to-after-blocks
  337. modified-time)
  338. {:key :block/change
  339. :data (conj block-changes target-block)}
  340. [[target-file-path new-target-file-content]
  341. [to-file-path new-to-file-content]]))))
  342. (defn- move-block-in-different-repos
  343. [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]
  344. (let [target-file (db/entity target-block-repo (:db/id (:block/file target-block)))
  345. target-file-path (:file/path target-file)
  346. target-file-content (db/get-file target-block-repo target-file-path)
  347. to-file (db/entity to-block-repo (:db/id (:block/file to-block)))
  348. to-file-path (:file/path to-file)
  349. target-block-end-pos (db/get-block-end-pos-rec target-block-repo target-block)
  350. to-block-start-pos (get-start-pos to-block)
  351. to-block-end-pos (db/get-block-end-pos-rec to-block-repo to-block)
  352. new-target-file-content (utf8/delete! target-file-content
  353. (get-start-pos target-block)
  354. target-block-end-pos)
  355. to-file-content (utf8/encode (db/get-file to-block-repo to-file-path))
  356. new-to-file-content (let [separate-pos (cond nested?
  357. (get-end-pos to-block)
  358. top?
  359. to-block-start-pos
  360. :else
  361. to-block-end-pos)]
  362. (string/trim
  363. (util/join-newline
  364. (utf8/substring to-file-content 0 separate-pos)
  365. target-content
  366. (utf8/substring to-file-content separate-pos))))
  367. target-delete-tx (map (fn [id]
  368. [:db.fn/retractEntity [:block/uuid id]])
  369. (db/get-block-ids target-block))
  370. [target-modified-time to-modified-time]
  371. (let [modified-at (tc/to-long (t/now))]
  372. [[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
  373. [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]]
  374. [[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
  375. [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]])
  376. target-after-blocks (rebuild-dnd-blocks target-block-repo target-file target-child?
  377. (get-start-pos target-block)
  378. block-changes nil {:delete? true})
  379. to-after-blocks (cond
  380. top?
  381. (rebuild-dnd-blocks to-block-repo to-file target-child?
  382. (get-start-pos to-block)
  383. block-changes
  384. nil
  385. {:same-file? false})
  386. :else
  387. (let [offset-block-id (if nested?
  388. (:block/uuid to-block)
  389. (last (db/get-block-ids to-block)))
  390. offset-end-pos (get-end-pos
  391. (db/entity to-block-repo [:block/uuid offset-block-id]))]
  392. (rebuild-dnd-blocks to-block-repo to-file target-child?
  393. offset-end-pos
  394. block-changes
  395. nil
  396. {:same-file? false})))]
  397. (profile
  398. "[Target file] Move block between different files: "
  399. (repo-handler/transact-react-and-alter-file!
  400. target-block-repo
  401. (concat
  402. target-delete-tx
  403. target-after-blocks
  404. target-modified-time)
  405. {:key :block/change
  406. :data [(dissoc target-block :block/children)]}
  407. [[target-file-path new-target-file-content]]))
  408. (profile
  409. "[Destination file] Move block between different files: "
  410. (repo-handler/transact-react-and-alter-file!
  411. to-block-repo
  412. (concat
  413. to-after-blocks
  414. to-modified-time)
  415. {:key :block/change
  416. :data [block-changes]}
  417. [[to-file-path new-to-file-content]]))))
  418. (defn move-block
  419. "There can be at least 3 possible situations:
  420. 1. Move a block in the same file (either top-to-bottom or bottom-to-top).
  421. 2. Move a block between two different files.
  422. 3. Move a block between two files in different repos.
  423. Notes:
  424. 1. Those two blocks might have different formats, e.g. one is `org` and another is `markdown`,
  425. we don't handle this now. TODO: transform between different formats in mldoc.
  426. 2. Sometimes we might need to move a parent block to it's own child.
  427. "
  428. [target-block to-block target-dom-id top? nested?]
  429. (when (and target-block to-block (:block/format target-block) (:block/format to-block))
  430. (cond
  431. (not= (:block/format target-block)
  432. (:block/format to-block))
  433. (notification/show!
  434. (util/format "Sorry, you can't move a block of format %s to another file of format %s."
  435. (:block/format target-block)
  436. (:block/format to-block))
  437. :error)
  438. (= (:block/uuid target-block) (:block/uuid to-block))
  439. nil
  440. :else
  441. (let [pattern (config/get-block-pattern (:block/format to-block))
  442. target-block-repo (:block/repo target-block)
  443. to-block-repo (:block/repo to-block)
  444. target-block (assoc target-block
  445. :block/meta
  446. (:block/meta (db/entity target-block-repo [:block/uuid (:block/uuid target-block)])))
  447. to-block (assoc to-block
  448. :block/meta
  449. (:block/meta (db/entity [:block/uuid (:block/uuid to-block)])))
  450. same-repo? (= target-block-repo to-block-repo)
  451. target-file (:block/file target-block)
  452. same-file? (and
  453. same-repo?
  454. (= (:db/id target-file)
  455. (:db/id (:block/file to-block))))
  456. [top-block bottom-block] (if same-file?
  457. (if (< (get-start-pos target-block)
  458. (get-start-pos to-block))
  459. [target-block to-block]
  460. [to-block target-block])
  461. [nil nil])
  462. target-child? (compute-target-child? target-block to-block)
  463. direction (compute-direction target-block top-block nested? top? target-child?)
  464. original-top-block-start-pos (get-start-pos top-block)
  465. [target-content block-changes] (recompute-block-content-and-changes target-block to-block nested? same-repo? same-file?)]
  466. (cond
  467. same-file?
  468. (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)
  469. ;; same repo but different files
  470. same-repo?
  471. (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)
  472. ;; different repos
  473. :else
  474. (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))))))