Browse Source

enhance(ui): WIP enhance the multi select component for shui

charlie 1 year ago
parent
commit
44a4ecdcd0

+ 35 - 10
deps/shui/src/logseq/shui/demo2.cljs

@@ -15,11 +15,13 @@
 
    (let [items [{:key 1 :value "Apple" :class "bg-gray-800 text-gray-50"}
                 {:key 2 :value "Orange" :class "bg-orange-700 text-gray-50"}
-                nil
-                {:key 3 :value "Pear"}]
+                {:key 3 :value "Pear"}
+                {:key 4 :value "Banana" :class "bg-yellow-700 text-gray-700"}]
 
          [selected-items set-selected-items!]
-         (rum/use-state [(last items)])
+         (rum/use-state [(second items)])
+
+         [search? set-search?] (rum/use-state false)
 
          rm-item! (fn [item] (set-selected-items! (remove #(= item %) selected-items)))
          add-item! (fn [item] (set-selected-items! (conj selected-items item)))
@@ -33,7 +35,10 @@
          (ui/card-title "Basic")
          (ui/card-description "x multiselect for shui"))
        (ui/card-content
-
+         [:label.block.flex.items-center.pb-3.cursor-pointer
+          (ui/checkbox {:checked search?
+                        :on-click #(set-search? (not search?))})
+          [:small.pl-2 "Enable basic search input"]]
          ;; Basic
          (ui/dropdown-menu
            {:open open?}
@@ -47,6 +52,12 @@
            ;; content
            (x-select items selected-items
              {:close! #(set-open! false)
+              :search-enabled? search?
+              :search-key-render (fn [q {:keys [items]}]
+                                   (when (and (not (string/blank? q))
+                                           (not (seq items)))
+                                     [:b.flex.items-center.justify-center.py-4.gap-2.font-normal.opacity-80
+                                      (ui/tabler-icon "lemon") [:small "No fruits!"]]))
               :on-chosen on-chosen
               :value-render (fn [v {:keys [selected?]}]
                               (if selected?
@@ -59,13 +70,16 @@
 
    [:hr]
 
-   (let [items [{:key 1 :value "Apple" :class "bg-gray-800 text-gray-50"}
-                {:key 2 :value "Orange" :class "bg-orange-700 text-gray-50"}
-                nil
-                {:key 3 :value "Pear"}]
+   (let [[items set-items!]
+         (rum/use-state
+           [{:key 1 :value "Apple" :class "bg-gray-800 text-gray-50"}
+            {:key 2 :value "Orange" :class "bg-orange-700 text-gray-50"}
+            nil
+            {:key 3 :value "Pear"}
+            {:key 4 :value "Banana" :class "bg-yellow-700 text-gray-700"}])
 
          [selected-items set-selected-items!]
-         (rum/use-state [(last items)])
+         (rum/use-state [(last items) (first items)])
 
          rm-item! (fn [item] (set-selected-items! (remove #(= item %) selected-items)))
          add-item! (fn [item] (set-selected-items! (conj selected-items item)))
@@ -76,7 +90,7 @@
 
      (ui/card
        (ui/card-header
-         (ui/card-title "Search")
+         (ui/card-title "Search & Custom")
          (ui/card-description "x multiselect for shui"))
        (ui/card-content
 
@@ -107,6 +121,17 @@
                                  (:value item))
                                (ui/dropdown-menu-separator)))
 
+              :search-key-render
+              (fn [k {:keys [items x-item exist-fn]}]
+                (when (and
+                        (not (string/blank? k))
+                        (not (exist-fn)))
+                  (x-item
+                    {:on-click (fn []
+                                 (ui/toast! (str "Create: " k) :warning)
+                                 (set-open! false))}
+                    (str "+ create: " k))))
+
               ;:head-render (fn [] [:b "header"])
               ;:foot-render (fn [] [:b "footer"])
               :content-props

+ 89 - 61
deps/shui/src/logseq/shui/select/multi.cljs

@@ -5,10 +5,11 @@
             [logseq.shui.form.core :as form]))
 
 (defn- get-k [item]
-  (when (map? item)
+  (if (map? item)
     (some->> ((juxt :id :key :label) item)
       (remove nil?)
-      (first))))
+      (first))
+    item))
 
 (defn- get-v
   [item]
@@ -16,16 +17,17 @@
     item (or (:title item) (:value item))))
 
 (rum/defc search-input
-  [input-props]
+  [input-props & {:keys [on-enter valid-search-key?]}]
   (let [*el (rum/use-ref nil)
         [down set-down!] (rum/use-state 0)]
 
     (rum/use-effect!
       (fn []
-        (when (> down 0)
-          (some-> (rum/deref *el)
-            (.closest ".head")
-            (.-nextSibling)
+        (when-let [^js item (and (> down 0)
+                              (some-> (rum/deref *el)
+                                (.closest ".head")
+                                (.-nextSibling)))]
+          (some-> (if valid-search-key? (.-nextSibling item) item)
             (.focus))))
       [down])
 
@@ -37,6 +39,7 @@
                :on-key-up #(case (.-key %)
                              "ArrowDown" (set-down! (inc down))
                              "ArrowUp" nil
+                             "Enter" (when (fn? on-enter) (on-enter))
                              :dune)
                :auto-focus true}
          input-props))]))
@@ -53,11 +56,15 @@
 (rum/defc x-select
   [items selected-items & {:keys [on-chosen item-render value-render
                                   head-render foot-render open? close!
-                                  search-enabled? search-key search-fn
+                                  search-enabled? search-key
+                                  search-fn search-key-render
                                   item-props content-props]}]
   (let [x-content popup/dropdown-menu-content
         x-item popup/dropdown-menu-item
+        *head-ref (rum/use-ref nil)
         [search-key1 set-search-key!] (rum/use-state search-key)
+        search-key1' (some-> search-key1 (string/trim) (string/lower-case))
+        valid-search-key? (and search-enabled? (not (string/blank? search-key1')))
         items (if search-enabled?
                 (if (fn? search-fn)
                   (search-fn items search-key1)
@@ -71,56 +78,77 @@
           (js/setTimeout #(set-search-key! "") 500)))
       [open?])
 
-    [:div.flex.flex-1.flex-col
-     (x-content
-       (merge
-         {:onCloseAutoFocus false
-          :onInteractOutside close1!
-          :onEscapeKeyDown close1!}
-         content-props)
-       ;; header
-       (when (or search-enabled? (fn? head-render))
-         [:div.head
-          (when search-enabled?
-            (search-input
-              {:value search-key1
-               :on-key-down (fn [^js e]
-                              (.stopPropagation e)
-                              (case (.-key e)
-                                "Escape" (if (string/blank? search-key1)
-                                           (some-> (.-target e) (.blur))
-                                           (set-search-key! ""))
-                                :dune))
-               :on-change #(set-search-key! (.-value (.-target %)))}))
-          (when head-render (head-render))])
-       ;; items
-       (for [item items
-             :let [selected? (some #(let [k (get-k item)
-                                          k' (get-k %)]
-                                      (or (= item %)
-                                        (and (not (nil? k))
-                                          (not (nil? k'))
-                                          (= k k'))))
-                               selected-items)]]
-         (if (fn? item-render)
-           (item-render item {:x-item x-item :selected? selected?})
-           (let [k (get-k item)
-                 v (get-v item)]
-             (when k
-               (let [opts {:selected? selected?}
-                     on-click' (:on-click item-props)
-                     on-click (fn [e]
-                                ;; TODO: return value
-                                (when (fn? on-click') (on-click' e))
-                                (when (fn? on-chosen)
-                                  (on-chosen item opts)))]
-                 (x-item (merge {:data-k k :on-click on-click} item-props)
-                   [:span.flex.items-center.gap-2.w-full
-                    (form/checkbox {:checked selected?})
-                    (let [v' (if (fn? v) (v item opts) v)]
-                      (if (fn? value-render)
-                        (value-render v' (assoc opts :item item)) v'))]))))))
-       ;; footer
-       (when (fn? foot-render)
-         [:div.foot
-          (foot-render)]))]))
+    (x-content
+      (merge
+        {:onCloseAutoFocus false
+         :onInteractOutside close1!
+         :onEscapeKeyDown close1!
+         :class (str (:class content-props)
+                  (when valid-search-key? " has-search-key"))}
+        (dissoc content-props :class))
+      ;; header
+      (when (or search-enabled? (fn? head-render))
+        [:div.head
+         {:ref *head-ref}
+         (when search-enabled?
+           (search-input
+             {:value search-key1
+              :on-key-down (fn [^js e]
+                             (.stopPropagation e)
+                             (case (.-key e)
+                               "Escape" (if (string/blank? search-key1)
+                                          (some-> (.-target e) (.blur))
+                                          (set-search-key! ""))
+                               :dune))
+              :on-change #(set-search-key! (.-value (.-target %)))}
+
+             {:on-enter (fn []
+                          (when-let [head-el (and (not (string/blank? search-key1'))
+                                               (rum/deref *head-ref))]
+                            (when-let [^js item (.-nextSibling head-el)]
+                              (when (contains? #{"menuitemcheckbox" "menuitem"}
+                                      (.getAttribute item "role"))
+                                (.click item)))))
+              :valid-search-key? valid-search-key?}))
+         (when head-render (head-render))])
+      ;; items
+      (for [item items
+            :let [selected? (some #(let [k (get-k item)
+                                         k' (get-k %)]
+                                     (or (= item %)
+                                       (and (not (nil? k))
+                                         (not (nil? k'))
+                                         (= k k'))))
+                              selected-items)]]
+        (if (fn? item-render)
+          (item-render item {:x-item x-item :selected? selected?})
+          (let [k (get-k item)
+                v (get-v item)]
+            (when k
+              (let [opts {:selected? selected?}
+                    on-click' (:on-click item-props)
+                    on-click (fn [e]
+                               ;; TODO: return value
+                               (when (fn? on-click') (on-click' e))
+                               (when (fn? on-chosen)
+                                 (on-chosen item opts)))]
+                (x-item (merge {:data-k k :on-click on-click} item-props)
+                  [:span.flex.items-center.gap-2.w-full
+                   (form/checkbox {:checked selected?})
+                   (let [v' (if (fn? v) (v item opts) v)]
+                     (if (fn? value-render)
+                       (value-render v' (assoc opts :item item)) v'))]))))))
+
+      (when (and search-enabled?
+              (fn? search-key-render))
+        (let [exist-fn (fn []
+                         (and (not (string/blank? search-key1))
+                           (seq items)
+                           (contains? (into #{} (map #(some-> (get-v %) (string/lower-case)) items))
+                             (string/lower-case search-key1))))]
+          (search-key-render search-key1
+            {:items items :x-item x-item :exist-fn exist-fn})))
+      ;; footer
+      (when (fn? foot-render)
+        [:div.foot
+         (foot-render)]))))

+ 8 - 2
resources/css/shui.css

@@ -43,8 +43,14 @@ html {
 
   &:not([data-color=logseq]) {
     .ui__dropdown-menu-item,
-    div[data-radix-popper-content-wrapper] div[role=menuitem] {
-      &:focus, &:hover {
+    div[data-radix-popper-content-wrapper] div[data-radix-collection-item] {
+      &:focus, &:hover, &.is-active {
+        background-color: var(--lx-gray-04, hsl(var(--accent)));
+      }
+    }
+
+    .ui__dropdown-menu-content.has-search-key {
+      > .head:focus-within + div[data-radix-collection-item] {
         background-color: var(--lx-gray-04, hsl(var(--accent)));
       }
     }