mirror of https://github.com/logseq/logseq
parent
3ce794eaf8
commit
9325a93e06
|
@ -9,3 +9,5 @@ dummy.fullpath = function() {};
|
|||
dummy.getRangeAt = function() {};
|
||||
dummy.getElementsByClassName = function() {};
|
||||
dummy.containsNode = function() {};
|
||||
dummy.select = function() {};
|
||||
dummy.setAttribute = function() {};
|
||||
|
|
|
@ -54,34 +54,28 @@
|
|||
(mixins/listen state js/window "click"
|
||||
(fn [e]
|
||||
;; hide context menu
|
||||
(let [context-menu (d/by-id "custom-context-menu")]
|
||||
(when-not (d/has-class? context-menu "hidden")
|
||||
(d/add-class! context-menu "hidden"))
|
||||
;; enable scroll
|
||||
(let [main (d/by-id "main")]
|
||||
(d/remove-class! main "overflow-hidden")
|
||||
(d/add-class! main "overflow-y-scroll")))
|
||||
(state/hide-custom-context-menu!)
|
||||
|
||||
(when (state/in-selection-mode?)
|
||||
(doseq [heading (state/get-selection-headings)]
|
||||
(d/remove-class! heading "selected")
|
||||
(d/remove-class! heading "noselect"))
|
||||
(state/clear-selection!))))
|
||||
;; enable scroll
|
||||
(let [main (d/by-id "main")]
|
||||
(d/remove-class! main "overflow-hidden")
|
||||
(d/add-class! main "overflow-y-scroll"))
|
||||
|
||||
(handler/clear-selection!)))
|
||||
|
||||
(mixins/listen state js/window "contextmenu"
|
||||
(fn [e]
|
||||
(when (state/in-selection-mode?)
|
||||
(util/stop e)
|
||||
(let [client-x (gobj/get e "clientX")
|
||||
client-y (gobj/get e "clientY")
|
||||
context-menu (d/by-id "custom-context-menu")]
|
||||
(when context-menu
|
||||
(let [main (d/by-id "main")]
|
||||
;; disable scroll
|
||||
(d/remove-class! main "overflow-y-scroll")
|
||||
(d/add-class! main "overflow-hidden"))
|
||||
(d/remove-class! context-menu
|
||||
"hidden")
|
||||
client-y (gobj/get e "clientY")]
|
||||
(let [main (d/by-id "main")]
|
||||
;; disable scroll
|
||||
(d/remove-class! main "overflow-y-scroll")
|
||||
(d/add-class! main "overflow-hidden"))
|
||||
|
||||
(state/show-custom-context-menu!)
|
||||
(when-let [context-menu (d/by-id "custom-context-menu")]
|
||||
(d/set-style! context-menu
|
||||
:left (str client-x "px")
|
||||
:top (str client-y "px")))))))))
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
[frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.config :as config]))
|
||||
[frontend.config :as config]
|
||||
[daiquiri.core]))
|
||||
|
||||
(def active-button :a.mt-1.group.flex.items-center.px-2.py-2.text-base.leading-6.font-medium.rounded-md.text-white.bg-gray-900.focus:outline-none.focus:bg-gray-700.transition.ease-in-out.duration-150)
|
||||
(def inactive-button :a.mt-1.group.flex.items-center.px-2.py-2.text-base.leading-6.font-medium.rounded-md.text-gray-300.hover:text-white.hover:bg-gray-700.focus:outline-none.focus:text-white.focus:bg-gray-700.transition.ease-in-out.duration-150)
|
||||
|
@ -145,27 +146,39 @@
|
|||
|
||||
(rum/defc custom-context-menu-content
|
||||
[]
|
||||
[:div#custom-context-menu.w-48.rounded-md.shadow-lg.transition.ease-out.duration-100.transform.opacity-100.scale-100.enter-done.absolute.hidden
|
||||
[:div#custom-context-menu.w-48.rounded-md.shadow-lg.transition.ease-out.duration-100.transform.opacity-100.scale-100.enter-done.absolute
|
||||
[:div.py-1.rounded-md.bg-white.shadow-xs
|
||||
(ui/menu-link
|
||||
{:key "cut"
|
||||
:on-click (fn []
|
||||
(prn "cut"))}
|
||||
:on-click handler/cut-selection-headings}
|
||||
"Cut")
|
||||
(ui/menu-link
|
||||
{:key "copy"
|
||||
:on-click (fn []
|
||||
(prn "copy"))}
|
||||
:on-click handler/copy-selection-headings}
|
||||
"Copy")]])
|
||||
|
||||
;; TODO: content could be changed
|
||||
(rum/defc custom-context-menu
|
||||
;; Also, keyboard bindings should only be activated after
|
||||
;; headings were already selected.
|
||||
(defn cut-headings-and-clear-selections!
|
||||
[]
|
||||
(ui/css-transition
|
||||
{:class-names "fade"
|
||||
:timeout {:enter 500
|
||||
:exit 300}}
|
||||
(custom-context-menu-content)))
|
||||
(handler/cut-selection-headings)
|
||||
(handler/clear-selection!))
|
||||
(rum/defc custom-context-menu < rum/reactive
|
||||
(mixins/keyboard-mixin "ctrl+c"
|
||||
(fn []
|
||||
(handler/copy-selection-headings)
|
||||
(handler/clear-selection!)))
|
||||
(mixins/keyboard-mixin "ctrl+x" cut-headings-and-clear-selections!)
|
||||
(mixins/keyboard-mixin "backspace" cut-headings-and-clear-selections!)
|
||||
(mixins/keyboard-mixin "delete" handler/cut-selection-headings)
|
||||
[]
|
||||
(when (state/sub :custom-context-menu/show?)
|
||||
(ui/css-transition
|
||||
{:class-names "fade"
|
||||
:timeout {:enter 500
|
||||
:exit 300}}
|
||||
(custom-context-menu-content))))
|
||||
|
||||
(rum/defcs sidebar < (mixins/modal)
|
||||
rum/reactive
|
||||
|
|
|
@ -343,20 +343,20 @@
|
|||
react
|
||||
sort-by-pos)))
|
||||
|
||||
;; (defn get-file-by-concat-headings-debug-version
|
||||
;; ([file]
|
||||
;; (get-file-by-concat-headings-debug-version
|
||||
;; (state/get-current-repo)
|
||||
;; file))
|
||||
;; ([repo-url file]
|
||||
;; (->> (d/q '[:find (pull ?heading [*])
|
||||
;; :in $ ?file
|
||||
;; :where
|
||||
;; [?p :file/path ?file]
|
||||
;; [?heading :heading/file ?p]]
|
||||
;; (get-conn) file)
|
||||
;; seq-flatten
|
||||
;; sort-by-pos)))
|
||||
(defn get-file-by-concat-headings-debug-version
|
||||
([file]
|
||||
(get-file-by-concat-headings-debug-version
|
||||
(state/get-current-repo)
|
||||
file))
|
||||
([repo-url file]
|
||||
(->> (d/q '[:find (pull ?heading [*])
|
||||
:in $ ?file
|
||||
:where
|
||||
[?p :file/path ?file]
|
||||
[?heading :heading/file ?p]]
|
||||
(get-conn) file)
|
||||
seq-flatten
|
||||
sort-by-pos)))
|
||||
|
||||
(defn get-page-headings
|
||||
([page]
|
||||
|
@ -702,6 +702,14 @@
|
|||
(get-conn))
|
||||
seq-flatten))
|
||||
|
||||
;; TODO: Does the result preserves the order of the arguments?
|
||||
(defn get-headings-contents
|
||||
[heading-uuids]
|
||||
(let [conn (get-conn (state/get-current-repo) false)]
|
||||
;; (prn {:db db})
|
||||
(d/pull-many (d/db conn) '[:heading/content]
|
||||
(mapv (fn [id] [:heading/uuid id]) heading-uuids))))
|
||||
|
||||
(defn reset-config!
|
||||
[repo-url content]
|
||||
(let [config (some->> content
|
||||
|
|
|
@ -672,32 +672,36 @@
|
|||
[(str prefix value postfix)
|
||||
value]))
|
||||
|
||||
(defn rebuild-after-headings
|
||||
[repo file before-end-pos new-end-pos]
|
||||
(let [file-id (:db/id file)
|
||||
after-headings (db/get-file-after-headings repo file-id before-end-pos)
|
||||
last-start-pos (atom new-end-pos)]
|
||||
(mapv
|
||||
(fn [{:heading/keys [uuid meta] :as heading}]
|
||||
(let [old-start-pos (:pos meta)
|
||||
old-end-pos (:end-pos meta)
|
||||
new-end-pos (if old-end-pos
|
||||
(+ @last-start-pos (- old-end-pos old-start-pos)))
|
||||
new-meta {:pos @last-start-pos
|
||||
:end-pos new-end-pos}]
|
||||
(reset! last-start-pos new-end-pos)
|
||||
{:heading/uuid uuid
|
||||
:heading/meta new-meta}))
|
||||
after-headings)))
|
||||
|
||||
(defn save-heading-if-changed!
|
||||
[{:heading/keys [uuid content meta file dummy?] :as heading} value]
|
||||
(let [repo (state/get-current-repo)
|
||||
value (string/trim value)]
|
||||
(when (not= (string/trim content) value) ; heading content changed
|
||||
(let [file-id (:db/id file)
|
||||
file (db/entity file-id)
|
||||
(let [file (db/entity (:db/id file))
|
||||
file-content (:file/content file)
|
||||
file-path (:file/path file)
|
||||
format (format/get-format file-path)
|
||||
[new-content value] (new-file-content heading file-content value)
|
||||
after-headings (db/get-file-after-headings repo file-id (get meta :end-pos))
|
||||
{:keys [headings pages start-pos end-pos]} (block/parse-heading (assoc heading :heading/content value) format)
|
||||
last-start-pos (atom end-pos)
|
||||
after-headings (mapv
|
||||
(fn [{:heading/keys [uuid meta] :as heading}]
|
||||
(let [old-start-pos (:pos meta)
|
||||
old-end-pos (:end-pos meta)
|
||||
new-end-pos (if old-end-pos
|
||||
(+ @last-start-pos (- old-end-pos old-start-pos)))
|
||||
new-meta {:pos @last-start-pos
|
||||
:end-pos new-end-pos}]
|
||||
(reset! last-start-pos new-end-pos)
|
||||
{:heading/uuid uuid
|
||||
:heading/meta new-meta}))
|
||||
after-headings)]
|
||||
after-headings (rebuild-after-headings repo file (:end-pos meta) end-pos)]
|
||||
(db/transact!
|
||||
(concat
|
||||
pages
|
||||
|
@ -705,10 +709,7 @@
|
|||
after-headings
|
||||
[{:file/path file-path
|
||||
:file/content new-content}]))
|
||||
(alter-file repo
|
||||
file-path
|
||||
new-content
|
||||
{:reset? false})))))
|
||||
(alter-file repo file-path new-content {:reset? false})))))
|
||||
|
||||
(defn delete-heading!
|
||||
[{:heading/keys [uuid meta content file] :as heading} dummy?]
|
||||
|
@ -716,32 +717,39 @@
|
|||
(let [repo (state/get-current-repo)
|
||||
file-path (:file/path (db/entity (:db/id file)))
|
||||
file-content (:file/content (db/entity (:db/id file)))
|
||||
after-headings (db/get-file-after-headings repo (:db/id file) (:end-pos meta))
|
||||
last-start-pos (atom (:pos meta))
|
||||
updated-headings (mapv
|
||||
(fn [{:heading/keys [uuid meta] :as heading}]
|
||||
(let [old-start-pos (:pos meta)
|
||||
old-end-pos (:end-pos meta)
|
||||
new-end-pos (if old-end-pos
|
||||
(+ @last-start-pos (- old-end-pos old-start-pos)))
|
||||
new-meta {:pos @last-start-pos
|
||||
:end-pos new-end-pos}]
|
||||
(reset! last-start-pos new-end-pos)
|
||||
{:heading/uuid uuid
|
||||
:heading/meta new-meta}))
|
||||
after-headings)
|
||||
after-headings (rebuild-after-headings repo file (:end-pos meta) (:pos meta))
|
||||
new-content (utf8/delete! file-content (:pos meta) (:end-pos meta))]
|
||||
(db/transact!
|
||||
(concat
|
||||
[[:db.fn/retractEntity [:heading/uuid uuid]]]
|
||||
updated-headings
|
||||
after-headings
|
||||
[{:file/path file-path
|
||||
:file/content new-content}]))
|
||||
(alter-file repo
|
||||
file-path
|
||||
new-content
|
||||
{:reset? false})
|
||||
)))
|
||||
(alter-file repo file-path new-content {:reset? false}))))
|
||||
|
||||
(defn delete-headings!
|
||||
[heading-uuids]
|
||||
(when (seq heading-uuids)
|
||||
(let [repo (state/get-current-repo)
|
||||
first-heading (db/entity [:heading/uuid (first heading-uuids)])
|
||||
last-heading (db/entity [:heading/uuid (last heading-uuids)])
|
||||
file (db/entity (:db/id (:heading/file first-heading)))
|
||||
file-path (:file/path file)
|
||||
file-content (:file/content file)
|
||||
start-pos (:pos (:heading/meta first-heading))
|
||||
end-pos (:end-pos (:heading/meta last-heading))
|
||||
after-headings (rebuild-after-headings repo file end-pos start-pos)
|
||||
new-content (utf8/delete! file-content start-pos end-pos)]
|
||||
(db/transact!
|
||||
(concat
|
||||
(mapv
|
||||
(fn [uuid]
|
||||
[:db.fn/retractEntity [:heading/uuid uuid]])
|
||||
heading-uuids)
|
||||
after-headings
|
||||
[{:file/path file-path
|
||||
:file/content new-content}]))
|
||||
(alter-file repo file-path new-content {:reset? false}))))
|
||||
|
||||
(defn clone-and-pull
|
||||
[repo-url]
|
||||
|
@ -811,6 +819,32 @@
|
|||
(state/set-cursor-range! text-range)
|
||||
(state/set-edit-input-id! edit-input-id))))
|
||||
|
||||
;; headings
|
||||
|
||||
(defn clear-selection!
|
||||
[]
|
||||
(when (state/in-selection-mode?)
|
||||
(doseq [heading (state/get-selection-headings)]
|
||||
(dom/remove-class! heading "selected")
|
||||
(dom/remove-class! heading "noselect"))
|
||||
(state/clear-selection!)))
|
||||
|
||||
(defn copy-selection-headings
|
||||
[]
|
||||
(when-let [headings (seq (get @state/state :selection/headings))]
|
||||
(let [ids (map #(util/get-heading-id (gobj/get % "id")) headings)
|
||||
content (some->> (db/get-headings-contents ids)
|
||||
(map :heading/content)
|
||||
(string/join ""))]
|
||||
(when-not (string/blank? content)
|
||||
(util/copy-to-clipboard! content)))))
|
||||
|
||||
(defn cut-selection-headings
|
||||
[]
|
||||
(when-let [headings (seq (get @state/state :selection/headings))]
|
||||
(let [ids (map #(util/get-heading-id (gobj/get % "id")) headings)]
|
||||
(delete-headings! ids))))
|
||||
|
||||
(defn start!
|
||||
[]
|
||||
(let [{:keys [repos] :as me} (set-me-if-exists!)]
|
||||
|
@ -838,15 +872,15 @@
|
|||
(p/let [changes (git/get-status-matrix (state/get-current-repo))]
|
||||
(prn changes)))
|
||||
|
||||
;; (defn debug-file-and-headings
|
||||
;; [path]
|
||||
;; (p/let [content (load-file (state/get-current-repo)
|
||||
;; path)]
|
||||
;; (let [db-content (db/get-file path)
|
||||
;; headings (db/get-file-by-concat-headings-debug-version path)]
|
||||
;; (prn {:content content
|
||||
;; :utf8-length (utf8/length (utf8/encode content))
|
||||
;; :headings headings}))))
|
||||
(defn debug-file-and-headings
|
||||
[path]
|
||||
(p/let [content (load-file (state/get-current-repo)
|
||||
path)]
|
||||
(let [db-content (db/get-file path)
|
||||
headings (db/get-file-by-concat-headings-debug-version path)]
|
||||
(prn {:content content
|
||||
:utf8-length (utf8/length (utf8/encode content))
|
||||
:headings headings}))))
|
||||
|
||||
;; (debug-file-and-headings "readme.org")
|
||||
)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
(ns frontend.keyboard
|
||||
(:require [goog.events :as events]
|
||||
[goog.ui.KeyboardShortcutHandler.EventType :as EventType]
|
||||
[goog.events.KeyCodes :as KeyCodes])
|
||||
(:import [goog.ui KeyboardShortcutHandler]))
|
||||
|
||||
;; Copy from https://github.com/tonsky/rum/blob/gh-pages/doc/useful-mixins.md#keyboard-shortcut
|
||||
|
||||
(defn install-shortcut!
|
||||
"Installs a Keyboard Shortcut handler.
|
||||
The key is a string the trigger is a function that will receive the keyboard event as the
|
||||
first argument. If once? is true the keyboard shortcut is only fired once.
|
||||
The unregister handler is returned and can be called to unregister the listener.
|
||||
If target is not given it's attached to window."
|
||||
([key trigger] (install-shortcut! key trigger false js/window))
|
||||
([key trigger once?] (install-shortcut! key trigger once? js/window))
|
||||
([key trigger once? target]
|
||||
(let [handler (new KeyboardShortcutHandler target)]
|
||||
(.registerShortcut handler (str key once?) key)
|
||||
(events/listen
|
||||
handler
|
||||
EventType/SHORTCUT_TRIGGERED
|
||||
(fn [e]
|
||||
(trigger e)
|
||||
(when once?
|
||||
(.unregisterShortcut handler keys))))
|
||||
(fn []
|
||||
(.unregisterShortcut handler key)))))
|
|
@ -1,7 +1,8 @@
|
|||
(ns frontend.mixins
|
||||
(:require [rum.core :as rum]
|
||||
[goog.dom :as dom]
|
||||
[goog.object :as gobj])
|
||||
[goog.object :as gobj]
|
||||
[frontend.keyboard :as keyboard])
|
||||
(:import [goog.events EventHandler]))
|
||||
|
||||
(defn detach
|
||||
|
@ -142,3 +143,23 @@
|
|||
{:will-mount (fn [state]
|
||||
(handler (:rum/args state))
|
||||
state)})
|
||||
|
||||
(defn keyboard-mixin
|
||||
"Triggers f when key is pressed while the component is mounted.
|
||||
if target is a function it will be called AFTER the component mounted
|
||||
with state and should return a dom node that is the target of the listener.
|
||||
If no target is given it is defaulted to js/window (global handler)
|
||||
Ex:
|
||||
(keyboard-mixin \"esc\" #(browse-to :home/home))"
|
||||
([key f] (keyboard-mixin key f js/window))
|
||||
([key f target]
|
||||
(let [target-fn (if (fn? target) target (fn [_] target))]
|
||||
{:did-mount
|
||||
(fn [state]
|
||||
;; (prn "add shortcut: " key)
|
||||
(assoc state ::keyboard-listener
|
||||
(keyboard/install-shortcut! key f false (target-fn state))))
|
||||
:will-unmount
|
||||
(fn [state]
|
||||
((::keyboard-listener state))
|
||||
state)})))
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
|
||||
:selection/mode false
|
||||
:selection/headings nil
|
||||
:custom-context-menu/show? false
|
||||
}))
|
||||
|
||||
(defn sub
|
||||
|
@ -209,3 +210,11 @@
|
|||
(defn in-selection-mode?
|
||||
[]
|
||||
(:selection/mode @state))
|
||||
|
||||
(defn show-custom-context-menu!
|
||||
[]
|
||||
(swap! state assoc :custom-context-menu/show? true))
|
||||
|
||||
(defn hide-custom-context-menu!
|
||||
[]
|
||||
(swap! state assoc :custom-context-menu/show? false))
|
||||
|
|
|
@ -471,16 +471,31 @@
|
|||
[class-name]
|
||||
(try
|
||||
(when (gobj/get js/window "getSelection")
|
||||
(let [selection (js/window.getSelection)
|
||||
range (.getRangeAt selection 0)
|
||||
container (gobj/get range "commonAncestorContainer")]
|
||||
(let [container-nodes (array-seq (.getElementsByClassName container class-name))]
|
||||
(filter
|
||||
(fn [node]
|
||||
(.containsNode selection node true))
|
||||
container-nodes))))
|
||||
(let [selection (js/window.getSelection)
|
||||
range (.getRangeAt selection 0)
|
||||
container (gobj/get range "commonAncestorContainer")]
|
||||
(let [container-nodes (array-seq (.getElementsByClassName container class-name))]
|
||||
(filter
|
||||
(fn [node]
|
||||
(.containsNode selection node true))
|
||||
container-nodes))))
|
||||
(catch js/Error _e
|
||||
nil)))
|
||||
|
||||
(comment
|
||||
(get-selected-nodes "ls-heading-parent"))
|
||||
(defn get-heading-id
|
||||
[id]
|
||||
(try
|
||||
(uuid (string/replace id "ls-heading-parent-" ""))
|
||||
(catch js/Error e
|
||||
(prn "get-heading-id failed, error: " e))))
|
||||
|
||||
(defn copy-to-clipboard! [s]
|
||||
(let [el (js/document.createElement "textarea")]
|
||||
(set! (.-value el) s)
|
||||
(.setAttribute el "readonly" "")
|
||||
(set! (-> el .-style .-position) "absolute")
|
||||
(set! (-> el .-style .-left) "-9999px")
|
||||
(js/document.body.appendChild el)
|
||||
(.select el)
|
||||
(js/document.execCommand "copy")
|
||||
(js/document.body.removeChild el)))
|
||||
|
|
Loading…
Reference in New Issue