search.cljs 17 KB

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