Forráskód Böngészése

enhance: property keyboard navigation (#11825)

Keyboard enhancements:

1. UP/DOWN to navigate between blocks and properties, the property value will be focused and highlighted
2. Backspace/Delete to delete a property value and move it to the nearest property or block above
3. Enter/Space to open datetime and select type property value
Tienson Qin 6 hónapja
szülő
commit
6788818aec

+ 4 - 2
src/main/frontend/components/block.cljs

@@ -2799,7 +2799,9 @@
               [:div.flex.flex-row.items-center
                (property-component/property-key-cp block property opts)
                [:div.select-none ":"]]
-              (pv/property-value block property opts)]))]
+              [:div.ls-block.property-value-container
+               {:style {:min-height 20}}
+               (pv/property-value block property opts)]]))]
         [:div.positioned-properties.flex.flex-row.gap-1.select-none.h-6.self-start
          {:class (name position)}
          (for [pid properties]
@@ -3592,7 +3594,7 @@
         (when (and @*show-left-menu? (not in-whiteboard?) (not (or table? property?)))
           (block-left-menu config block))
 
-        [:div.flex.flex-col.w-full.overflow-hidden
+        [:div.flex.flex-col.w-full
          [:div.block-main-content.flex.flex-row.gap-2
           (when-let [actions-cp (:page-title-actions-cp config)]
             (actions-cp block))

+ 2 - 2
src/main/frontend/components/property.cljs

@@ -146,7 +146,7 @@
                                               (util/stop-propagation e)))} label)))))
      (when show-type-change-hints?
        (ui/tooltip (svg/info)
-         [:span "Changing the property type clears some property configurations."]))]))
+                   [:span "Changing the property type clears some property configurations."]))]))
 
 (rum/defc property-select
   [exclude-properties select-opts]
@@ -505,7 +505,7 @@
          (let [class-properties? (= (:db/ident property) :logseq.property.class/properties)
                property-desc (when-not (= (:db/ident property) :logseq.property/description)
                                (:logseq.property/description property))]
-           [:div.property-value-container.flex.flex-row.gap-1.items-center
+           [:div.ls-block.property-value-container.flex.flex-row.gap-1.items-center
             (cond-> {}
               class-properties? (assoc :class (if (:logseq.property.class/properties block)
                                                 "ml-2 -mt-1"

+ 14 - 2
src/main/frontend/components/property.css

@@ -41,6 +41,10 @@
   margin-top: 2px;
 }
 
+.property-value-container .jtrigger {
+  @apply focus-visible:outline-none focus-visible:bg-gray-02 focus-visible:rounded;
+}
+
 .ls-properties-area {
   .property-pair {
     @apply flex flex-row flex-wrap space-x-1;
@@ -90,6 +94,12 @@
         }
       }
 
+      &-inner[data-type=checkbox] {
+        .data-\[state\=checked\]\:bg-primary[data-state="checked"] {
+          background-color: hsl(var(--primary));
+        }
+      }
+
       &-inner[data-type=date] {
         .jtrigger-id {
           @apply top-[4px];
@@ -192,7 +202,7 @@
     @apply flex items-center;
   }
 
-  &[data-type="datetime"] {
+  &[data-type=datetime] {
     @apply whitespace-nowrap;
   }
 }
@@ -271,10 +281,12 @@ a.control-link {
   }
 }
 
-.property-value-container {
+.ls-block.property-value-container {
   @apply flex-1 shrink-0;
   min-width: 100px;
   min-height: 28px;
+  padding-top: 0;
+  padding-bottom: 0;
 }
 
 .ls-page-properties .property-key, .ls-properties-area .property-key {

+ 53 - 16
src/main/frontend/components/property/value.cljs

@@ -455,9 +455,17 @@
         (overdue date content)
         content))))
 
+(defn- delete-block-property!
+  [block property]
+  (editor-handler/move-cross-boundary-up-down :up {})
+  (property-handler/remove-block-property! (state/get-current-repo)
+                                           (:db/id block)
+                                           (:db/ident property)))
+
 (rum/defc date-picker
   [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
-  (let [content-fn (fn [{:keys [id]}] (calendar-inner id
+  (let [*el (rum/use-ref nil)
+        content-fn (fn [{:keys [id]}] (calendar-inner id
                                                       {:block block
                                                        :property property
                                                        :on-change on-change
@@ -480,13 +488,25 @@
          {:class "jtrigger h-6 empty-btn"
           :variant :text
           :size :sm
-          :on-click open-popup!}
+          :on-click open-popup!
+          :on-key-down (fn [e]
+                         (when (contains? #{"Backspace" "Delete"} (util/ekey e))
+                           (delete-block-property! block property)))}
          (ui/icon "calendar-plus" {:size 16}))
         (shui/trigger-as
          :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
-         {:tabIndex 0
+         {:ref *el
+          :tabIndex 0
           :class "jtrigger min-h-[24px]"                     ; FIXME: min-h-6 not works
-          :on-click open-popup!}
+          :on-click open-popup!
+          :on-key-down (fn [e]
+                         (case (util/ekey e)
+                           ("Backspace" "Delete")
+                           (delete-block-property! block property)
+                           (" " "Enter")
+                           (do (some-> (rum/deref *el) (.click))
+                               (util/stop e))
+                           nil))}
          [:div.flex.flex-row.gap-1.items-center
           (when repeated-task?
             (ui/icon "repeat" {:size 14 :class "opacity-40"}))
@@ -1078,12 +1098,18 @@
                            (select block property select-opts' opts)
 
                            (:node :class :property :page :date)
-                           (property-value-select-node block property select-opts' opts))])]
+                           (property-value-select-node block property select-opts' opts))])
+        trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
+        show-popup! (fn [target]
+                      (shui/popup-show! target popup-content
+                                        {:align "start"
+                                         :as-dropdown? true
+                                         :auto-focus? true
+                                         :trigger-id trigger-id}))]
     (if editing?
       (popup-content nil)
-      (let [trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
-            show! (fn [e]
-                    (let [target (.-target e)]
+      (let [show! (fn [e]
+                    (let [target (when e (.-target e))]
                       (when-not (or config/publishing?
                                     (util/shift-key? e)
                                     (util/meta-key? e)
@@ -1091,18 +1117,21 @@
                                     (when-let [node (.closest target "a")]
                                       (not (or (d/has-class? node "page-ref")
                                                (d/has-class? node "tag")))))
-
-                        (shui/popup-show! target popup-content
-                                          {:align "start"
-                                           :as-dropdown? true
-                                           :auto-focus? true
-                                           :trigger-id trigger-id}))))]
+                        (show-popup! target))))]
         (shui/trigger-as
          (if (:other-position? opts) :div.jtrigger :div.jtrigger.flex.flex-1.w-full)
          {:ref *el
           :id trigger-id
           :tabIndex 0
-          :on-click show!}
+          :on-click show!
+          :on-key-down (fn [e]
+                         (case (util/ekey e)
+                           ("Backspace" "Delete")
+                           (delete-block-property! block property)
+                           (" " "Enter")
+                           (do (some-> (rum/deref *el) (.click))
+                               (util/stop e))
+                           nil))}
          (if (string/blank? value)
            (property-empty-text-value property opts)
            (value-render)))))))
@@ -1120,6 +1149,10 @@
      {:id (or dom-id (random-uuid))
       :tabIndex 0
       :class (str class " " (when-not text-ref-type? "jtrigger"))
+      :on-key-down (fn [e]
+                     (when-not text-ref-type?
+                       (when (contains? #{"Backspace" "Delete"} (util/ekey e))
+                         (delete-block-property! block property))))
       :style {:min-height 24}}
      (cond
        (and (= :logseq.property/default-value (:db/ident property)) (nil? (:block/title value)))
@@ -1189,7 +1222,9 @@
                              :on-checked-change add-property!
                              :on-key-down (fn [e]
                                             (when (= (util/ekey e) "Enter")
-                                              (add-property!)))})])
+                                              (add-property!))
+                                            (when (contains? #{"Backspace" "Delete"} (util/ekey e))
+                                              (delete-block-property! block property)))})])
           ;; :others
           [:div.flex.flex-1
            (property-value-inner block property value opts)])))))
@@ -1243,6 +1278,8 @@
                            (" " "Enter")
                            (do (some-> (rum/deref *el) (.click))
                                (util/stop e))
+                           ("Backspace" "Delete")
+                           (delete-block-property! block property)
                            :dune))
           :class "flex flex-1 flex-row items-center flex-wrap gap-1"}
          (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]

