improve animations

This commit is contained in:
scheinriese
2025-11-28 04:07:26 +01:00
parent 41321fcb0f
commit 9df5f8b0aa
6 changed files with 127 additions and 56 deletions

View File

@@ -81,32 +81,42 @@
(defn- normalize-binding
"Normalizes a shortcut binding to a string format for data attributes.
Examples: 'cmd+k', 'shift+cmd+k', 'cmd up'"
Examples: 'cmd+k', 'shift+cmd+k', 'cmd up'
Handles 'mod' -> 'meta' on Mac, 'ctrl' on Windows/Linux for consistency with shortcut system."
[binding]
(cond
(string? binding)
(string/lower-case (string/trim binding))
(coll? binding)
(let [first-item (first binding)
keys (flatten-keys binding)
normalize-key (fn [k]
(cond
(string? k) (string/lower-case k)
(keyword? k) (name k)
(symbol? k) (name k)
(number? k) (str k)
:else (str k)))]
(string/join "+" (map normalize-key keys)))
(keyword? binding)
(name binding)
(symbol? binding)
(name binding)
:else
(str binding)))
(let [normalize-key (fn [k]
(cond
(string? k) (let [lowered (string/lower-case k)]
;; Convert 'mod' to 'meta' on Mac, 'ctrl' on Windows/Linux
;; to match what the shortcut system uses
(if (= lowered "mod")
(if mac? "meta" "ctrl")
lowered))
(keyword? k) (name k)
(symbol? k) (name k)
(number? k) (str k)
:else (str k)))]
(cond
(string? binding)
(let [trimmed (string/trim binding)
normalized (string/lower-case trimmed)]
;; Handle 'mod' in strings like "mod+k"
(if (string/includes? normalized "mod")
(string/replace normalized "mod" (if mac? "meta" "ctrl"))
normalized))
(coll? binding)
(let [keys (flatten-keys binding)]
(string/join "+" (map normalize-key keys)))
(keyword? binding)
(name binding)
(symbol? binding)
(name binding)
:else
(str binding))))
(defn- detect-style
"Automatically detects style from shortcut format.
@@ -140,7 +150,7 @@
(defn shortcut-press!
"Central helper to trigger key press animation.
Finds all nodes with matching data-shortcut-binding and toggles pressed class.
Finds all nodes with matching data-shortcut-binding and animates individual keys.
Optionally highlights parent row.
Args:
@@ -150,24 +160,46 @@
([binding highlight-row?]
(let [normalized (normalize-binding binding)
selector (str "[data-shortcut-binding=\"" normalized "\"]")
elements (.querySelectorAll js/document selector)]
(doseq [^js el (array-seq elements)]
(.add (.-classList el) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest el ".shui-shortcut-row")
(.-parentElement el))]
(when row
(.add (.-classList row) "shui-shortcut-row--pressed"))))
;; Auto-reset after animation duration
(js/setTimeout
(fn []
(.remove (.-classList el) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest el ".shui-shortcut-row")
(.-parentElement el))]
(when row
(.remove (.-classList row) "shui-shortcut-row--pressed")))))
160)))))
containers (.querySelectorAll js/document selector)]
(doseq [^js container (array-seq containers)]
;; Find all individual keys within this container
(let [keys (.querySelectorAll container "kbd.shui-shortcut-key")]
(if (> (.-length keys) 0)
;; Animate individual keys
(doseq [^js key-el (array-seq keys)]
(.add (.-classList key-el) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest container ".shui-shortcut-row")
(.-parentElement container))]
(when row
(.add (.-classList row) "shui-shortcut-row--pressed"))))
;; Auto-reset after animation duration
(js/setTimeout
(fn []
(.remove (.-classList key-el) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest container ".shui-shortcut-row")
(.-parentElement container))]
(when row
(.remove (.-classList row) "shui-shortcut-row--pressed")))))
160))
;; Fallback: animate container if no keys found
(do
(.add (.-classList container) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest container ".shui-shortcut-row")
(.-parentElement container))]
(when row
(.add (.-classList row) "shui-shortcut-row--pressed"))))
(js/setTimeout
(fn []
(.remove (.-classList container) "shui-shortcut-key-pressed")
(when highlight-row?
(let [^js row (or (.closest container ".shui-shortcut-row")
(.-parentElement container))]
(when row
(.remove (.-classList row) "shui-shortcut-row--pressed")))))
160))))))))
(rum/defc combo-keys
"Renders combo keys (simultaneous key combinations) with separator."
@@ -252,8 +284,10 @@
- :aria-label - accessibility label for container
- :aria-hidden? - if true, hides from screen readers (default: false for decorative hints)
- :animate-on-press? - if true, enables press animation (default: true)
- :glow? - if true, adds inner glow effect to combo/separate keys (default: true)"
[shortcut & {:keys [style size theme interactive? aria-label aria-hidden? animate-on-press? glow?]
- :glow? - if true, adds inner glow effect to combo/separate keys (default: true)
- :raw-binding - raw binding format for data-shortcut-binding (for animation matching).
If not provided, will normalize from shortcut prop."
[shortcut & {:keys [style size theme interactive? aria-label aria-hidden? animate-on-press? glow? raw-binding]
:or {style :auto
size :xs
interactive? false
@@ -293,6 +327,14 @@
:else
[(str binding)])
;; Use raw-binding if provided, otherwise normalize from binding
binding-for-data (if raw-binding
(if (coll? raw-binding)
(if (= (count raw-binding) 1)
(first raw-binding)
raw-binding)
raw-binding)
binding)
render-fn (case detected-style
:combo combo-keys
:separate separate-keys
@@ -307,5 +349,5 @@
(when (< 0 index)
[:span.text-gray-11.text-sm {:key (str "sep-" index)
:style {:margin "0 4px"}} "|"])
(render-fn keys binding opts)])))))
(render-fn keys binding-for-data opts)])))))

