search.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. (ns frontend.components.search
  2. (:require [rum.core :as rum]
  3. [frontend.util :as util]
  4. [frontend.components.block :as block]
  5. [frontend.components.svg :as svg]
  6. [frontend.handler.route :as route]
  7. [frontend.handler.page :as page-handler]
  8. [frontend.db :as db]
  9. [frontend.db.model :as model]
  10. [frontend.handler.search :as search-handler]
  11. [frontend.extensions.pdf.assets :as pdf-assets]
  12. [frontend.ui :as ui]
  13. [frontend.state :as state]
  14. [frontend.mixins :as mixins]
  15. [frontend.config :as config]
  16. [frontend.search :as search]
  17. [clojure.string :as string]
  18. [goog.object :as gobj]
  19. [medley.core :as medley]
  20. [frontend.context.i18n :as i18n]
  21. [frontend.date :as date]
  22. [reitit.frontend.easy :as rfe]
  23. [frontend.modules.shortcut.core :as shortcut]
  24. [frontend.mobile.util :as mobile-util]))
  25. (defn- partition-between
  26. "Split `coll` at positions where `pred?` is true."
  27. [pred? coll]
  28. (let [switch (reductions not= true (map pred? coll (rest coll)))]
  29. (map (partial map first) (partition-by second (map list coll switch)))))
  30. (defn highlight-exact-query
  31. [content q]
  32. (if (or (string/blank? content) (string/blank? q))
  33. content
  34. (when (and content q)
  35. (let [q-words (string/split q #" ")
  36. lc-content (util/search-normalize content)
  37. lc-q (util/search-normalize q)]
  38. (if (and (string/includes? lc-content lc-q)
  39. (not (util/safe-re-find #" " q)))
  40. (let [i (string/index-of lc-content lc-q)
  41. [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
  42. [:div
  43. (when-not (string/blank? before)
  44. [:span before])
  45. [:mark.p-0.rounded-none (subs content i (+ i (count q)))]
  46. (when-not (string/blank? after)
  47. [:span after])])
  48. (let [elements (loop [words q-words
  49. content content
  50. result []]
  51. (if (and (seq words) content)
  52. (let [word (first words)
  53. lc-word (string/lower-case word)
  54. lc-content (string/lower-case content)]
  55. (if-let [i (string/index-of lc-content lc-word)]
  56. (recur (rest words)
  57. (subs content (+ i (count word)))
  58. (vec
  59. (concat result
  60. [[:span (subs content 0 i)]
  61. [:mark.p-0.rounded-none (subs content i (+ i (count word)))]])))
  62. (recur nil
  63. content
  64. result)))
  65. (conj result [:span content])))]
  66. [:p {:class "m-0"} elements]))))))
  67. (rum/defc search-result-item
  68. [type content]
  69. [:.text-sm.font-medium.flex.items-baseline
  70. [:.text-xs.rounded.border.mr-2.px-1 {:title type}
  71. (get type 0)]
  72. content])
  73. (rum/defc block-search-result-item
  74. [repo uuid format content q search-mode]
  75. [:div [(when (not= search-mode :page)
  76. [:div {:class "mb-1" :key "parents"} (block/block-parents {:id "block-search-block-parent"
  77. :block? true
  78. :search? true}
  79. repo
  80. (clojure.core/uuid uuid)
  81. {:indent? false})])
  82. [:div {:class "font-medium" :key "content"} (highlight-exact-query content q)]]])
  83. (defonce search-timeout (atom nil))
  84. (rum/defc search-auto-complete
  85. [{:keys [pages files blocks has-more?] :as result} search-q all?]
  86. (rum/with-context [[t] i18n/*tongue-context*]
  87. (let [pages (when-not all? (map (fn [page]
  88. (let [alias (model/get-redirect-page-name page)]
  89. (cond->
  90. {:type :page
  91. :data page}
  92. (and alias
  93. (not= (util/page-name-sanity-lc page)
  94. (util/page-name-sanity-lc alias)))
  95. (assoc :alias alias))))
  96. (remove nil? pages)))
  97. files (when-not all? (map (fn [file] {:type :file :data file}) files))
  98. blocks (map (fn [block] {:type :block :data block}) blocks)
  99. search-mode (state/sub :search/mode)
  100. new-page (if (or
  101. (and (seq pages)
  102. (= (util/safe-search-normalize search-q)
  103. (util/safe-search-normalize (:data (first pages)))))
  104. (nil? result)
  105. all?)
  106. []
  107. [{:type :new-page}])
  108. result (if config/publishing?
  109. (concat pages files blocks)
  110. (concat new-page pages files blocks))
  111. result (if (= search-mode :graph)
  112. [{:type :graph-add-filter}]
  113. result)
  114. repo (state/get-current-repo)]
  115. [:div
  116. (ui/auto-complete
  117. result
  118. {:class "search-results"
  119. :on-chosen (fn [{:keys [type data alias]}]
  120. (search-handler/add-search-to-recent! repo search-q)
  121. (search-handler/clear-search!)
  122. (case type
  123. :graph-add-filter
  124. (state/add-graph-search-filter! search-q)
  125. :new-page
  126. (page-handler/create! search-q)
  127. :page
  128. (let [data (or alias data)]
  129. (route/redirect-to-page! data))
  130. :file
  131. (route/redirect! {:to :file
  132. :path-params {:path data}})
  133. :block
  134. (let [block-uuid (uuid (:block/uuid data))
  135. collapsed? (db/parents-collapsed? (state/get-current-repo) block-uuid)
  136. page (:block/name (:block/page (db/entity [:block/uuid block-uuid])))]
  137. (if page
  138. (if collapsed?
  139. (route/redirect-to-page! block-uuid)
  140. (route/redirect-to-page! page (str "ls-block-" (:block/uuid data))))
  141. ;; search indice outdated
  142. (println "[Error] Block page missing: "
  143. {:block-id block-uuid
  144. :block (db/pull [:block/uuid block-uuid])})))
  145. nil)
  146. (state/close-modal!))
  147. :on-shift-chosen (fn [{:keys [type data alias]}]
  148. (search-handler/add-search-to-recent! repo search-q)
  149. (case type
  150. :page
  151. (let [data (or alias data)
  152. page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))]
  153. (when page
  154. (state/sidebar-add-block!
  155. (state/get-current-repo)
  156. (:db/id page)
  157. :page
  158. {:page page})))
  159. :block
  160. (let [block-uuid (uuid (:block/uuid data))
  161. block (db/entity [:block/uuid block-uuid])]
  162. (state/sidebar-add-block!
  163. (state/get-current-repo)
  164. (:db/id block)
  165. :block
  166. block))
  167. :new-page
  168. (page-handler/create! search-q)
  169. :file
  170. (route/redirect! {:to :file
  171. :path-params {:path data}})
  172. nil)
  173. (state/close-modal!))
  174. :item-render (fn [{:keys [type data alias]}]
  175. (let [search-mode (state/get-search-mode)
  176. data (if (string? data) (pdf-assets/fix-local-asset-filename data) data)]
  177. [:div {:class "py-2"} (case type
  178. :graph-add-filter
  179. [:b search-q]
  180. :new-page
  181. [:div.text.font-bold (str (t :new-page) ": ")
  182. [:span.ml-1 (str "\"" search-q "\"")]]
  183. :page
  184. [:span {:data-page-ref data}
  185. (when alias
  186. (let [target-original-name (model/get-page-original-name alias)]
  187. [:span.mr-2.text-sm.font-medium.mb-2 (str "Alias -> " target-original-name)]))
  188. (search-result-item "Page" (highlight-exact-query data search-q))]
  189. :file
  190. (search-result-item "File" (highlight-exact-query data search-q))
  191. :block
  192. (let [{:block/keys [page content uuid]} data
  193. page (util/get-page-original-name page)
  194. repo (state/sub :git/current-repo)
  195. format (db/get-page-format page)]
  196. [:span {:data-block-ref uuid}
  197. (search-result-item "Block"
  198. (block-search-result-item repo uuid format content search-q search-mode))])
  199. nil)]))})
  200. (when (and has-more? (util/electron?) (not all?))
  201. [:div.px-2.py-4.search-more
  202. [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
  203. :on-click (fn []
  204. (when-not (string/blank? search-q)
  205. (search-handler/search (state/get-current-repo) search-q {:limit 1000
  206. :more? true})
  207. (search-handler/clear-search!)))}
  208. (t :more)]])])))
  209. (rum/defc recent-search-and-pages
  210. [in-page-search?]
  211. [:div.recent-search
  212. [:div.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items
  213. [:div "Recent search:"]
  214. (ui/with-shortcut :go/search-in-page "bottom"
  215. [:div.flex-row.flex.align-items
  216. [:div.mr-2 "Search in page:"]
  217. [:div {:style {:margin-top 3}}
  218. (ui/toggle in-page-search?
  219. (fn [_value]
  220. (state/set-search-mode! (if in-page-search? :global :page)))
  221. true)]
  222. (ui/tippy {:html [:div
  223. ;; TODO: fetch from config
  224. "Tip: " [:code (util/->platform-shortcut "Ctrl + Shift + p")] " to open the commands palette"]
  225. :interactive true
  226. :arrow true
  227. :theme "monospace"}
  228. [:a.inline-block.fade-link
  229. {:style {:margin-left 12}
  230. :on-click #(state/toggle! :ui/command-palette-open?)}
  231. (ui/icon "command" {:style {:font-size 20}})])])]
  232. (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
  233. pages (->> (db/get-key-value :recent/pages)
  234. (remove nil?)
  235. (filter string?)
  236. (remove #(= (string/lower-case %) "contents"))
  237. (mapv (fn [page] {:type :page :data page})))
  238. result (concat (take 5 recent-search) pages)]
  239. (ui/auto-complete
  240. result
  241. {:on-chosen (fn [{:keys [type data]}]
  242. (case type
  243. :page
  244. (route/redirect-to-page! data)
  245. :search
  246. (let [q data]
  247. (state/set-q! q)
  248. (let [search-mode (state/get-search-mode)
  249. opts (if (= :page search-mode)
  250. (let [current-page (or (state/get-current-page)
  251. (date/today))]
  252. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  253. {})]
  254. (if (= :page search-mode)
  255. (search-handler/search (state/get-current-repo) q opts)
  256. (search-handler/search (state/get-current-repo) q))))
  257. nil))
  258. :on-shift-chosen (fn [{:keys [type data]}]
  259. (case type
  260. :page
  261. (let [page data]
  262. (when (string? page)
  263. (when-let [page (db/pull [:block/name (util/page-name-sanity-lc page)])]
  264. (state/sidebar-add-block!
  265. (state/get-current-repo)
  266. (:db/id page)
  267. :page
  268. {:page page}))))
  269. nil))
  270. :item-render (fn [{:keys [type data]}]
  271. (case type
  272. :search [:div.flex-row.flex.search-item.font-medium
  273. svg/search
  274. [:span.ml-2 data]]
  275. :page (let [original-name (model/get-page-original-name data)]
  276. (search-result-item "Page" original-name))
  277. nil))}))])
  278. (rum/defcs search-modal < rum/reactive
  279. (shortcut/disable-all-shortcuts)
  280. (mixins/event-mixin
  281. (fn [state]
  282. (mixins/hide-when-esc-or-outside
  283. state
  284. :on-hide (fn []
  285. (search-handler/clear-search!)))))
  286. [state]
  287. (let [search-result (state/sub :search/result)
  288. search-q (state/sub :search/q)
  289. blocks-count (or (db/blocks-count) 0)
  290. search-mode (state/sub :search/mode)
  291. timeout (cond
  292. (> blocks-count 2000)
  293. 400
  294. :else
  295. 300)
  296. in-page-search? (= search-mode :page)]
  297. (rum/with-context [[t] i18n/*tongue-context*]
  298. (let [input (::input state)]
  299. [:div.cp__palette.cp__palette-main
  300. (when (mobile-util/is-native-platform?)
  301. {:style {:min-height "50vh"}})
  302. [:div.input-wrap
  303. [:input.cp__palette-input.w-full
  304. {:type "text"
  305. :auto-focus true
  306. :placeholder (case search-mode
  307. :graph
  308. (t :graph-search)
  309. :page
  310. (t :page-search)
  311. (t :search))
  312. :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
  313. :value search-q
  314. :on-change (fn [e]
  315. (when @search-timeout
  316. (js/clearTimeout @search-timeout))
  317. (let [value (util/evalue e)
  318. is-composing? (util/onchange-event-is-composing? e)] ;; #3199
  319. (if (and (string/blank? value) (not is-composing?))
  320. (search-handler/clear-search! false)
  321. (let [search-mode (state/get-search-mode)
  322. opts (if (= :page search-mode)
  323. (when-let [current-page (or (state/get-current-page)
  324. (date/today))]
  325. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  326. {})]
  327. (state/set-q! value)
  328. (reset! search-timeout
  329. (js/setTimeout
  330. (fn []
  331. (if (= :page search-mode)
  332. (search-handler/search (state/get-current-repo) value opts)
  333. (search-handler/search (state/get-current-repo) value)))
  334. timeout))))))}]]
  335. [:div.search-results-wrap
  336. (if (seq search-result)
  337. (search-auto-complete search-result search-q false)
  338. (recent-search-and-pages in-page-search?))]]))))
  339. (rum/defc more < rum/reactive
  340. [route]
  341. (let [search-q (get-in route [:path-params :q])
  342. search-result (state/sub :search/more-result)]
  343. (rum/with-context [[t] i18n/*tongue-context*]
  344. [:div#search.flex-1.flex
  345. [:div.inner
  346. [:h1.title (t :search/result-for) [:i search-q]]
  347. [:p.font-medium.tx-sm (str (count (:blocks search-result)) " " (t :search/items))]
  348. [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
  349. (when-not (string/blank? search-q)
  350. (search-auto-complete search-result search-q true))]]])))