+ 82 - 38
src/main/frontend/handler/editor.cljs

@@ -1262,7 +1262,9 @@
     ;; when selection and one block selected, select next block
     (and (state/selection?) (== 1 (count (state/get-selection-blocks))))
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
-          element (f (first (state/get-selection-blocks)))]
+          element (f (first (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
       (when element
         (util/scroll-to-block element)
         (state/conj-selection-block! element direction)))
@@ -1271,7 +1273,9 @@
     (and (state/selection?) (= direction (state/get-selection-direction)))
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
           first-last (if (= :up direction) first last)
-          element (f (first-last (state/get-selection-blocks)) {:up-down? true})]
+          element (f (first-last (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
       (when element
         (util/scroll-to-block element)
         (state/conj-selection-block! element direction)))
@@ -1280,7 +1284,9 @@
     (state/selection?)
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
           last-first (if (= :up direction) last first)
-          element (f (last-first (state/get-selection-blocks)) {:up-down? true})]
+          element (f (last-first (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
       (when element
         (util/scroll-to-block element)
         (state/drop-last-selection-block!))))
@@ -2674,43 +2680,65 @@
         f (case direction
             :up util/get-prev-block-non-collapsed
             :down util/get-next-block-non-collapsed)
-        sibling-block (f selected {:up-down? true})]
+        sibling-block (f selected {:up-down? true
+                                   :exclude-property? true})]
     (when (and sibling-block (dom/attr sibling-block "blockid"))
       (util/scroll-to-block sibling-block)
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
 
+(defn- active-jtrigger?
+  []
+  (some-> js/document.activeElement (dom/has-class? "jtrigger")))
+
+(defn- property-value-node?
+  [node]
+  (some-> node (dom/has-class? "property-value-container")))
+
 (defn move-cross-boundary-up-down
   [direction move-opts]
-  (when-let [input (or (:input move-opts) (state/get-input))]
-    (let [repo (state/get-current-repo)
-          f (case direction
-              :up util/get-prev-block-non-collapsed
-              :down util/get-next-block-non-collapsed)
-          current-block (util/rec-get-node input "ls-block")
-          sibling-block (f current-block {:up-down? true})
-          {:block/keys [uuid title format]} (state/get-edit-block)
-          format (or format :markdown)]
-      (if sibling-block
-        (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
-          (let [container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
+  (let [input (or (:input move-opts) (state/get-input))
+        active-element js/document.activeElement
+        input-or-active-element (or input active-element)]
+    (when input-or-active-element
+      (let [repo (state/get-current-repo)
+            f (case direction
+                :up util/get-prev-block-non-collapsed
+                :down util/get-next-block-non-collapsed)
+            current-block (util/rec-get-node input-or-active-element "ls-block")
+            sibling-block (f current-block {:up-down? true})
+            {:block/keys [uuid title format]} (state/get-edit-block)
+            format (or format :markdown)
+            sibling-block (or (when (property-value-node? sibling-block)
+                                (first (dom/by-class sibling-block "ls-block")))
+                              sibling-block)
+            property-value-container? (property-value-node? sibling-block)]
+        (if sibling-block
+          (let [sibling-block-id (dom/attr sibling-block "blockid")
+                container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
                 value (state/get-edit-content)]
             (p/do!
              (when (and
+                    uuid
                     (not (state/block-component-editing?))
                     (not= (clean-content! repo format title)
                           (string/trim value)))
                (save-block! repo uuid value))
 
-             (let [new-uuid (cljs.core/uuid sibling-block-id)
-                   block (db/entity [:block/uuid new-uuid])]
-               (edit-block! block
-                            (or (:pos move-opts)
-                                [direction (util/get-line-pos (.-value input) (util/get-selection-start input))])
-                            {:container-id container-id
-                             :direction direction})))))
-        (case direction
-          :up (cursor/move-cursor-to input 0)
-          :down (cursor/move-cursor-to-end input))))))
+             (if property-value-container?
+               (when-let [trigger (first (dom/by-class sibling-block "jtrigger"))]
+                 (state/clear-edit!)
+                 (.focus trigger))
+               (let [new-uuid (cljs.core/uuid sibling-block-id)
+                     block (db/entity [:block/uuid new-uuid])]
+                 (edit-block! block
+                              (or (:pos move-opts)
+                                  (when input [direction (util/get-line-pos (.-value input) (util/get-selection-start input))])
+                                  0)
+                              {:container-id container-id
+                               :direction direction})))))
+          (case direction
+            :up (cursor/move-cursor-to input 0)
+            :down (cursor/move-cursor-to-end input)))))))
 
 (defn keydown-up-down-handler
   [direction {:keys [_pos] :as move-opts}]
@@ -2720,13 +2748,17 @@
         up? (= direction :up)
         down? (= direction :down)]
     (cond
+      (active-jtrigger?)
+      (move-cross-boundary-up-down direction move-opts)
+
       (not= selected-start selected-end)
       (if up?
         (cursor/move-cursor-to input selected-start)
         (cursor/move-cursor-to input selected-end))
 
-      (or (and up? (cursor/textarea-cursor-first-row? input))
-          (and down? (cursor/textarea-cursor-last-row? input)))
+      (and input
+           (or (and up? (cursor/textarea-cursor-first-row? input))
+               (and down? (cursor/textarea-cursor-last-row? input))))
       (move-cross-boundary-up-down direction move-opts)
 
       :else
@@ -2743,16 +2775,27 @@
         repo (state/get-current-repo)
         editing-block (gdom/getElement (state/get-editing-block-dom-id))
         f (if up? util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
-        sibling-block (f editing-block)]
+        sibling-block (f editing-block)
+        sibling-block (or (when (and sibling-block (property-value-node? sibling-block))
+                            (if (and up? editing-block (gdom/contains sibling-block editing-block))
+                              (f sibling-block)
+                              (first (dom/by-class sibling-block "ls-block"))))
+                          sibling-block)]
     (when sibling-block
-      (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
-        (let [content (:block/title block)
-              value (state/get-edit-content)]
-          (when (and value (not= (clean-content! repo format content) (string/trim value)))
-            (save-block! repo uuid value)))
-        (let [container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
-              block (db/entity repo [:block/uuid (cljs.core/uuid sibling-block-id)])]
-          (edit-block! block pos {:container-id container-id}))))))
+      (if-let [sibling-block-id (dom/attr sibling-block "blockid")]
+        (do
+          (let [content (:block/title block)
+                value (state/get-edit-content)]
+            (when (and value (not= (clean-content! repo format content) (string/trim value)))
+              (save-block! repo uuid value)))
+
+          (let [container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
+                block (db/entity repo [:block/uuid (cljs.core/uuid sibling-block-id)])]
+            (edit-block! block pos {:container-id container-id})))
+        (when (property-value-node? sibling-block)
+          (when-let [trigger (first (dom/by-class sibling-block "jtrigger"))]
+            (state/clear-edit!)
+            (.focus trigger)))))))
 
 (defn keydown-arrow-handler
   [direction]
@@ -3402,8 +3445,9 @@
                (not (slide-focused?))
                (not (state/get-timestamp-block)))
       (util/stop e)
