cmdk.cljs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. (ns frontend.components.cmdk
  2. (:require
  3. [clojure.string :as string]
  4. [frontend.components.block :as block]
  5. [frontend.components.command-palette :as cp]
  6. [frontend.context.i18n :refer [t]]
  7. [frontend.db :as db]
  8. [frontend.db.model :as model]
  9. [frontend.handler.command-palette :as cp-handler]
  10. [frontend.handler.editor :as editor-handler]
  11. [frontend.handler.page :as page-handler]
  12. [frontend.handler.route :as route-handler]
  13. [frontend.handler.whiteboard :as whiteboard-handler]
  14. [frontend.modules.shortcut.core :as shortcut]
  15. [frontend.search :as search]
  16. [frontend.shui :refer [make-shui-context]]
  17. [frontend.state :as state]
  18. [frontend.ui :as ui]
  19. [frontend.util :as util]
  20. [frontend.util.page :as page-util]
  21. [goog.functions :as gfun]
  22. [logseq.shui.core :as shui]
  23. [promesa.core :as p]
  24. [rum.core :as rum]
  25. [frontend.mixins :as mixins]
  26. [logseq.graph-parser.util.block-ref :as block-ref]
  27. [logseq.graph-parser.util :as gp-util]))
  28. (def GROUP-LIMIT 5)
  29. (def search-actions
  30. [{:text "Search only pages" :info "Add filter to search" :icon-theme :gray :icon "page" :filter {:group :pages}}
  31. {:text "Search only current page" :info "Add filter to search" :icon-theme :gray :icon "page" :filter {:group :current-page}}
  32. {:text "Search only blocks" :info "Add filter to search" :icon-theme :gray :icon "block" :filter {:group :blocks}}
  33. {:text "Search only commands" :info "Add filter to search" :icon-theme :gray :icon "command" :filter {:group :commands}}
  34. ;; {:text "Search only files" :info "Add filter to search" :icon-theme :gray :icon "file" :filter {:group :files}}
  35. ])
  36. (def filters search-actions)
  37. (def default-commands
  38. [{:text "Open settings" :icon "settings" :icon-theme :gray}
  39. {:text "Open settings" :icon "settings" :icon-theme :gray}
  40. {:text "Open settings" :icon "settings" :icon-theme :gray}])
  41. ;; The results are separated into groups, and loaded/fetched/queried separately
  42. (def default-results
  43. {:recents {:status :success :show :less :items nil}
  44. :commands {:status :success :show :less :items nil}
  45. :favorites {:status :success :show :less :items nil}
  46. :current-page {:status :success :show :less :items nil}
  47. :pages {:status :success :show :less :items nil}
  48. :blocks {:status :success :show :less :items nil}
  49. :files {:status :success :show :less :items nil}
  50. :filters {:status :success :show :less :items nil}})
  51. (defn lower-case-str [x]
  52. (.toLowerCase (str x)))
  53. (defn create-items [q]
  54. (when-not (string/blank? q)
  55. [{:text "Create page" :icon "new-page" :icon-theme :color :info (str "Create page called '" q "'") :source-create :page}
  56. {:text "Create whiteboard" :icon "new-whiteboard" :icon-theme :color :info (str "Create whiteboard called '" q "'") :source-create :whiteboard}]))
  57. ;; Take the results, decide how many items to show, and order the results appropriately
  58. (defn state->results-ordered [state]
  59. (let [results @(::results state)
  60. input @(::input state)
  61. index (volatile! -1)
  62. visible-items (fn [group]
  63. (let [{:keys [items show]} (get results group)]
  64. (case show
  65. :more items
  66. (take 5 items))))
  67. page-exists? (db/entity [:block/name (string/trim input)])
  68. filter-mode? (string/includes? input " /")
  69. order* (if filter-mode?
  70. [["Filters" :filters (visible-items :filters)]
  71. ["Pages" :pages (visible-items :pages)]
  72. (when-not page-exists?
  73. ["Create" :create (create-items input)])]
  74. (->>
  75. [["Commands" :commands (visible-items :commands)]
  76. ["Pages" :pages (visible-items :pages)]
  77. (when-not page-exists?
  78. ["Create" :create (create-items input)])
  79. ["Current page" :current-page (visible-items :current-page)]
  80. ["Whiteboards" :whiteboards (visible-items :whiteboards)]
  81. ["Blocks" :blocks (visible-items :blocks)]
  82. ["Recents" :recents (visible-items :recents)]]
  83. (remove nil?)))
  84. order (remove nil? order*)]
  85. (for [[group-name group-key group-items] order]
  86. [group-name
  87. group-key
  88. (if (= group-key :create)
  89. (count group-items)
  90. (count (get-in results [group-key :items])))
  91. (mapv #(assoc % :item-index (vswap! index inc)) group-items)])))
  92. (defn state->highlighted-item [state]
  93. (or (some-> state ::highlighted-item deref)
  94. (some->> (state->results-ordered state)
  95. (mapcat last)
  96. (first))))
  97. (defn state->action [state]
  98. (let [highlighted-item (state->highlighted-item state)]
  99. (cond (:source-page highlighted-item) :open
  100. (:source-block highlighted-item) :open
  101. (:source-search highlighted-item) :search
  102. (:source-command highlighted-item) :trigger
  103. (:source-create highlighted-item) :create
  104. (:filter highlighted-item) :filter
  105. :else nil)))
  106. ;; Each result gorup has it's own load-results function
  107. (defmulti load-results (fn [group _state] group))
  108. ;; Initially we want to load the recents into the results
  109. (defmethod load-results :initial [_ state]
  110. (let [!results (::results state)
  111. recent-searches (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
  112. recent-pages (mapv (fn [page] {:type :page :data page}) (db/get-key-value :recent/pages))
  113. recent-items (->> (concat recent-searches recent-pages)
  114. (map #(hash-map :icon (if (= :page (:type %)) "page" "history")
  115. :icon-theme :gray
  116. :text (:data %)
  117. :source-recent %
  118. :source-page (when (= :page (:type %)) (:data %))
  119. :source-search (when (= :search (:type %)) (:data %)))))
  120. command-items (->> (cp-handler/top-commands 1000)
  121. (remove (fn [c] (= :window/close (:id c))))
  122. (map #(hash-map :icon "command"
  123. :icon-theme :gray
  124. :text (cp/translate t %)
  125. :shortcut (:shortcut %)
  126. :source-command %)))]
  127. (reset! !results (-> default-results (assoc-in [:recents :items] recent-items)
  128. (assoc-in [:commands :items] command-items)))))
  129. ;; The commands search uses the command-palette hander
  130. (defmethod load-results :commands [group state]
  131. (let [!input (::input state)
  132. !results (::results state)]
  133. (swap! !results assoc-in [group :status] :loading)
  134. (if (empty? @!input)
  135. (swap! !results update group merge {:status :success :items default-commands})
  136. (->> (cp-handler/top-commands 1000)
  137. (map #(assoc % :t (cp/translate t %)))
  138. (filter #(string/includes? (lower-case-str (pr-str %)) (lower-case-str @!input)))
  139. (map #(hash-map :icon "command"
  140. :icon-theme :gray
  141. :text (cp/translate t %)
  142. :shortcut (:shortcut %)
  143. :source-command %))
  144. (hash-map :status :success :items)
  145. (swap! !results update group merge)))))
  146. ;; The pages search action uses an existing handler
  147. (defmethod load-results :pages [group state]
  148. (let [!input (::input state)
  149. !results (::results state)]
  150. (swap! !results assoc-in [group :status] :loading)
  151. (swap! !results assoc-in [:whiteboards :status] :loading)
  152. (p/let [pages (search/page-search @!input)
  153. whiteboards (filter model/whiteboard-page? pages)
  154. non-boards (remove model/whiteboard-page? pages)
  155. whiteboard-items (map #(hash-map :icon "page"
  156. :icon-theme :gray
  157. :text %
  158. :source-page %) whiteboards)
  159. non-board-items (map #(hash-map :icon "page"
  160. :icon-theme :gray
  161. :text %
  162. :source-page %) non-boards)]
  163. (swap! !results update group merge {:status :success :items non-board-items})
  164. (swap! !results update :whiteboards merge {:status :success :items whiteboard-items}))))
  165. ;; The blocks search action uses an existing handler
  166. (defmethod load-results :blocks [group state]
  167. (let [!input (::input state)
  168. !results (::results state)
  169. repo (state/get-current-repo)
  170. current-page (page-util/get-current-page-id)
  171. opts {:limit 100}]
  172. (swap! !results assoc-in [group :status] :loading)
  173. (swap! !results assoc-in [:current-page :status] :loading)
  174. (p/let [blocks (search/block-search repo @!input opts)
  175. blocks (remove nil? blocks)
  176. items (map (fn [block]
  177. (let [id (if (uuid? (:block/uuid block))
  178. (:block/uuid block)
  179. (uuid (:block/uuid block)))]
  180. {:icon "block"
  181. :icon-theme :gray
  182. :text (:block/content block)
  183. :header (block/breadcrumb {:search? true} repo id {})
  184. :current-page? (some-> block :block/page #{current-page})
  185. :source-block block})) blocks)
  186. items-on-other-pages (remove :current-page? items)
  187. items-on-current-page (filter :current-page? items)]
  188. ; (js/console.log "blocks" (clj->js items) current-page)
  189. ; ; (js/console.log "blocks" (clj->js items)
  190. ; ; (pr-str (map (comp pr-str :block/page) blocks))
  191. ; ; (pr-str (map (comp :block/name :block/page) blocks))
  192. ; ; (pr-str (map (comp :block/name db/entity :block/page) blocks)))
  193. ; ; (js/console.log "load-results/blocks"
  194. ; ; (clj->js blocks)
  195. ; ; (pr-str (first blocks)))
  196. (swap! !results update group merge {:status :success :items items-on-other-pages})
  197. (swap! !results update :current-page merge {:status :success :items items-on-current-page}))))
  198. (defmethod load-results :files [group state]
  199. (let [!input (::input state)
  200. !results (::results state)]
  201. (swap! !results assoc-in [group :status] :loading)
  202. (p/let [files (search/file-search @!input 99)]
  203. (js/console.log "load-results/files" (clj->js files)))))
  204. (defmethod load-results :recents [group state]
  205. (let [!input (::input state)
  206. !results (::results state)
  207. recent-searches (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
  208. recent-pages (mapv (fn [page] {:type :page :data page}) (db/get-key-value :recent/pages))]
  209. (js/console.log "recents" (clj->js recent-searches) (clj->js recent-pages))
  210. (swap! !results assoc-in [group :status] :loading)
  211. (let [items (->> (concat recent-searches recent-pages)
  212. (filter #(string/includes? (lower-case-str (:data %)) (lower-case-str @!input)))
  213. (map #(hash-map :icon (if (= :page (:type %)) "page" "history")
  214. :icon-theme :gray
  215. :text (:data %)
  216. :source-recent %
  217. :source-page (when (= :page (:type %)) (:data %))
  218. :source-search (when (= :search (:type %)) (:data %)))))]
  219. (swap! !results update group merge {:status :success :items items}))))
  220. (defmethod load-results :filters [group state]
  221. (let [!results (::results state)
  222. !input (::input state)
  223. input @!input
  224. q (or (last (gp-util/split-last " /" input)) "")
  225. matched-items (if (string/blank? q)
  226. filters
  227. (search/fuzzy-search filters q {:extract-fn :text}))]
  228. (swap! !results update group merge {:status :success :items matched-items})))
  229. ;; The default load-results function triggers all the other load-results function
  230. (defmethod load-results :default [_ state]
  231. (js/console.log "load-results/default" @(::input state))
  232. (if-not (some-> state ::input deref seq)
  233. (load-results :initial state)
  234. (do
  235. (load-results :commands state)
  236. (load-results :blocks state)
  237. (load-results :pages state)
  238. (load-results :filters state)
  239. (load-results :recents state))))
  240. (defn close-unless-alt! [state]
  241. (when-not (some-> state ::alt? deref)
  242. (state/close-modal!)))
  243. (defn- copy-block-ref [state]
  244. (when-let [block-uuid (some-> state state->highlighted-item :source-block :block/uuid uuid)]
  245. (editor-handler/copy-block-ref! block-uuid block-ref/->block-ref)
  246. (close-unless-alt! state)))
  247. (defmulti handle-action (fn [action _state _event] action))
  248. (defmethod handle-action :open-page [_ state event]
  249. (when-let [page-name (some-> state state->highlighted-item :source-page)]
  250. (route-handler/redirect-to-page! page-name)
  251. (close-unless-alt! state)))
  252. (defmethod handle-action :open-block [_ state event]
  253. (let [block-id (some-> state state->highlighted-item :source-block :block/uuid uuid)
  254. get-block-page (partial model/get-block-page (state/get-current-repo))]
  255. (when-let [page (some-> block-id get-block-page :block/name)]
  256. (route-handler/redirect-to-page! page {:anchor (str "ls-block-" block-id)})
  257. (close-unless-alt! state))))
  258. (defmethod handle-action :open-page-right [_ state event]
  259. (when-let [page-name (some-> state state->highlighted-item :source-page)]
  260. (when-let [page (db/entity [:block/name (util/page-name-sanity-lc page-name)])]
  261. (editor-handler/open-block-in-sidebar! (:block/uuid page)))
  262. (close-unless-alt! state)))
  263. (defmethod handle-action :open-block-right [_ state event]
  264. (when-let [block-uuid (some-> state state->highlighted-item :source-block :block/uuid uuid)]
  265. (editor-handler/open-block-in-sidebar! block-uuid)
  266. (close-unless-alt! state)))
  267. (defmethod handle-action :open [_ state event]
  268. (when-let [item (some-> state state->highlighted-item)]
  269. (let [shift? @(::shift? state)
  270. page? (boolean (:source-page item))
  271. block? (boolean (:source-block item))]
  272. (cond
  273. (and shift? block?) (handle-action :open-block-right state event)
  274. (and shift? page?) (handle-action :open-page-right state event)
  275. block? (handle-action :open-block state event)
  276. page? (handle-action :open-page state event)))))
  277. (defmethod handle-action :search [_ state event]
  278. (when-let [item (some-> state state->highlighted-item)]
  279. (let [search-query (:source-search item)]
  280. (reset! (::input state) search-query))))
  281. (defmethod handle-action :trigger [_ state event]
  282. (when-let [action (some-> state state->highlighted-item :source-command :action)]
  283. (action)
  284. (close-unless-alt! state)))
  285. (defmethod handle-action :create [_ state event]
  286. (let [item (state->highlighted-item state)
  287. create-whiteboard? (= :whiteboard (:source-create item))
  288. create-page? (= :page (:source-create item))
  289. alt? (some-> state ::alt deref)
  290. !input (::input state)]
  291. (cond
  292. (and create-whiteboard? alt?) (whiteboard-handler/create-new-whiteboard-page! @!input)
  293. (and create-whiteboard? (not alt?)) (whiteboard-handler/create-new-whiteboard-and-redirect! @!input)
  294. (and create-page? alt?) (page-handler/create! @!input {:redirect? false})
  295. (and create-page? (not alt?)) (page-handler/create! @!input {:redirect? true}))
  296. (close-unless-alt! state)))
  297. (defmethod handle-action :filter [_ state event]
  298. (let [item (some-> state state->highlighted-item)
  299. !input (::input state)]
  300. (reset! !input (first (gp-util/split-last " /" @!input)))
  301. (let [!filter (::filter state)
  302. group (get-in item [:filter :group])]
  303. (swap! !filter assoc :group group)
  304. (load-results group state))))
  305. (defmethod handle-action :default [_ state event]
  306. (when-let [action (state->action state)]
  307. (handle-action action state event)))
  308. (rum/defc result-group < rum/reactive
  309. [state title group visible-items first-item]
  310. (let [{:keys [show items]} (some-> state ::results deref group)
  311. highlighted-item (or @(::highlighted-item state) first-item)
  312. can-show-less? (< GROUP-LIMIT (count visible-items))
  313. can-show-more? (< (count visible-items) (count items))
  314. show-less #(swap! (::results state) assoc-in [group :show] :less)
  315. show-more #(swap! (::results state) assoc-in [group :show] :more)]
  316. [:div {:class "border-b border-gray-06 pb-1 last:border-b-0"}
  317. [:div {:class "text-xs py-1.5 px-3 flex justify-between items-center gap-2 text-gray-11 bg-gray-02"}
  318. [:div {:class "font-bold text-gray-11 pl-0.5"} title]
  319. (when (not= group :create)
  320. [:div {:class "bg-gray-05 px-1.5 py-px text-gray-12 rounded-full"
  321. :style {:font-size "0.6rem"}}
  322. (if (<= 100 (count items))
  323. (str "99+")
  324. (count items))])
  325. [:div {:class "flex-1"}]
  326. (when (or can-show-more? can-show-less?)
  327. [:a.fade-link.select-node {:on-click (if (= show :more) show-less show-more)}
  328. (if (= show :more)
  329. "Show less"
  330. "Show more")])]
  331. [:div
  332. (for [item visible-items
  333. :let [highlighted? (= item highlighted-item)]]
  334. (shui/list-item (assoc item
  335. :query (when-not (= group :create) @(::input state))
  336. :compact true
  337. :rounded false
  338. :highlighted highlighted?
  339. ;; for some reason, the highlight effect does not always trigger on a
  340. ;; boolean value change so manually pass in the dep
  341. :on-highlight-dep highlighted-item
  342. :on-click (fn [e]
  343. (reset! (::highlighted-item state) item)
  344. (handle-action :default state item)
  345. (when-let [on-click (:on-click item)]
  346. (on-click e)))
  347. :on-mouse-enter (fn [e]
  348. (when (not highlighted?)
  349. (reset! (::highlighted-item state) (assoc item :mouse-enter-triggered-highlight true))))
  350. :on-highlight (fn [ref]
  351. (reset! (::highlighted-group state) group)
  352. (when (and ref (.-current ref)
  353. (not (:mouse-enter-triggered-highlight @(::highlighted-item state))))
  354. (.. ref -current (scrollIntoView #js {:block "center"
  355. :inline "nearest"
  356. :behavior "smooth"})))))
  357. (make-shui-context)))]]))
  358. (defn move-highlight [state n]
  359. (js/console.log "move-highlight" n)
  360. (let [items (mapcat last (state->results-ordered state))
  361. highlighted-item (some-> state ::highlighted-item deref (dissoc :mouse-enter-triggered-highlight))
  362. current-item-index (some->> highlighted-item (.indexOf items))
  363. next-item-index (some-> (or current-item-index 0) (+ n) (mod (count items)))]
  364. (if-let [next-highlighted-item (nth items next-item-index nil)]
  365. (reset! (::highlighted-item state) next-highlighted-item)
  366. (reset! (::highlighted-item state) nil))))
  367. (defn handle-input-change
  368. ([state e] (handle-input-change state e (.. e -target -value)))
  369. ([state _ input]
  370. (let [!input (::input state)
  371. !load-results-throttled (::load-results-throttled state)]
  372. ;; update the input value in the UI
  373. (reset! !input input)
  374. ;; ensure that there is a throttled version of the load-results function
  375. (when-not @!load-results-throttled
  376. (reset! !load-results-throttled (gfun/throttle load-results 100)))
  377. ;; retreive the laod-results function and update all the results
  378. (when-let [load-results-throttled @!load-results-throttled]
  379. (load-results-throttled :default state)))))
  380. (defn- keydown-handler
  381. [state e]
  382. (let [shift? (.-shiftKey e)
  383. meta? (.-metaKey e)
  384. alt? (.-altKey e)
  385. highlighted-group @(::highlighted-group state)
  386. show-less (fn [] (swap! (::results state) assoc-in [highlighted-group :show] :less))
  387. show-more (fn [] (swap! (::results state) assoc-in [highlighted-group :show] :more))
  388. input @(::input state)]
  389. (reset! (::shift? state) shift?)
  390. (reset! (::meta? state) meta?)
  391. (reset! (::alt? state) alt?)
  392. (when (get #{"ArrowUp" "ArrowDown"} (.-key e))
  393. (.preventDefault e))
  394. (case (.-key e)
  395. "ArrowDown" (if meta?
  396. (show-more)
  397. (move-highlight state 1))
  398. "ArrowUp" (if meta?
  399. (show-less)
  400. (move-highlight state -1))
  401. "Enter" (handle-action :default state e)
  402. "Escape" (when-not (string/blank? input)
  403. (util/stop e)
  404. (reset! (::filter state) nil)
  405. (handle-input-change state nil ""))
  406. "c" (copy-block-ref state)
  407. nil)))
  408. (defn keyup-handler
  409. [state e]
  410. (let [shift? (.-shiftKey e)
  411. meta? (.-metaKey e)
  412. alt? (.-altKey e)]
  413. (reset! (::shift? state) shift?)
  414. (reset! (::alt? state) alt?)
  415. (reset! (::meta? state) meta?)))
  416. (defn print-group-name [group]
  417. (case group
  418. :current-page "Current page"
  419. :blocks "Blocks"
  420. :pages "Pages"
  421. :whiteboards "Whiteboards"
  422. :commands "Commands"
  423. :recents "Recents"
  424. (string/capitalize (name group))))
  425. (rum/defc input-row
  426. [state all-items]
  427. (let [highlighted-item @(::highlighted-item state)
  428. input @(::input state)
  429. input-ref (::input-ref state)]
  430. ;; use-effect [results-ordered input] to check whether the highlighted item is still in the results,
  431. ;; if not then clear that puppy out!
  432. ;; This was moved to a fucntional component
  433. (rum/use-effect! (fn []
  434. (when (and highlighted-item (= -1 (.indexOf all-items (dissoc highlighted-item :mouse-enter-triggered-highlight))))
  435. (reset! (::highlighted-item state) nil)))
  436. [all-items])
  437. (rum/use-effect! (fn []
  438. (load-results :default state))
  439. [])
  440. (rum/use-effect! (fn []
  441. (js/setTimeout #(when (some-> input-ref deref) (.focus @input-ref)) 0))
  442. [])
  443. [:div {:class ""
  444. :style {:background "var(--lx-gray-02)"
  445. :border-bottom "1px solid var(--lx-gray-07)"}}
  446. [:input {:class "text-xl bg-transparent border-none w-full outline-none px-4 py-3"
  447. :placeholder "What are you looking for?"
  448. :ref #(when-not @input-ref (reset! input-ref %))
  449. :on-change (fn [e]
  450. (when (= "" (.-value @input-ref))
  451. (reset! (::filter state) nil))
  452. (handle-input-change state e))
  453. :on-key-down (fn [e]
  454. (let [value (.-value @input-ref)
  455. last-char (last value)]
  456. (when (and (some? @(::filter state))
  457. (or (= (util/ekey e) "/")
  458. (and (= (util/ekey e) "Backspace")
  459. (= last-char "/"))))
  460. (reset! (::filter state) nil))))
  461. :value input}]]))
  462. (rum/defc input-row-sidebar
  463. [state all-items]
  464. (let [highlighted-item @(::highlighted-item state)
  465. input @(::input state)
  466. input-ref (::input-ref state)]
  467. ;; use-effect [results-ordered input] to check whether the highlighted item is still in the results,
  468. ;; if not then clear that puppy out!
  469. ;; This was moved to a fucntional component
  470. (rum/use-effect! (fn []
  471. (when (= -1 (.indexOf all-items highlighted-item))
  472. (reset! (::highlighted-item state) nil)))
  473. [all-items])
  474. (rum/use-effect! (fn []
  475. (load-results :default state))
  476. [])
  477. (rum/use-effect! (fn []
  478. (js/setTimeout #(when (some-> input-ref deref) (.focus @input-ref)) 0))
  479. [])
  480. [:div {:class "bg-gray-04 text-white flex items-center px-2 gap-2"}
  481. (ui/rotating-arrow false)
  482. (shui/icon "search" {:class "text-gray-12"})
  483. [:input {:class "text-base bg-transparent border-none w-full outline-none py-2"
  484. :placeholder "What are you looking for?"
  485. :ref #(reset! input-ref %)
  486. :on-change (partial handle-input-change state)
  487. :value input}]
  488. (shui/icon "x" {:class "text-gray-11"})]))
  489. (rum/defc hints
  490. [state]
  491. (let [action (state->action state)
  492. button-fn (fn [text shortcut]
  493. (shui/button {:text text
  494. :theme :gray
  495. :on-click #(handle-action action state %)
  496. :shortcut shortcut
  497. :muted true}
  498. (make-shui-context)))]
  499. (when action
  500. [:div {:class "flex w-full px-4 py-2 gap-2 justify-between"
  501. :style {:background "var(--lx-gray-03)"
  502. :border-top "1px solid var(--lx-gray-07)"}}
  503. [:div.flex.gap-2
  504. (case action
  505. :open
  506. [:<>
  507. (button-fn "Open" ["return"])
  508. (button-fn "Open in sidebar" ["shift" "return"])
  509. (when (:source-block @(::highlighted-item state)) (button-fn "Copy ref" ["⌘" "c"]))]
  510. :search
  511. [:<>
  512. (button-fn "Search" ["return"])]
  513. :trigger
  514. [:<>
  515. (button-fn "Trigger" ["return"])]
  516. :create
  517. [:<>
  518. (button-fn "Create" ["return"])]
  519. :filter
  520. [:<>
  521. (button-fn "Filter" ["return"])]
  522. nil)]
  523. [:div.text-sm.opacity-30.hover:opacity-90.leading-6
  524. "Tip: type / to add search filters"]])))
  525. (rum/defcs cmdk <
  526. shortcut/disable-all-shortcuts
  527. rum/reactive
  528. {:init (fn [state]
  529. (assoc state ::ref (atom nil)))}
  530. (mixins/event-mixin
  531. (fn [state]
  532. (let [ref @(::ref state)]
  533. (mixins/on-key-down state {}
  534. {:target ref
  535. :all-handler (fn [e _key] (keydown-handler state e))})
  536. (mixins/on-key-up state {}
  537. {:target ref
  538. :all-handler (fn [e _key] (keyup-handler state e))}))))
  539. (rum/local "" ::input)
  540. (rum/local false ::shift?)
  541. (rum/local false ::meta?)
  542. (rum/local false ::alt?)
  543. (rum/local nil ::highlighted-group)
  544. (rum/local nil ::highlighted-item)
  545. (rum/local nil ::filter)
  546. (rum/local default-results ::results)
  547. (rum/local nil ::load-results-throttled)
  548. (rum/local nil ::scroll-container-ref)
  549. (rum/local nil ::input-ref)
  550. [state {:keys [sidebar?]}]
  551. (let [filter (not-empty @(::filter state))
  552. group-filter (:group filter)
  553. results-ordered (state->results-ordered state)
  554. all-items (mapcat last results-ordered)
  555. first-item (first all-items)]
  556. [:div.cp__cmdk {:ref #(when-not @(::ref state) (reset! (::ref state) %))
  557. :class (cond-> "w-full h-full relative flex flex-col justify-start"
  558. (not sidebar?) (str " border border-gray-06 rounded-lg"))}
  559. (if sidebar?
  560. (input-row-sidebar state all-items)
  561. (input-row state all-items))
  562. [:div {:class (cond-> "w-full flex-1 overflow-y-auto max-h-[65dvh]"
  563. (not sidebar?) (str " pb-14"))
  564. :ref #(when % (some-> state ::scroll-container-ref (reset! %)))
  565. :style {:background "var(--lx-gray-02)"}}
  566. (for [[group-name group-key group-count group-items] results-ordered
  567. :when (not= 0 group-count)
  568. :when (if-not group-filter true
  569. (or (= group-filter group-key)
  570. (and filter (= group-key :create))))]
  571. (result-group state group-name group-key group-items first-item))]
  572. (hints state)]))
  573. (rum/defc cmdk-modal [props]
  574. [:div {:class "cp__cmdk__modal rounded-lg max-h-[65dvh] w-[90dvw] max-w-4xl shadow-xl relative"}
  575. (cmdk props)])
  576. (comment
  577. (rum/defc cmdk-block [props]
  578. [:div {:class "cp__cmdk__block rounded-md"}
  579. (cmdk props)]))