View File

@@ -826,6 +826,9 @@
(util/stop-propagation e))
(and meta? (= keyname "o"))
(open-current-item-link state)
(= keyname "/") (do
(shui/shortcut-press! "/" true)
nil) ; Don't prevent default, allow typing
:else nil)))
(defn- keyup-handler

View File

@@ -219,7 +219,7 @@
true)]]
(when (not (or (util/mobile?) (mobile-util/native-platform?)))
[:div {:style {:text-align "right"}}
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))])])
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets) :shortcut-id :ui/toggle-brackets)])])
(defn toggle-wide-mode-row [t wide-mode?]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
@@ -233,7 +233,7 @@
true)]]
(when (not (or (util/mobile?) (mobile-util/native-platform?)))
[:div {:style {:text-align "right"}}
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-wide-mode))])])
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-wide-mode) :shortcut-id :ui/toggle-wide-mode)])])
(defn editor-font-family-row [t font]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4
@@ -370,7 +370,7 @@
(row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
:-for "toggle_theme"
:action pick-theme
:desc (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-theme))})))
:desc (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-theme) :shortcut-id :ui/toggle-theme)})))
(rum/defc accent-color-row < rum/reactive
[_in-modal?]
@@ -426,7 +426,7 @@
(accent-color-row true)])
(defn date-format-row [t preferred-date-format]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "custom_date_format"}
(t :settings-page/custom-date-format)

View File

@@ -482,4 +482,6 @@
:white-space "nowrap"}}
(shui/shortcut
(string/join " | " (map #(dh/binding-for-display id %) binding))
{:size :md :interactive? true})])]]))))])])]]))
{:size :md
:interactive? true
:raw-binding binding})])]]))))])])]]))

View File

@@ -12,7 +12,8 @@
[frontend.util :as util]
[goog.events :as events]
[goog.ui.KeyboardShortcutHandler.EventType :as EventType]
[lambdaisland.glogi :as log])
[lambdaisland.glogi :as log]
[logseq.shui.ui :as shui])
(:import [goog.events KeyCodes KeyNames]
[goog.ui KeyboardShortcutHandler]))
@@ -135,8 +136,18 @@
(let [f (fn [e]
(let [id (keyword (.-identifier e))
shortcut-map (dh/shortcuts-map-by-handler-id handler-id state) ;; required to get shortcut map dynamically
dispatch-fn (get shortcut-map id)]
dispatch-fn (get shortcut-map id)
binding (dh/shortcut-binding id)]
(state/set-state! :editor/latest-shortcut id)
;; Trigger animation for visible shortcuts
(when binding
(let [bindings (if (coll? binding) binding [binding])]
(doseq [b bindings]
(when b
;; Use the raw binding, not decorated, for matching data attributes
(try
(shui/shortcut-press! b true)
(catch :default _e nil))))))
;; trigger fn
(when dispatch-fn
(plugin-handler/hook-lifecycle-fn! id dispatch-fn e))))

View File

@@ -21,6 +21,7 @@
[frontend.mobile.util :as mobile-util]
[frontend.modules.shortcut.config :as shortcut-config]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as shortcut-dh]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.rum :as r]
[frontend.state :as state]
@@ -201,15 +202,27 @@
(dropdown-content-wrapper dropdown-state close-fn modal-content modal-class {:z-index z-index}))))]))
;; `sequence` can be a list of symbols, a list of strings, or a string
(defn render-keyboard-shortcut [sequence & {:as opts}]
;; If `shortcut-id` is provided, uses raw binding from shortcut system for data attribute matching
(defn render-keyboard-shortcut [sequence & {:keys [shortcut-id] :as opts}]
(let [sequence (if (string? sequence)
(-> sequence ;; turn string into sequence
(string/trim)
(string/lower-case)
(string/split #" "))
sequence)
;; Get raw binding for data attribute matching
raw-binding (if shortcut-id
;; Get raw binding from shortcut system
(shortcut-dh/shortcut-binding shortcut-id)
;; Otherwise use sequence as-is (it's already the raw format)
(if (and (coll? sequence) (every? string? sequence))
sequence
(if (string? sequence)
[sequence]
sequence)))
opts (merge {:interactive? false
:aria-hidden? true} opts)]
:aria-hidden? true
:raw-binding raw-binding} opts)]
[:span.keyboard-shortcut
(shui/shortcut sequence opts)]))