+
       (cond
-        (state/editing?)
+        (or (state/editing?) (active-jtrigger?))
         (keydown-up-down-handler direction {})
 
         (state/selection?)

+ 14 - 8
src/main/frontend/util.cljc

@@ -878,13 +878,16 @@
      "Gets previous non-collapsed block. If given a container
       looks up blocks in that container e.g. for embed"
      ([block] (get-prev-block-non-collapsed block {}))
-     ([block {:keys [container up-down?]}]
+     ([block {:keys [container up-down? exclude-property?]}]
       (when-let [blocks (if container
                           (get-blocks-noncollapse container)
                           (get-blocks-noncollapse))]
-        (let [blocks (if up-down?
-                       (skip-same-top-blocks blocks block)
-                       blocks)]
+        (let [blocks (cond->>
+                      (if up-down?
+                        (skip-same-top-blocks blocks block)
+                        blocks)
+                       exclude-property?
+                       (remove (fn [node] (d/has-class? node "property-value-container"))))]
           (when-let [index (.indexOf blocks block)]
             (let [idx (dec index)]
               (when (>= idx 0)
@@ -902,11 +905,14 @@
 
 #?(:cljs
    (defn get-next-block-non-collapsed
-     [block {:keys [up-down?]}]
+     [block {:keys [up-down? exclude-property?]}]
      (when-let [blocks (and block (get-blocks-noncollapse))]
-       (let [blocks (if up-down?
-                      (skip-same-top-blocks blocks block)
-                      blocks)]
+       (let [blocks (cond->>
+                     (if up-down?
+                       (skip-same-top-blocks blocks block)
+                       blocks)
+                      exclude-property?
+                      (remove (fn [node] (d/has-class? node "property-value-container"))))]
          (when-let [index (.indexOf blocks block)]
            (let [idx (inc index)]
              (when (>= (count blocks) idx)