|
3 | 3 | [clojure.string :as string] |
4 | 4 | [frontend.components.block :as block] |
5 | 5 | [frontend.components.cmdk.list-item :as list-item] |
| 6 | + [frontend.components.cmdk.scroll :as scroll] |
6 | 7 | [frontend.components.cmdk.state :as cmdk-state] |
7 | 8 | [frontend.components.icon :as icon-component] |
8 | 9 | [frontend.config :as config] |
|
622 | 623 | (when-let [action (state->action state)] |
623 | 624 | (handle-action action state event))) |
624 | 625 |
|
625 | | -(defn- scroll-into-view-when-invisible |
| 626 | +(defn- ensure-focus-visible! |
626 | 627 | [state target] |
627 | | - (let [*container-ref (::scroll-container-ref state) |
628 | | - container-rect (.getBoundingClientRect @*container-ref) |
629 | | - t1 (.-top container-rect) |
630 | | - b1 (.-bottom container-rect) |
631 | | - target-rect (.getBoundingClientRect target) |
632 | | - t2 (.-top target-rect) |
633 | | - b2 (.-bottom target-rect)] |
634 | | - (when-not (<= t1 t2 b2 b1) ; not visible |
635 | | - (.scrollIntoView target |
636 | | - #js {:inline "nearest" |
637 | | - :behavior "smooth"})))) |
| 628 | + (let [container @(::scroll-container-ref state) |
| 629 | + rect (scroll/focus-row-visible-rect container target)] |
| 630 | + (when rect |
| 631 | + (let [next-scroll-top (scroll/ensure-focus-visible-scroll-top rect)] |
| 632 | + (when (not= next-scroll-top (.-scrollTop container)) |
| 633 | + (set! (.-scrollTop container) next-scroll-top)))))) |
| 634 | + |
| 635 | +(defn- apply-anchored-wheel-scroll! |
| 636 | + [state e] |
| 637 | + (when (and @(::wheel-focus-anchor? state) |
| 638 | + (= :keyboard @(::focus-source state))) |
| 639 | + (let [container @(::scroll-container-ref state) |
| 640 | + target @(::highlighted-row-el state) |
| 641 | + rect (scroll/focus-row-visible-rect container target) |
| 642 | + delta-y (some-> e .-deltaY)] |
| 643 | + (when (and rect (number? delta-y) (not (zero? delta-y))) |
| 644 | + (let [next-scroll-top (scroll/anchored-scroll-top (assoc rect :delta-y delta-y))] |
| 645 | + (.preventDefault e) |
| 646 | + (when (not= next-scroll-top (.-scrollTop container)) |
| 647 | + (set! (.-scrollTop container) next-scroll-top))))))) |
| 648 | + |
| 649 | +(defn- handle-results-wheel! |
| 650 | + [state e] |
| 651 | + ;; Wheel input means user wants free scrolling, so exit keyboard-anchored mode first. |
| 652 | + (when (= :keyboard @(::focus-source state)) |
| 653 | + (reset! (::focus-source state) :mouse) |
| 654 | + (reset! (::highlighted-row-el state) nil)) |
| 655 | + (apply-anchored-wheel-scroll! state e)) |
638 | 656 |
|
639 | 657 | (rum/defcs result-group |
640 | 658 | < rum/reactive |
641 | 659 | [state' state title group visible-items first-item sidebar?] |
642 | 660 | (let [{:keys [show items]} (some-> state ::results deref group) |
643 | | - highlighted-item (or @(::highlighted-item state) first-item) |
| 661 | + focus-source @(::focus-source state) |
| 662 | + highlighted-item (or @(::highlighted-item state) |
| 663 | + (when (= :keyboard focus-source) first-item)) |
| 664 | + disable-lazy? @(::disable-lazy? state) |
644 | 665 | *mouse-active? (::mouse-active? state) |
645 | 666 | filter' @(::filter state) |
646 | 667 | can-show-less? (< (get-group-limit group) (count visible-items)) |
|
650 | 671 | [:div {:class (if (= title "Create") |
651 | 672 | "border-b border-gray-06 last:border-b-0" |
652 | 673 | "border-b border-gray-06 pb-1 last:border-b-0") |
653 | | - :on-mouse-move #(reset! *mouse-active? true) |
654 | | - :on-mouse-enter #(reset! *mouse-active? true)} |
| 674 | + :on-mouse-move (fn [e] |
| 675 | + (let [dx (or (.-movementX e) 0) |
| 676 | + dy (or (.-movementY e) 0) |
| 677 | + real-pointer-move? (or (not (zero? dx)) |
| 678 | + (not (zero? dy)))] |
| 679 | + (when real-pointer-move? |
| 680 | + (when-not @*mouse-active? |
| 681 | + (reset! *mouse-active? true)))))} |
655 | 682 | (when-not (= title "Create") |
656 | 683 | [:div {:class "text-xs py-1.5 px-3 flex justify-between items-center gap-2 text-gray-11 bg-gray-02 h-8"} |
657 | 684 | [:div {:class "font-bold text-gray-11 pl-0.5 cursor-pointer select-none" |
|
664 | 691 | [:div {:class "pl-1.5 text-gray-12 rounded-full" |
665 | 692 | :style {:font-size "0.7rem"}} |
666 | 693 | (if (<= 100 (count items)) |
667 | | - (str "99+") |
| 694 | + "99+" |
668 | 695 | (count items))]) |
669 | 696 |
|
670 | 697 | [:div {:class "flex-1"}] |
|
673 | 700 | (empty? filter') |
674 | 701 | (not sidebar?)) |
675 | 702 | [:a.text-link.select-node.opacity-50.hover:opacity-90 |
676 | | - {:on-click (if (= show :more) show-less show-more)} |
| 703 | + {:on-click (fn [e] |
| 704 | + (util/stop e) |
| 705 | + (reset! (::focus-source state) :mouse) |
| 706 | + (when-let [input-el @(::input-ref state)] |
| 707 | + (.focus input-el)) |
| 708 | + ((if (= show :more) show-less show-more)))} |
677 | 709 | (if (= show :more) |
678 | 710 | [:div.flex.flex-row.gap-1.items-center |
679 | 711 | "Show less" |
|
709 | 741 | (handle-action :default state item) |
710 | 742 | (when-let [on-click (:on-click item)] |
711 | 743 | (on-click e))) |
712 | | - :on-mouse-enter |
713 | | - (fn [_e] |
714 | | - (when (not= item @(::highlighted-item state)) |
715 | | - (reset! (::highlighted-item state) item))) |
716 | | - :on-highlight (fn [ref] |
717 | | - (reset! (::highlighted-group state) group) |
718 | | - (when (and ref (.-current ref) |
719 | | - (not (:mouse-enter-triggered-highlight @(::highlighted-item state)))) |
720 | | - (scroll-into-view-when-invisible state (.-current ref))))) |
721 | | - nil)] |
722 | | - (if (= group :nodes) |
| 744 | + :on-mouse-enter |
| 745 | + (fn [_e] |
| 746 | + (when (and @*mouse-active? |
| 747 | + (= :mouse @(::focus-source state))) |
| 748 | + (when (not= item @(::highlighted-item state)) |
| 749 | + (reset! (::highlighted-item state) item)))) |
| 750 | + :component-opts |
| 751 | + {:on-mouse-move |
| 752 | + (fn [e] |
| 753 | + (let [dx (or (.-movementX e) 0) |
| 754 | + dy (or (.-movementY e) 0) |
| 755 | + real-pointer-move? (or (not (zero? dx)) |
| 756 | + (not (zero? dy)))] |
| 757 | + (when real-pointer-move? |
| 758 | + (when-not @*mouse-active? |
| 759 | + (reset! *mouse-active? true)) |
| 760 | + (when-not (= :mouse @(::focus-source state)) |
| 761 | + (reset! (::focus-source state) :mouse)) |
| 762 | + (when (not= item @(::highlighted-item state)) |
| 763 | + (reset! (::highlighted-item state) item)))))} |
| 764 | + :on-highlight (fn [ref] |
| 765 | + (reset! (::highlighted-group state) group) |
| 766 | + (when (and ref (.-current ref)) |
| 767 | + (let [row-el (.-current ref)] |
| 768 | + (reset! (::highlighted-row-el state) row-el) |
| 769 | + (when (= :keyboard @(::focus-source state)) |
| 770 | + (ensure-focus-visible! state row-el)))))) |
| 771 | + nil)] |
| 772 | + (if (and (= group :nodes) (not disable-lazy?)) |
723 | 773 | (ui/lazy-visible (fn [] item) {:trigger-once? true}) |
724 | 774 | item)))]])) |
725 | 775 |
|
726 | 776 | (defn move-highlight [state n] |
727 | 777 | (let [items (mapcat last (state->results-ordered state (:search/mode @state/state))) |
| 778 | + focus-source @(::focus-source state) |
728 | 779 | highlighted-item (some-> state ::highlighted-item deref (dissoc :mouse-enter-triggered-highlight)) |
729 | | - current-item-index (some->> highlighted-item (.indexOf items)) |
730 | | - next-item-index (some-> (or current-item-index 0) (+ n) (mod (count items)))] |
731 | | - (if-let [next-highlighted-item (nth items next-item-index nil)] |
732 | | - (reset! (::highlighted-item state) next-highlighted-item) |
733 | | - (reset! (::highlighted-item state) nil)))) |
| 780 | + fallback-highlighted? (and (nil? highlighted-item) |
| 781 | + (= :keyboard focus-source) |
| 782 | + (seq items)) |
| 783 | + current-item-index (cond |
| 784 | + highlighted-item (.indexOf items highlighted-item) |
| 785 | + fallback-highlighted? 0 |
| 786 | + :else nil) |
| 787 | + items-count (count items)] |
| 788 | + (if (pos? items-count) |
| 789 | + (let [base-index (if (some? current-item-index) |
| 790 | + current-item-index |
| 791 | + (if (pos? n) -1 0)) |
| 792 | + next-item-index (mod (+ base-index n) items-count) |
| 793 | + next-highlighted-item (nth items next-item-index nil)] |
| 794 | + (if next-highlighted-item |
| 795 | + (reset! (::highlighted-item state) next-highlighted-item) |
| 796 | + (do |
| 797 | + (reset! (::highlighted-item state) nil) |
| 798 | + (reset! (::highlighted-row-el state) nil)))) |
| 799 | + (do |
| 800 | + (reset! (::highlighted-item state) nil) |
| 801 | + (reset! (::highlighted-row-el state) nil))))) |
734 | 802 |
|
735 | 803 | (defn handle-input-change |
736 | 804 | ([state e] (handle-input-change state e (.. e -target -value))) |
|
808 | 876 | (shui/dialog-close! :ls-dialog-cmdk) |
809 | 877 | (state/sidebar-add-block! repo input :search)) |
810 | 878 | as-keydown? (if meta? |
811 | | - (show-more) |
812 | 879 | (do |
| 880 | + (reset! (::disable-lazy? state) true) |
| 881 | + (show-more)) |
| 882 | + (do |
| 883 | + (reset! (::disable-lazy? state) true) |
813 | 884 | (reset! (::mouse-active? state) false) |
| 885 | + (reset! (::focus-source state) :keyboard) |
| 886 | + (reset! (::highlighted-row-el state) nil) |
814 | 887 | (move-highlight state 1))) |
815 | 888 | as-keyup? (if meta? |
816 | | - (show-less) |
817 | 889 | (do |
| 890 | + (reset! (::disable-lazy? state) true) |
| 891 | + (show-less)) |
| 892 | + (do |
| 893 | + (reset! (::disable-lazy? state) true) |
818 | 894 | (reset! (::mouse-active? state) false) |
| 895 | + (reset! (::focus-source state) :keyboard) |
| 896 | + (reset! (::highlighted-row-el state) nil) |
819 | 897 | (move-highlight state -1))) |
820 | 898 | (and enter? (not composing?)) (do |
821 | 899 | (handle-action :default state e) |
|
888 | 966 | ;; This was moved to a functional component |
889 | 967 | (hooks/use-effect! (fn [] |
890 | 968 | (when (and highlighted-item (= -1 (.indexOf all-items (dissoc highlighted-item :mouse-enter-triggered-highlight)))) |
891 | | - (reset! (::highlighted-item state) nil))) |
| 969 | + (reset! (::highlighted-item state) nil) |
| 970 | + (reset! (::highlighted-row-el state) nil))) |
892 | 971 | [all-items]) |
893 | 972 | (hooks/use-effect! |
894 | 973 | (fn [] |
|
1072 | 1151 | (rum/local false ::meta?) |
1073 | 1152 | (rum/local nil ::highlighted-group) |
1074 | 1153 | (rum/local nil ::highlighted-item) |
| 1154 | + (rum/local nil ::highlighted-row-el) |
| 1155 | + (rum/local false ::disable-lazy?) |
| 1156 | + (rum/local :keyboard ::focus-source) |
1075 | 1157 | (rum/local false ::mouse-active?) |
| 1158 | + (rum/local true ::wheel-focus-anchor?) |
1076 | 1159 | (rum/local default-results ::results) |
1077 | 1160 | (rum/local nil ::scroll-container-ref) |
1078 | 1161 | (rum/local nil ::input-ref) |
|
1090 | 1173 | :class (cond-> "w-full h-full relative flex flex-col justify-start" |
1091 | 1174 | (not sidebar?) (str " rounded-lg"))} |
1092 | 1175 | (input-row state all-items opts) |
1093 | | - [:div {:class (cond-> "w-full flex-1 overflow-y-auto min-h-[65dvh] max-h-[65dvh]" |
1094 | | - (not sidebar?) (str " pb-14")) |
1095 | | - :ref #(let [*ref (::scroll-container-ref state)] |
1096 | | - (when-not @*ref (reset! *ref %))) |
1097 | | - :on-mouse-enter #(reset! (::mouse-active? state) true) |
1098 | | - :on-mouse-leave #(reset! (::mouse-active? state) false) |
1099 | | - :style {:background "var(--lx-gray-02)" |
1100 | | - :scroll-padding-block 32}} |
1101 | | - |
1102 | | - (when group-filter |
1103 | | - [:div.flex.flex-col.px-3.py-1.opacity-70.text-sm |
1104 | | - (search-only state (string/capitalize (name group-filter)))]) |
1105 | | - |
1106 | | - (let [items (filter |
1107 | | - (fn [[_group-name group-key group-count _group-items]] |
1108 | | - (and (not= 0 group-count) |
1109 | | - (if-not group-filter true |
1110 | | - (or (= group-filter group-key) |
1111 | | - (and (= group-filter :nodes) |
1112 | | - (= group-key :current-page)) |
1113 | | - (and (contains? #{:create} group-filter) |
1114 | | - (= group-key :create)))))) |
1115 | | - results-ordered)] |
1116 | | - (if (seq items) |
1117 | | - (for [[group-name group-key _group-count group-items] items] |
1118 | | - (let [title (string/capitalize group-name)] |
1119 | | - (result-group state title group-key group-items first-item sidebar?))) |
1120 | | - [:div.flex.flex-col.p-4.opacity-50 |
1121 | | - (when-not (string/blank? @*input) |
1122 | | - "No matched results")]))] |
| 1176 | + [:div {:class (cond-> "w-full flex-1 overflow-y-auto min-h-[65dvh] max-h-[65dvh]" |
| 1177 | + (not sidebar?) (str " pb-14")) |
| 1178 | + :ref #(let [*ref (::scroll-container-ref state)] |
| 1179 | + (when-not @*ref (reset! *ref %))) |
| 1180 | + :on-mouse-leave #(reset! (::mouse-active? state) false) |
| 1181 | + :on-wheel #(handle-results-wheel! state %) |
| 1182 | + :style {:background "var(--lx-gray-02)" |
| 1183 | + :scroll-padding-block 32}} |
| 1184 | + |
| 1185 | + (when group-filter |
| 1186 | + [:div.flex.flex-col.px-3.py-1.opacity-70.text-sm |
| 1187 | + (search-only state (string/capitalize (name group-filter)))]) |
| 1188 | + |
| 1189 | + (let [items (filter |
| 1190 | + (fn [[_group-name group-key group-count _group-items]] |
| 1191 | + (and (not= 0 group-count) |
| 1192 | + (if-not group-filter true |
| 1193 | + (or (= group-filter group-key) |
| 1194 | + (and (= group-filter :nodes) |
| 1195 | + (= group-key :current-page)) |
| 1196 | + (and (contains? #{:create} group-filter) |
| 1197 | + (= group-key :create)))))) |
| 1198 | + results-ordered)] |
| 1199 | + (if (seq items) |
| 1200 | + (for [[group-name group-key _group-count group-items] items] |
| 1201 | + (let [title (string/capitalize group-name)] |
| 1202 | + (result-group state title group-key group-items first-item sidebar?))) |
| 1203 | + [:div.flex.flex-col.p-4.opacity-50 |
| 1204 | + (when-not (string/blank? @*input) |
| 1205 | + "No matched results")]))] |
1123 | 1206 | (when-not sidebar? (hints state))])) |
1124 | 1207 |
|
1125 | 1208 | (rum/defc cmdk-modal [props] |
|
0 commit comments