(ns ^:no-doc frontend.handler.block (:require [clojure.string :as string] [clojure.walk :as walk] [datascript.impl.entity :as de] [dommy.core :as dom] [frontend.config :as config] [frontend.db :as db] [frontend.db.async :as db-async] [frontend.db.model :as db-model] [frontend.handler.file-based.property.util :as property-util] [frontend.handler.property.util :as pu] [frontend.mobile.haptics :as haptics] [frontend.modules.outliner.op :as outliner-op] [frontend.modules.outliner.ui :as ui-outliner-tx] [frontend.state :as state] [frontend.util :as util] [frontend.util.file-based.drawer :as drawer] [goog.object :as gobj] [logseq.db :as ldb] [logseq.db.sqlite.util :as sqlite-util] [logseq.graph-parser.block :as gp-block] [logseq.outliner.core :as outliner-core] [logseq.outliner.op] [promesa.core :as p])) ;; Fns ;; TODO: reduced version (defn- walk-block [block check? transform] (let [result (atom nil)] (walk/postwalk (fn [x] (if (check? x) (reset! result (transform x)) x)) (:block.temp/ast-body block)) @result)) (defn get-timestamp [block typ] (walk-block block (fn [x] (and (gp-block/timestamp-block? x) (= typ (first (second x))))) #(second (second %)))) (defn get-scheduled-ast [block] (get-timestamp block "Scheduled")) (defn get-deadline-ast [block] (get-timestamp block "Deadline")) (defn indentable? [{:block/keys [parent] :as block}] (when parent (not= (:db/id (ldb/get-first-child (db/get-db) (:db/id parent))) (:db/id block)))) (defn outdentable? [{:block/keys [level] :as _block}] (not= level 1)) (defn select-block! [block-uuid] (let [blocks (util/get-blocks-by-id block-uuid)] (when (seq blocks) (state/exit-editing-and-set-selected-blocks! blocks)))) (defn get-idx-of-order-list-block [block order-list-type] (let [order-block-fn? (fn [block] (let [type (pu/lookup block :logseq.property/order-list-type)] (= type order-list-type))) prev-block-fn #(some-> (db/entity (:db/id %)) ldb/get-left-sibling) prev-block (prev-block-fn block)] (letfn [(page-fn? [b] (some-> b :block/name some?)) (order-sibling-list [b] (lazy-seq (when (and (not (page-fn? b)) (order-block-fn? b)) (cons b (order-sibling-list (prev-block-fn b)))))) (order-parent-list [b] (lazy-seq (when (and (not (page-fn? b)) (order-block-fn? b)) (cons b (order-parent-list (db-model/get-block-parent (:block/uuid b)))))))] (let [idx (if prev-block (count (order-sibling-list block)) 1) order-parents-count (dec (count (order-parent-list block))) delta (if (neg? order-parents-count) 0 (mod order-parents-count 3))] (cond (zero? delta) idx (= delta 1) (some-> (util/convert-to-letters idx) util/safe-lower-case) :else (util/convert-to-roman idx)))))) (defn attach-order-list-state [config block] (let [type (pu/lookup block :logseq.property/order-list-type) own-order-list-type (some-> type str string/lower-case) own-order-list-index (some->> own-order-list-type (get-idx-of-order-list-block block))] (assoc config :own-order-list-type own-order-list-type :own-order-list-index own-order-list-index :own-order-number-list? (= own-order-list-type "number")))) (defn- text-range-by-lst-fst-line [content [direction pos]] (case direction :up (let [last-new-line (or (string/last-index-of content \newline) -1) end (+ last-new-line pos 1)] (subs content 0 end)) :down (-> (string/split-lines content) first (or "") (subs 0 pos)))) (defn mark-last-input-time! [repo] (when repo (state/set-editor-last-input-time! repo (util/time-ms)))) (defn- edit-block-aux [repo block content text-range {:keys [container-id direction event pos]}] (when block (let [container-id (or container-id (state/get-current-editor-container-id) :unknown-container)] (state/set-editing! (str "edit-block-" (:block/uuid block)) content block text-range {:db (db/get-db) :container-id container-id :direction direction :event event :pos pos})) (mark-last-input-time! repo))) (defn sanity-block-content [repo format content] (if (sqlite-util/db-based-graph? repo) content (-> (property-util/remove-built-in-properties format content) (drawer/remove-logbook)))) (defn block-unique-title "Multiple pages/objects may have the same `:block/title`. Notice: this doesn't prevent for pages/objects that have the same tag or created by different clients." [block] (let [block-e (cond (de/entity? block) block (uuid? (:block/uuid block)) (db/entity [:block/uuid (:block/uuid block)]) :else block) tags (remove (fn [t] (or (some-> (:block/raw-title block-e) (ldb/inline-tag? t)) (ldb/private-tags (:db/ident t)))) (map (fn [tag] (if (number? tag) (db/entity tag) tag)) (:block/tags block)))] (if (seq tags) (str (:block/title block) " " (string/join ", " (keep (fn [tag] (when-let [title (:block/title tag)] (str "#" title))) tags))) (:block/title block)))) (defn edit-block! [block pos & {:keys [_container-id custom-content tail-len save-code-editor?] :or {tail-len 0 save-code-editor? true} :as opts}] (when (and (not config/publishing?) (:block/uuid block)) (util/mobile-keep-keyboard-open) (let [repo (state/get-current-repo)] (p/do! (when-not (:block.temp/fully-loaded? (db/entity (:db/id block))) (db-async/ tail-len 0) (>= (count content) tail-len)) (subs content 0 (- (count content) tail-len)) (or (= :max pos) (<= content-length pos)) content :else (subs content 0 pos)) content (sanity-block-content repo (get block :block/format :markdown) content)] (state/clear-selection!) (edit-block-aux repo block content text-range (assoc opts :pos pos)))))))) (defn- get-original-block-by-dom [node] (when-let [id (some-> node (gobj/get "parentNode") (util/rec-get-node "ls-block") (dom/attr "originalblockid") uuid)] (db/entity [:block/uuid id]))) (defn- get-original-block "Get the original block from the current editing block or selected blocks" [linked-block] (cond (and (= (:block/uuid linked-block) (:block/uuid (state/get-edit-block))) (state/get-input)) ; editing block (get-original-block-by-dom (state/get-input)) (seq (state/get-selection-blocks)) (->> (state/get-selection-blocks) (remove nil?) (keep #(when-let [id (dom/attr % "blockid")] (when (= (uuid id) (:block/uuid linked-block)) (when-let [original-id (some-> (dom/attr % "originalblockid") uuid)] (db/entity [:block/uuid original-id]))))) ;; FIXME: what if there're multiple same blocks in the selection first))) (defn get-top-level-blocks "Get only the top level blocks and their original blocks." [blocks] {:pre [(seq blocks)]} (let [level-blocks (outliner-core/blocks-with-level blocks)] (->> (filter (fn [b] (= 1 (:block/level b))) level-blocks) (map (fn [b] (let [original (get-original-block b)] (or (and original (db/entity (:db/id original))) b))))))) (defn get-current-editing-original-block [] (when-let [input (state/get-input)] (get-original-block-by-dom (util/rec-get-node input "ls-block")))) (defn get-first-block-original [] (or (get-current-editing-original-block) (when-let [node (some-> (first (state/get-selection-blocks)))] (get-original-block-by-dom node)))) (comment (defn- get-last-block-original [last-top-block] (or (get-current-editing-original-block) (when-let [last-block-node (->> (state/get-selection-blocks) (filter (fn [node] (= (dom/attr node "blockid") (str (:block/uuid last-top-block))))) last)] (get-original-block-by-dom last-block-node))))) (defn indent-outdent-blocks! [blocks indent? save-current-block] (when (seq blocks) (let [blocks (get-top-level-blocks blocks)] (ui-outliner-tx/transact! {:outliner-op :move-blocks :real-outliner-op :indent-outdent} (when save-current-block (save-current-block)) (outliner-op/indent-outdent-blocks! (get-top-level-blocks blocks) indent? {:parent-original (get-first-block-original) :logical-outdenting? (state/logical-outdenting?)}))))) (def *swipe (atom nil)) (def *touch-start (atom nil)) (defn- target-disable-swipe? [target] (let [user-defined-tags (get-in (state/get-config) [:mobile :gestures/disabled-in-block-with-tags])] (or (.closest target ".dsl-query") (.closest target ".drawer") (.closest target ".draw-wrap") (some #(.closest target (util/format "[data-refs-self*=%s]" %)) user-defined-tags)))) (defn on-touch-start [event uuid] (util/stop-propagation event) (let [input (state/get-input) input-id (state/get-edit-input-id) selection-type (.-type (.getSelection js/document))] (reset! *touch-start (js/Date.now)) (when-not (and input (string/ends-with? input-id (str uuid))) (state/clear-edit!)) (when (not= selection-type "Range") (when-let [touches (.-targetTouches event)] (when (= (.-length touches) 1) (let [touch (aget touches 0) x (.-clientX touch) y (.-clientY touch)] (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil}))))))) ;; FIXME: disable scroll (defn on-touch-move [^js goog-event] (let [event (.-event_ goog-event)] (when-let [touches (.-targetTouches event)] (let [selection-type (.-type (.getSelection js/document)) target (.-target event) block-container (util/rec-get-node target "ls-block")] (when-not (= selection-type "Range") (when (or (not (state/editing?)) (< (- (js/Date.now) @*touch-start) 600)) (when (and (= (.-length touches) 1) @*swipe) (let [{:keys [x0 xi direction]} @*swipe touch (aget touches 0) tx (.-clientX touch) ty (.-clientY touch) direction (if (nil? direction) (if (> tx x0) :right :left) direction)] (swap! *swipe #(-> % (assoc :tx tx) (assoc :ty ty) (assoc :xi tx) (assoc :yi ty) (assoc :direction direction))) (when (< (* (- xi x0) (- tx xi)) 0) (swap! *swipe #(-> % (assoc :x0 tx) (assoc :y0 ty)))) (let [{:keys [x0 y0]} @*swipe dx (- tx x0) dy (- ty y0)] (when (and (< (. js/Math abs dy) 30) (> (. js/Math abs dx) 10) direction) (.preventDefault goog-event) (let [left (if (= direction :right) (if (>= dx 0) (min dx 60) (max dx 0)) (if (<= dx 0) (- (min (js/Math.abs dx) 60)) (min dx 60)))] (dom/set-style! block-container :transform (util/format "translateX(%dpx)" left))))))))))))) (defn on-touch-end [event] (util/stop-propagation event) (when @*swipe (let [target (.-target event) {:keys [x0 y0 tx ty]} @*swipe dy (- ty y0) dx (- tx x0) block-container (util/rec-get-node target "ls-block")] (try (when (and (> (. js/Math abs dx) (. js/Math abs dy)) (> (. js/Math abs dx) 10)) (dom/set-style! block-container :transform "translateX(0)") (state/exit-editing-and-set-selected-blocks! [block-container]) (haptics/haptics) ;; (haptics/with-haptics-impact ;; (do (state/set-state! :mobile/show-action-bar? true) ;; (state/set-state! :mobile/actioned-block block) ;; (select-block! uuid)) ;; :light) ) (catch :default e (js/console.error e)) (finally (reset! *swipe nil)))))) (defn on-touch-cancel [_e] (reset! *swipe nil))