1
0

cmdk.cljs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. (ns frontend.components.cmdk
  2. (:require
  3. [clojure.string :as string]
  4. [frontend.components.block :as block]
  5. [frontend.context.i18n :refer [t]]
  6. [frontend.db :as db]
  7. [frontend.db.model :as model]
  8. [frontend.handler.command-palette :as cp-handler]
  9. [frontend.handler.editor :as editor-handler]
  10. [frontend.handler.page :as page-handler]
  11. [frontend.handler.route :as route-handler]
  12. [frontend.handler.whiteboard :as whiteboard-handler]
  13. [frontend.modules.shortcut.core :as shortcut]
  14. [frontend.search :as search]
  15. [frontend.shui :refer [make-shui-context]]
  16. [frontend.state :as state]
  17. [frontend.ui :as ui]
  18. [frontend.util :as util]
  19. [frontend.util.page :as page-util]
  20. [goog.functions :as gfun]
  21. [goog.object :as gobj]
  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. [logseq.shui.button.v2 :as button]
  29. [frontend.modules.shortcut.utils :as shortcut-utils]))
  30. (defn translate [t {:keys [id desc]}]
  31. (when id
  32. (let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
  33. (if (string/starts-with? desc-i18n "{Missing key")
  34. desc
  35. desc-i18n))))
  36. (def GROUP-LIMIT 5)
  37. (def search-actions
  38. [{:text "Search only pages" :info "Add filter to search" :icon-theme :gray :icon "page" :filter {:mode "search"
  39. :group :pages}}
  40. {:text "Search only current page" :info "Add filter to search" :icon-theme :gray :icon "page" :filter {:mode "search"
  41. :group :current-page}}
  42. {:text "Search only blocks" :info "Add filter to search" :icon-theme :gray :icon "block" :filter {:mode "search"
  43. :group :blocks}}
  44. {:text "Search only commands" :info "Add filter to search" :icon-theme :gray :icon "command" :filter {:mode "search"
  45. :group :commands}}
  46. {:text "Search only files" :info "Add filter to search" :icon-theme :gray :icon "file" :filter {:mode "search"
  47. :group :files}}])
  48. (def filters search-actions)
  49. ;; The results are separated into groups, and loaded/fetched/queried separately
  50. (def default-results
  51. {:commands {:status :success :show :less :items nil}
  52. :favorites {:status :success :show :less :items nil}
  53. :current-page {:status :success :show :less :items nil}
  54. :pages {:status :success :show :less :items nil}
  55. :blocks {:status :success :show :less :items nil}
  56. :files {:status :success :show :less :items nil}
  57. :filters {:status :success :show :less :items nil}})
  58. (defn create-items [q]
  59. (when-not (string/blank? q)
  60. (let [class? (string/starts-with? q "#")]
  61. (->> [{:text (if class? "Create class" "Create page") :icon "new-page"
  62. :icon-theme :gray
  63. :info (str "Create page called '" q "'") :source-create :page}
  64. (when-not class?
  65. {:text "Create whiteboard" :icon "new-whiteboard"
  66. :icon-theme :gray
  67. :info (str "Create whiteboard called '" q "'") :source-create :whiteboard})]
  68. (remove nil?)))))
  69. ;; Take the results, decide how many items to show, and order the results appropriately
  70. (defn state->results-ordered [state search-mode]
  71. (let [sidebar? (:sidebar? (last (:rum/args state)))
  72. results @(::results state)
  73. input @(::input state)
  74. filter @(::filter state)
  75. filter-group (:group filter)
  76. index (volatile! -1)
  77. visible-items (fn [group]
  78. (let [{:keys [items show]} (get results group)]
  79. (cond
  80. (or sidebar? (= group filter-group))
  81. items
  82. (= :more show)
  83. items
  84. :else
  85. (take 5 items))))
  86. page-exists? (when-not (string/blank? input)
  87. (db/entity [:block/name (string/trim input)]))
  88. filter-mode? (or (string/includes? input " /")
  89. (string/starts-with? input "/"))
  90. order* (cond
  91. (= search-mode :graph)
  92. [["Pages" :pages (visible-items :pages)]]
  93. filter-mode?
  94. [["Filters" :filters (visible-items :filters)]
  95. ["Pages" :pages (visible-items :pages)]
  96. (when-not page-exists?
  97. ["Create" :create (create-items input)])]
  98. filter-group
  99. [(when (= filter-group :blocks)
  100. ["Current page" :current-page (visible-items :current-page)])
  101. [(if (= filter-group :current-page) "Current page" (name filter-group))
  102. filter-group
  103. (visible-items filter-group)]
  104. (when (= filter-group :pages)
  105. (when-not page-exists?
  106. ["Create" :create (create-items input)]))]
  107. :else
  108. (->>
  109. [["Pages" :pages (visible-items :pages)]
  110. (when-not page-exists?
  111. ["Create" :create (create-items input)])
  112. ["Commands" :commands (visible-items :commands)]
  113. ["Current page" :current-page (visible-items :current-page)]
  114. ["Blocks" :blocks (visible-items :blocks)]
  115. ["Files" :files (visible-items :files)]]
  116. (remove nil?)))
  117. order (remove nil? order*)]
  118. (for [[group-name group-key group-items] order]
  119. [group-name
  120. group-key
  121. (if (= group-key :create)
  122. (count group-items)
  123. (count (get-in results [group-key :items])))
  124. (mapv #(assoc % :item-index (vswap! index inc)) group-items)])))
  125. (defn state->highlighted-item [state]
  126. (or (some-> state ::highlighted-item deref)
  127. (some->> (state->results-ordered state (:search/mode @state/state))
  128. (mapcat last)
  129. (first))))
  130. (defn state->action [state]
  131. (let [highlighted-item (state->highlighted-item state)]
  132. (cond (:source-page highlighted-item) :open
  133. (:source-block highlighted-item) :open
  134. (:file-path highlighted-item) :open
  135. (:source-search highlighted-item) :search
  136. (:source-command highlighted-item) :trigger
  137. (:source-create highlighted-item) :create
  138. (:filter highlighted-item) :filter
  139. :else nil)))
  140. ;; Each result group has it's own load-results function
  141. (defmulti load-results (fn [group _state] group))
  142. (defmethod load-results :initial [_ state]
  143. (let [!results (::results state)
  144. command-items (->> (cp-handler/top-commands 1000)
  145. (remove (fn [c] (= :window/close (:id c))))
  146. (map #(hash-map :icon "command"
  147. :icon-theme :gray
  148. :text (translate t %)
  149. :shortcut (:shortcut %)
  150. :source-command %)))]
  151. (reset! !results (assoc-in default-results [:commands :items] command-items))))
  152. ;; The commands search uses the command-palette handler
  153. (defmethod load-results :commands [group state]
  154. (let [!input (::input state)
  155. !results (::results state)]
  156. (swap! !results assoc-in [group :status] :loading)
  157. (let [commands (->> (cp-handler/top-commands 1000)
  158. (map #(assoc % :t (translate t %))))
  159. search-results (if (string/blank? @!input)
  160. commands
  161. (search/fuzzy-search commands @!input {:extract-fn :t}))]
  162. (->> search-results
  163. (map #(hash-map :icon "command"
  164. :icon-theme :gray
  165. :text (translate t %)
  166. :shortcut (:shortcut %)
  167. :source-command %))
  168. (hash-map :status :success :items)
  169. (swap! !results update group merge)))))
  170. ;; The pages search action uses an existing handler
  171. (defmethod load-results :pages [group state]
  172. (let [!input (::input state)
  173. !results (::results state)]
  174. (swap! !results assoc-in [group :status] :loading)
  175. (p/let [pages (search/page-search @!input)
  176. items (map
  177. (fn [page]
  178. (let [entity (db/entity [:block/name (util/page-name-sanity-lc page)])
  179. whiteboard? (= (:block/type entity) "whiteboard")]
  180. (hash-map :icon (if whiteboard? "whiteboard" "page")
  181. :icon-theme :gray
  182. :text page
  183. :source-page page)))
  184. pages)]
  185. (swap! !results update group merge {:status :success :items items}))))
  186. ;; The blocks search action uses an existing handler
  187. (defmethod load-results :blocks [group state]
  188. (let [!input (::input state)
  189. !results (::results state)
  190. repo (state/get-current-repo)
  191. current-page (page-util/get-current-page-id)
  192. opts {:limit 100}]
  193. (swap! !results assoc-in [group :status] :loading)
  194. (swap! !results assoc-in [:current-page :status] :loading)
  195. (p/let [blocks (search/block-search repo @!input opts)
  196. blocks (remove nil? blocks)
  197. items (map (fn [block]
  198. (let [id (if (uuid? (:block/uuid block))
  199. (:block/uuid block)
  200. (uuid (:block/uuid block)))]
  201. {:icon "block"
  202. :icon-theme :gray
  203. :text (:block/content block)
  204. :header (block/breadcrumb {:search? true} repo id {})
  205. :current-page? (some-> block :block/page #{current-page})
  206. :source-block block})) blocks)
  207. items-on-other-pages (remove :current-page? items)
  208. items-on-current-page (filter :current-page? items)]
  209. (swap! !results update group merge {:status :success :items items-on-other-pages})
  210. (swap! !results update :current-page merge {:status :success :items items-on-current-page}))))
  211. (defmethod load-results :files [group state]
  212. (let [!input (::input state)
  213. !results (::results state)]
  214. (swap! !results assoc-in [group :status] :loading)
  215. (p/let [files (search/file-search @!input 99)
  216. items (map
  217. (fn [file]
  218. (hash-map :icon "file"
  219. :icon-theme :gray
  220. :text file
  221. :file-path file))
  222. files)]
  223. (swap! !results update group merge {:status :success :items items}))))
  224. (defn- get-filter-q
  225. [input]
  226. (or (when (string/starts-with? input "/")
  227. (subs input 1))
  228. (last (gp-util/split-last " /" input))))
  229. (defmethod load-results :filters [group state]
  230. (let [!results (::results state)
  231. !input (::input state)
  232. input @!input
  233. q (or (get-filter-q input) "")
  234. matched-items (if (string/blank? q)
  235. filters
  236. (search/fuzzy-search filters q {:extract-fn :text}))]
  237. (swap! !results update group merge {:status :success :items matched-items})))
  238. ;; The default load-results function triggers all the other load-results function
  239. (defmethod load-results :default [_ state]
  240. (js/console.log "load-results/default" @(::input state))
  241. (if-not (some-> state ::input deref seq)
  242. (load-results :initial state)
  243. (do
  244. (load-results :commands state)
  245. (load-results :blocks state)
  246. (load-results :pages state)
  247. (load-results :filters state)
  248. (load-results :files state))))
  249. (defn close-unless-alt! [state]
  250. (when-not (some-> state ::alt? deref)
  251. (state/close-modal!)))
  252. (defn- copy-block-ref [state]
  253. (when-let [block-uuid (some-> state state->highlighted-item :source-block :block/uuid uuid)]
  254. (editor-handler/copy-block-ref! block-uuid block-ref/->block-ref)
  255. (close-unless-alt! state)))
  256. (defmulti handle-action (fn [action _state _event] action))
  257. (defmethod handle-action :open-page [_ state _event]
  258. (when-let [page-name (some-> state state->highlighted-item :source-page)]
  259. (let [page (db/entity [:block/name (util/page-name-sanity-lc page-name)])]
  260. (if (= (:block/type page) "whiteboard")
  261. (route-handler/redirect-to-whiteboard! page-name)
  262. (route-handler/redirect-to-page! page-name)))
  263. (close-unless-alt! state)))
  264. (defmethod handle-action :open-block [_ state _event]
  265. (let [block-id (some-> state state->highlighted-item :source-block :block/uuid uuid)
  266. get-block-page (partial model/get-block-page (state/get-current-repo))]
  267. (when-let [page (some-> block-id get-block-page)]
  268. (let [page-name (:block/name page)]
  269. (if (= (:block/type page) "whiteboard")
  270. (route-handler/redirect-to-whiteboard! page-name {:block-id block-id})
  271. (route-handler/redirect-to-page! page-name {:anchor (str "ls-block-" block-id)})))
  272. (close-unless-alt! state))))
  273. (defmethod handle-action :open-page-right [_ state _event]
  274. (when-let [page-name (some-> state state->highlighted-item :source-page)]
  275. (when-let [page (db/entity [:block/name (util/page-name-sanity-lc page-name)])]
  276. (editor-handler/open-block-in-sidebar! (:block/uuid page)))
  277. (close-unless-alt! state)))
  278. (defmethod handle-action :open-block-right [_ state _event]
  279. (when-let [block-uuid (some-> state state->highlighted-item :source-block :block/uuid uuid)]
  280. (editor-handler/open-block-in-sidebar! block-uuid)
  281. (close-unless-alt! state)))
  282. (defmethod handle-action :open [_ state event]
  283. (when-let [item (some-> state state->highlighted-item)]
  284. (let [page? (boolean (:source-page item))
  285. block? (boolean (:source-block item))
  286. shift? @(::shift? state)
  287. shift-or-sidebar? (or shift? (boolean (:open-sidebar? (:opts state))))
  288. search-mode (:search/mode @state/state)
  289. graph-view? (= search-mode :graph)]
  290. (cond
  291. (:file-path item) (do
  292. (route-handler/redirect! {:to :file
  293. :path-params {:path (:file-path item)}})
  294. (state/close-modal!))
  295. (and graph-view? page? (not shift?)) (do
  296. (state/add-graph-search-filter! @(::input state))
  297. (reset! (::input state) ""))
  298. (and shift-or-sidebar? block?) (handle-action :open-block-right state event)
  299. (and shift-or-sidebar? page?) (handle-action :open-page-right state event)
  300. block? (handle-action :open-block state event)
  301. page? (handle-action :open-page state event)))))
  302. (defmethod handle-action :search [_ state _event]
  303. (when-let [item (some-> state state->highlighted-item)]
  304. (let [search-query (:source-search item)]
  305. (reset! (::input state) search-query))))
  306. (defmethod handle-action :trigger [_ state _event]
  307. (let [command (some-> state state->highlighted-item :source-command)]
  308. (when-let [action (:action command)]
  309. (action)
  310. (when-not (contains? #{:graph/open :graph/remove :ui/toggle-settings :go/flashcards} (:id command))
  311. (close-unless-alt! state)))))
  312. (defmethod handle-action :create [_ state _event]
  313. (let [item (state->highlighted-item state)
  314. !input (::input state)
  315. create-class? (string/starts-with? @!input "#")
  316. create-whiteboard? (= :whiteboard (:source-create item))
  317. create-page? (= :page (:source-create item))
  318. alt? (some-> state ::alt deref)
  319. class (when create-class? (string/replace @!input #"^#+" ""))]
  320. (cond
  321. create-class? (page-handler/create! class
  322. {:redirect? false
  323. :create-first-block? false
  324. :class? true})
  325. (and create-whiteboard? alt?) (whiteboard-handler/create-new-whiteboard-page! @!input)
  326. (and create-whiteboard? (not alt?)) (whiteboard-handler/create-new-whiteboard-and-redirect! @!input)
  327. (and create-page? alt?) (page-handler/create! @!input {:redirect? false})
  328. (and create-page? (not alt?)) (page-handler/create! @!input {:redirect? true}))
  329. (if create-class?
  330. (state/pub-event! [:class/configure (db/entity [:block/name (util/page-name-sanity-lc class)])])
  331. (close-unless-alt! state))))
  332. (defn- get-filter-user-input
  333. [input]
  334. (cond
  335. (string/includes? input " /")
  336. (first (gp-util/split-last " /" input))
  337. (string/starts-with? input "/")
  338. ""
  339. :else
  340. input))
  341. (defmethod handle-action :filter [_ state _event]
  342. (let [item (some-> state state->highlighted-item)
  343. !input (::input state)]
  344. (reset! !input (get-filter-user-input @!input))
  345. (let [!filter (::filter state)
  346. group (get-in item [:filter :group])]
  347. (swap! !filter assoc :group group)
  348. (load-results group state))))
  349. (defmethod handle-action :default [_ state event]
  350. (when-let [action (state->action state)]
  351. (handle-action action state event)))
  352. (defn- scroll-into-view-when-invisible
  353. [state target]
  354. (let [*container-ref (::scroll-container-ref state)
  355. container-rect (.getBoundingClientRect @*container-ref)
  356. t1 (.-top container-rect)
  357. b1 (.-bottom container-rect)
  358. target-rect (.getBoundingClientRect target)
  359. t2 (.-top target-rect)
  360. b2 (.-bottom target-rect)]
  361. (when-not (<= t1 t2 b2 b1) ; not visible
  362. (.scrollIntoView target
  363. #js {:inline "nearest"
  364. :behavior "smooth"}))))
  365. (rum/defc mouse-active-effect!
  366. [*mouse-active? deps]
  367. (rum/use-effect!
  368. #(reset! *mouse-active? false)
  369. deps)
  370. nil)
  371. (rum/defcs result-group
  372. < rum/reactive
  373. (rum/local false ::mouse-active?)
  374. [state' state title group visible-items first-item sidebar?]
  375. (let [{:keys [show items]} (some-> state ::results deref group)
  376. highlighted-item (or @(::highlighted-item state) first-item)
  377. highlighted-group @(::highlighted-group state)
  378. *mouse-active? (::mouse-active? state')
  379. filter @(::filter state)
  380. can-show-less? (< GROUP-LIMIT (count visible-items))
  381. can-show-more? (< (count visible-items) (count items))
  382. show-less #(swap! (::results state) assoc-in [group :show] :less)
  383. show-more #(swap! (::results state) assoc-in [group :show] :more)
  384. context (make-shui-context)]
  385. [:<>
  386. (mouse-active-effect! *mouse-active? [highlighted-item])
  387. [:div {:class "border-b border-gray-06 pb-1 last:border-b-0"
  388. :on-mouse-move #(reset! *mouse-active? true)}
  389. [:div {:class "text-xs py-1.5 px-3 flex justify-between items-center gap-2 text-gray-11 bg-gray-02"}
  390. [:div {:class "font-bold text-gray-11 pl-0.5"} title]
  391. (when (not= group :create)
  392. [:div {:class "pl-1.5 text-gray-12 rounded-full"
  393. :style {:font-size "0.7rem"}}
  394. (if (<= 100 (count items))
  395. (str "99+")
  396. (count items))])
  397. [:div {:class "flex-1"}]
  398. (when (and (= group highlighted-group)
  399. (or can-show-more? can-show-less?)
  400. (empty? filter)
  401. (not sidebar?))
  402. [:a.text-link.select-node.opacity-50.hover:opacity-90
  403. {:on-click (if (= show :more) show-less show-more)}
  404. (if (= show :more)
  405. [:div.flex.flex-row.gap-1.items-center
  406. "Show less"
  407. (shui/shortcut "mod up" context)]
  408. [:div.flex.flex-row.gap-1.items-center
  409. "Show more"
  410. (shui/shortcut "mod down" context)])])]
  411. [:div.search-results
  412. (for [item visible-items
  413. :let [highlighted? (= item highlighted-item)]]
  414. (let [item (shui/list-item (assoc item
  415. :query (when-not (= group :create) @(::input state))
  416. :compact true
  417. :rounded false
  418. :hoverable @*mouse-active?
  419. :highlighted highlighted?
  420. :display-shortcut-on-highlight? true
  421. ;; for some reason, the highlight effect does not always trigger on a
  422. ;; boolean value change so manually pass in the dep
  423. :on-highlight-dep highlighted-item
  424. :on-click (fn [e]
  425. (reset! (::highlighted-item state) item)
  426. (handle-action :default state item)
  427. (when-let [on-click (:on-click item)]
  428. (on-click e)))
  429. ;; :on-mouse-enter (fn [e]
  430. ;; (when (not highlighted?)
  431. ;; (reset! (::highlighted-item state) (assoc item :mouse-enter-triggered-highlight true))))
  432. :on-highlight (fn [ref]
  433. (reset! (::highlighted-group state) group)
  434. (when (and ref (.-current ref)
  435. (not (:mouse-enter-triggered-highlight @(::highlighted-item state))))
  436. (scroll-into-view-when-invisible state (.-current ref)))))
  437. context)]
  438. (if (= group :blocks)
  439. (ui/lazy-visible (fn [] item) {:trigger-once? true})
  440. item)))]]]))
  441. (defn move-highlight [state n]
  442. (let [items (mapcat last (state->results-ordered state (:search/mode @state/state)))
  443. highlighted-item (some-> state ::highlighted-item deref (dissoc :mouse-enter-triggered-highlight))
  444. current-item-index (some->> highlighted-item (.indexOf items))
  445. next-item-index (some-> (or current-item-index 0) (+ n) (mod (count items)))]
  446. (if-let [next-highlighted-item (nth items next-item-index nil)]
  447. (reset! (::highlighted-item state) next-highlighted-item)
  448. (reset! (::highlighted-item state) nil))))
  449. (defn handle-input-change
  450. ([state e] (handle-input-change state e (.. e -target -value)))
  451. ([state e input]
  452. (let [composing? (util/onchange-event-is-composing? e)
  453. e-type (gobj/getValueByKeys e "type")
  454. !input (::input state)
  455. !load-results-throttled (::load-results-throttled state)]
  456. ;; update the input value in the UI
  457. (reset! !input input)
  458. ;; ensure that there is a throttled version of the load-results function
  459. (when-not @!load-results-throttled
  460. (reset! !load-results-throttled (gfun/throttle load-results 50)))
  461. ;; retrieve the load-results function and update all the results
  462. (when (or (not composing?) (= e-type "compositionend"))
  463. (when-let [load-results-throttled @!load-results-throttled]
  464. (load-results-throttled :default state))))))
  465. (defn- keydown-handler
  466. [state e]
  467. (let [shift? (.-shiftKey e)
  468. meta? (.-metaKey e)
  469. alt? (.-altKey e)
  470. ctrl? (.-ctrlKey e)
  471. keyname (.-key e)
  472. enter? (= keyname "Enter")
  473. esc? (= keyname "Escape")
  474. highlighted-group @(::highlighted-group state)
  475. show-less (fn [] (swap! (::results state) assoc-in [highlighted-group :show] :less))
  476. show-more (fn [] (swap! (::results state) assoc-in [highlighted-group :show] :more))
  477. input @(::input state)
  478. as-keydown? (or (= keyname "ArrowDown") (and ctrl? (= keyname "n")))
  479. as-keyup? (or (= keyname "ArrowUp") (and ctrl? (= keyname "p")))]
  480. (reset! (::shift? state) shift?)
  481. (reset! (::meta? state) meta?)
  482. (reset! (::alt? state) alt?)
  483. (when (or as-keydown? as-keyup?)
  484. (.preventDefault e))
  485. (when-not esc? (util/stop-propagation e))
  486. (cond
  487. (and meta? enter?
  488. (not (string/blank? input)))
  489. (let [repo (state/get-current-repo)]
  490. (state/close-modal!)
  491. (state/sidebar-add-block! repo input :search))
  492. as-keydown? (if meta?
  493. (show-more)
  494. (move-highlight state 1))
  495. as-keyup? (if meta?
  496. (show-less)
  497. (move-highlight state -1))
  498. enter? (handle-action :default state e)
  499. esc? (let [filter @(::filter state)]
  500. (when (or filter (not (string/blank? input)))
  501. (util/stop e)
  502. (reset! (::filter state) nil)
  503. (when-not filter (handle-input-change state nil ""))))
  504. (= keyname "c") (copy-block-ref state)
  505. :else nil)))
  506. (defn keyup-handler
  507. [state e]
  508. (let [shift? (.-shiftKey e)
  509. meta? (.-metaKey e)
  510. alt? (.-altKey e)]
  511. (reset! (::shift? state) shift?)
  512. (reset! (::alt? state) alt?)
  513. (reset! (::meta? state) meta?)))
  514. (defn- input-placeholder
  515. [sidebar?]
  516. (let [search-mode (:search/mode @state/state)]
  517. (cond
  518. (and (= search-mode :graph) (not sidebar?))
  519. "Add graph filter"
  520. :else
  521. "What are you looking for?")))
  522. (rum/defc input-row
  523. [state all-items opts]
  524. (let [highlighted-item @(::highlighted-item state)
  525. input @(::input state)
  526. input-ref (::input-ref state)]
  527. ;; use-effect [results-ordered input] to check whether the highlighted item is still in the results,
  528. ;; if not then clear that puppy out!
  529. ;; This was moved to a functional component
  530. (rum/use-effect! (fn []
  531. (when (and highlighted-item (= -1 (.indexOf all-items (dissoc highlighted-item :mouse-enter-triggered-highlight))))
  532. (reset! (::highlighted-item state) nil)))
  533. [all-items])
  534. (rum/use-effect! (fn [] (load-results :default state)) [])
  535. [:div {:class "bg-gray-02 border-b border-1 border-gray-07"}
  536. [:input#search
  537. {:class "text-xl bg-transparent border-none w-full outline-none px-3 py-3"
  538. :auto-focus true
  539. :autoComplete "off"
  540. :placeholder (input-placeholder false)
  541. :ref #(when-not @input-ref (reset! input-ref %))
  542. :on-change (fn [e]
  543. (handle-input-change state e)
  544. (when-let [on-change (:on-input-change opts)]
  545. (on-change (.-value (.-target e)))))
  546. :on-blur (fn [_e]
  547. (when-let [on-blur (:on-input-blur opts)]
  548. (on-blur input)))
  549. :on-composition-end (fn [e] (handle-input-change state e))
  550. :on-key-down (fn [e]
  551. (let [value (.-value @input-ref)
  552. last-char (last value)]
  553. (when (and (some? @(::filter state))
  554. (or (= (util/ekey e) "/")
  555. (and (= (util/ekey e) "Backspace")
  556. (= last-char "/"))))
  557. (reset! (::filter state) nil))))
  558. :value input}]]))
  559. (defn rand-tip
  560. [context]
  561. (rand-nth
  562. [[:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
  563. [:div "Type"]
  564. (shui/shortcut "/" context)
  565. [:div "to filter search results"]]
  566. [:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
  567. (shui/shortcut "mod enter" context)
  568. [:div "to open search in the sidebar"]]]))
  569. (rum/defcs tip <
  570. {:init (fn [state]
  571. (assoc state ::rand-tip (rand-tip (last (:rum/args state)))))}
  572. [inner-state state context]
  573. (let [filter @(::filter state)]
  574. (cond
  575. filter
  576. [:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
  577. [:div "Type"]
  578. (shui/shortcut "esc" context {:tiled false})
  579. [:div "to clear search filter"]]
  580. :else
  581. (::rand-tip inner-state))))
  582. (rum/defc hints
  583. [state]
  584. (let [context (make-shui-context)
  585. action (state->action state)
  586. button-fn (fn [text shortcut & {:as opts}]
  587. (shui/button {:text text
  588. :theme :text
  589. :hover-theme :gray
  590. :on-click #(handle-action action (assoc state :opts opts) %)
  591. :shortcut shortcut
  592. :muted true}
  593. context))]
  594. (when action
  595. [:div {:class "flex w-full px-3 py-2 gap-2 justify-between"
  596. :style {:background "var(--lx-gray-03)"
  597. :border-top "1px solid var(--lx-gray-07)"}}
  598. [:div.text-sm.leading-6
  599. [:div.flex.flex-row.gap-1.items-center
  600. [:div.font-medium.text-gray-12 "Tip:"]
  601. (tip state context)]]
  602. [:div.gap-2.hidden.md:flex {:style {:margin-right -6}}
  603. (case action
  604. :open
  605. [:<>
  606. (button-fn "Open" ["return"])
  607. (button-fn "Open in sidebar" ["shift" "return"] {:open-sidebar? true})
  608. (when (:source-block @(::highlighted-item state)) (button-fn "Copy ref" ["⌘" "c"]))]
  609. :search
  610. [:<>
  611. (button-fn "Search" ["return"])]
  612. :trigger
  613. [:<>
  614. (button-fn "Trigger" ["return"])]
  615. :create
  616. [:<>
  617. (button-fn "Create" ["return"])]
  618. :filter
  619. [:<>
  620. (button-fn "Filter" ["return"])]
  621. nil)]])))
  622. (rum/defc search-only
  623. [state group-name]
  624. [:div.flex.flex-row.gap-1.items-center
  625. [:div "Search only:"]
  626. [:div group-name]
  627. (button/root {:icon "x"
  628. :theme :text
  629. :hover-theme :gray
  630. :size :sm
  631. :on-click (fn []
  632. (reset! (::filter state) nil))}
  633. (make-shui-context))])
  634. (rum/defcs cmdk < rum/static
  635. rum/reactive
  636. {:will-mount
  637. (fn [state]
  638. (when-not (:sidebar? (last (:rum/args state)))
  639. (shortcut/unlisten-all!))
  640. state)
  641. :will-unmount
  642. (fn [state]
  643. (when-not (:sidebar? (last (:rum/args state)))
  644. (shortcut/listen-all!))
  645. state)}
  646. {:init (fn [state]
  647. (let [search-mode (:search/mode @state/state)
  648. opts (last (:rum/args state))]
  649. (assoc state
  650. ::ref (atom nil)
  651. ::filter (if (and search-mode
  652. (not (contains? #{:global :graph} search-mode))
  653. (not (:sidebar? opts)))
  654. (atom {:group search-mode})
  655. (atom nil))
  656. ::input (atom (or (:initial-input opts) "")))))
  657. :will-unmount (fn [state]
  658. (state/set-state! :search/mode nil)
  659. state)}
  660. (mixins/event-mixin
  661. (fn [state]
  662. (let [ref @(::ref state)]
  663. (mixins/on-key-down state {}
  664. {:target ref
  665. :all-handler (fn [e _key] (keydown-handler state e))})
  666. (mixins/on-key-up state {}
  667. {:target ref
  668. :all-handler (fn [e _key] (keyup-handler state e))}))))
  669. (rum/local false ::shift?)
  670. (rum/local false ::meta?)
  671. (rum/local false ::alt?)
  672. (rum/local nil ::highlighted-group)
  673. (rum/local nil ::highlighted-item)
  674. (rum/local default-results ::results)
  675. (rum/local nil ::load-results-throttled)
  676. (rum/local nil ::scroll-container-ref)
  677. (rum/local nil ::input-ref)
  678. [state {:keys [sidebar?] :as opts}]
  679. (let [*input (::input state)
  680. _input (rum/react *input)
  681. search-mode (:search/mode @state/state)
  682. group-filter (:group (rum/react (::filter state)))
  683. results-ordered (state->results-ordered state search-mode)
  684. all-items (mapcat last results-ordered)
  685. first-item (first all-items)]
  686. [:div.cp__cmdk {:ref #(when-not @(::ref state) (reset! (::ref state) %))
  687. :class (cond-> "w-full h-full relative flex flex-col justify-start"
  688. (not sidebar?) (str " rounded-lg"))}
  689. (input-row state all-items opts)
  690. [:div {:class (cond-> "w-full flex-1 overflow-y-auto min-h-[65dvh] max-h-[65dvh]"
  691. (not sidebar?) (str " pb-14"))
  692. :ref #(let [*ref (::scroll-container-ref state)]
  693. (when-not @*ref (reset! *ref %)))
  694. :style {:background "var(--lx-gray-02)"}}
  695. (when group-filter
  696. [:div.flex.flex-col.p-3.opacity-50.text-sm
  697. (search-only state (name group-filter))])
  698. (let [items (filter
  699. (fn [[_group-name group-key group-count _group-items]]
  700. (and (not= 0 group-count)
  701. (if-not group-filter true
  702. (or (= group-filter group-key)
  703. (and (= group-filter :blocks)
  704. (= group-key :current-page))
  705. (and (contains? #{:pages :create} group-filter)
  706. (= group-key :create))))))
  707. results-ordered)]
  708. (if (seq items)
  709. (for [[group-name group-key _group-count group-items] items]
  710. (let [title group-name]
  711. (result-group state title group-key group-items first-item sidebar?)))
  712. [:div.flex.flex-col.p-4.opacity-50
  713. (when-not (string/blank? (rum/react *input))
  714. "No matched results")]))]
  715. (when-not sidebar? (hints state))]))
  716. (rum/defc cmdk-modal [props]
  717. [:div {:class "cp__cmdk__modal rounded-lg w-[90dvw] max-w-4xl relative"}
  718. (cmdk props)])
  719. (rum/defc cmdk-block [props]
  720. [:div {:class "cp__cmdk__block rounded-md"}
  721. (cmdk props)])