icon.cljs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. (ns frontend.components.icon
  2. (:require ["@emoji-mart/data" :as emoji-data]
  3. ["emoji-mart" :refer [SearchIndex]]
  4. [promesa.core :as p]
  5. [cljs-bean.core :as bean]
  6. [camel-snake-kebab.core :as csk]
  7. [clojure.string :as string]
  8. [frontend.search :as search]
  9. [frontend.storage :as storage]
  10. [rum.core :as rum]
  11. [frontend.ui :as ui]
  12. [logseq.shui.ui :as shui]
  13. [frontend.util :as util]
  14. [goog.object :as gobj]
  15. [goog.functions :refer [debounce]]
  16. [frontend.config :as config]
  17. [frontend.handler.property.util :as pu]))
  18. (defn icon
  19. [icon & [opts]]
  20. (cond
  21. (and (= :emoji (:type icon)) (:id icon))
  22. [:em-emoji (merge {:id (:id icon)}
  23. opts)]
  24. (and (= :tabler-icon (:type icon)) (:id icon))
  25. (ui/icon (:id icon) opts)))
  26. (defn get-page-icon
  27. [page-entity opts]
  28. (let [default-icon (ui/icon "page" (merge opts {:extension? true}))
  29. page-icon (pu/get-block-property-value page-entity :icon)]
  30. (or
  31. (when-not (string/blank? page-icon)
  32. (icon page-icon opts))
  33. default-icon)))
  34. (defn- search-emojis
  35. [q]
  36. (p/let [result (.search SearchIndex q)]
  37. (bean/->clj result)))
  38. (defonce *tabler-icons (atom nil))
  39. (defn- get-tabler-icons
  40. []
  41. (if @*tabler-icons
  42. @*tabler-icons
  43. (let [result (->> (keys (bean/->clj js/tablerIcons))
  44. (map (fn [k]
  45. (-> (string/replace (csk/->Camel_Snake_Case (name k)) "_" " ")
  46. (string/replace-first "Icon " ""))))
  47. ;; FIXME: somehow those icons don't work
  48. (remove #{"Ab" "Ab 2" "Ab Off"}))]
  49. (reset! *tabler-icons result)
  50. result)))
  51. (defonce emojis
  52. (vals (bean/->clj (gobj/get emoji-data "emojis"))))
  53. (defn- search-tabler-icons
  54. [q]
  55. (search/fuzzy-search (get-tabler-icons) q :limit 100))
  56. (defn- search
  57. [q tab]
  58. (p/let [icons (when (not= tab :emoji) (search-tabler-icons q))
  59. emojis (when (not= tab :icon) (search-emojis q))]
  60. {:icons icons
  61. :emojis emojis}))
  62. (rum/defc pane-block
  63. [label items]
  64. [:div.pane-block
  65. [:div.hd.px-1.pb-1.leading-none
  66. [:strong.text-xs.font-medium.text-gray-07.dark:opacity-80 label]]
  67. [:div.its items]])
  68. (rum/defc emoji-cp < rum/static
  69. [{:keys [id name] :as emoji} {:keys [on-chosen hover]}]
  70. [:button.text-2xl.w-9.h-9.transition-opacity
  71. (cond->
  72. {:tabIndex "0"
  73. :title name
  74. :on-click (fn [e]
  75. (on-chosen e (assoc emoji :type :emoji)))}
  76. (not (nil? hover))
  77. (assoc :on-mouse-over #(reset! hover emoji)
  78. :on-mouse-out #()))
  79. [:em-emoji {:id id}]])
  80. (rum/defc emojis-cp < rum/static
  81. [emojis opts]
  82. (pane-block
  83. (util/format "Emojis (%s)" (count emojis))
  84. (for [emoji emojis]
  85. (rum/with-key (emoji-cp emoji opts) (:id emoji)))))
  86. (rum/defc icon-cp < rum/static
  87. [icon {:keys [on-chosen hover]}]
  88. [:button.w-9.h-9.transition-opacity
  89. {:key icon
  90. :tabIndex "0"
  91. :title icon
  92. :on-click (fn [e]
  93. (on-chosen e {:type :tabler-icon
  94. :id icon
  95. :name icon}))
  96. :on-mouse-over #(reset! hover {:type :tabler-icon
  97. :id icon
  98. :name icon
  99. :icon icon})
  100. :on-mouse-out #()}
  101. (ui/icon icon {:size 24})])
  102. (rum/defc icons-cp < rum/static
  103. [icons opts]
  104. (pane-block
  105. (util/format "Icons (%s)" (count icons))
  106. (for [icon icons]
  107. (icon-cp icon opts))))
  108. (defn get-used-items
  109. []
  110. (storage/get :ui/ls-icons-used))
  111. (defn add-used-item!
  112. [m]
  113. (let [s (some->> (or (get-used-items) [])
  114. (take 24)
  115. (filter #(not= m %))
  116. (cons m))]
  117. (storage/set :ui/ls-icons-used s)))
  118. (rum/defc all-cp
  119. [opts]
  120. (let [used-items (get-used-items)
  121. emoji-items (take 32 emojis)
  122. icon-items (take 48 (get-tabler-icons))
  123. item-cp (fn [d]
  124. (if (or (string? d)
  125. (= :tabler-icon (:type d)))
  126. (icon-cp (if (string? d) d (:id d)) opts)
  127. (emoji-cp d opts)))]
  128. [:div.all-pane.pb-10
  129. (when (count used-items)
  130. (pane-block "Frequently used"
  131. (->> used-items (map item-cp))))
  132. (pane-block (util/format "Emojis (%s)" (count emojis))
  133. (->> emoji-items (map item-cp)))
  134. (pane-block (util/format "Icons (%s)" (count (get-tabler-icons)))
  135. (->> icon-items (map item-cp)))]))
  136. (rum/defc tab-observer
  137. [tab {:keys [reset-q!]}]
  138. (rum/use-effect!
  139. #(reset-q!)
  140. [tab])
  141. nil)
  142. (rum/defc select-observer
  143. [*input-ref]
  144. (let [*el-ref (rum/use-ref nil)
  145. *items-ref (rum/use-ref [])
  146. *current-ref (rum/use-ref [-1])
  147. set-current! (fn [idx node] (set! (. *current-ref -current) [idx node]))
  148. get-cnt #(some-> (rum/deref *el-ref) (.closest ".cp__emoji-icon-picker"))
  149. focus! (fn [idx dir]
  150. (let [items (rum/deref *items-ref)
  151. ^js popup (some-> (get-cnt) (.-parentNode))
  152. idx (loop [n idx]
  153. (if (false? (nth items n nil))
  154. (recur (+ n (if (= dir :prev) -1 1))) n))]
  155. (if-let [node (nth items idx nil)]
  156. (do (.focus node #js {:preventScroll true :focusVisible true})
  157. (.scrollIntoView node #js {:block "center"})
  158. (when popup (set! (. popup -scrollTop) 0))
  159. (set-current! idx node))
  160. (do (.focus (rum/deref *input-ref)) (set-current! -1 nil)))))
  161. down-handler!
  162. (rum/use-callback
  163. (fn [^js e]
  164. (let []
  165. (if (= 13 (.-keyCode e))
  166. ;; enter
  167. (some-> (second (rum/deref *current-ref)) (.click))
  168. (let [[idx _node] (rum/deref *current-ref)]
  169. (case (.-keyCode e)
  170. ;;left
  171. 37 (focus! (dec idx) :prev)
  172. ;; tab & right
  173. (9 39) (focus! (inc idx) :next)
  174. ;; up
  175. 38 (do (focus! (- idx 9) :prev) (util/stop e))
  176. ;; down
  177. 40 (do (focus! (+ idx 9) :next) (util/stop e))
  178. :dune))))) [])]
  179. (rum/use-effect!
  180. (fn []
  181. ;; calculate items
  182. (let [^js blocks (.querySelectorAll (get-cnt) ".pane-block")
  183. items (map #(some-> (.querySelectorAll % ".its > button") (js/Array.from) (js->clj)) blocks)
  184. step 9
  185. items (map #(let [count (count %)
  186. m (mod count step)]
  187. (if (> m 0) (concat % (repeat (- step m) false)) %)) items)]
  188. (set! (. *items-ref -current) (flatten items))
  189. (focus! 0 :next))
  190. ;; handlers
  191. (let [^js cnt (get-cnt)]
  192. (.addEventListener cnt "keydown" down-handler! false)
  193. #(.removeEventListener cnt "keydown" down-handler!)))
  194. [])
  195. [:span.absolute.hidden {:ref *el-ref}]))
  196. (rum/defcs ^:large-vars/cleanup-todo icon-search <
  197. (rum/local "" ::q)
  198. (rum/local nil ::result)
  199. (rum/local false ::select-mode?)
  200. (rum/local :all ::tab)
  201. (rum/local nil ::hover)
  202. [state {:keys [on-chosen] :as opts}]
  203. (let [*q (::q state)
  204. *result (::result state)
  205. *tab (::tab state)
  206. *hover (::hover state)
  207. *input-ref (rum/create-ref)
  208. *result-ref (rum/create-ref)
  209. result @*result
  210. opts (assoc opts :hover *hover
  211. :on-chosen (fn [e m]
  212. (and on-chosen (on-chosen e m))
  213. (when (:type m) (add-used-item! m))))
  214. *select-mode? (::select-mode? state)
  215. reset-q! #(when-let [^js input (rum/deref *input-ref)]
  216. (reset! *q "")
  217. (reset! *result {})
  218. (reset! *select-mode? false)
  219. (set! (. input -value) "")
  220. (js/setTimeout
  221. (fn [] (.focus input)
  222. (util/scroll-to (rum/deref *result-ref) 0 false))
  223. 64))]
  224. [:div.cp__emoji-icon-picker
  225. ;; header
  226. [:div.hd
  227. (tab-observer @*tab {:reset-q! reset-q!})
  228. (when @*select-mode?
  229. (select-observer *input-ref))
  230. [:div.search-input
  231. (shui/tabler-icon "search" {:size 16})
  232. [:input.form-input
  233. {:auto-focus true
  234. :ref *input-ref
  235. :placeholder (util/format "Search %s items" (string/lower-case (name @*tab)))
  236. :default-value ""
  237. :on-focus #(reset! *select-mode? false)
  238. :on-key-down (fn [^js e]
  239. (case (.-keyCode e)
  240. ;; esc
  241. 27 (do (util/stop e)
  242. (if (string/blank? @*q)
  243. (some-> (rum/deref *input-ref) (.blur))
  244. (reset-q!)))
  245. 38 (do (util/stop e))
  246. (9 40) (do
  247. (reset! *select-mode? true)
  248. (util/stop e))
  249. :dune))
  250. :on-change (debounce
  251. (fn [e]
  252. (reset! *q (util/evalue e))
  253. (reset! *select-mode? false)
  254. (if (string/blank? @*q)
  255. (reset! *result {})
  256. (p/let [result (search @*q @*tab)]
  257. (reset! *result result))))
  258. 200)}]
  259. (when-not (string/blank? @*q)
  260. [:a.x {:on-click reset-q!} (shui/tabler-icon "x" {:size 14})])]]
  261. ;; body
  262. [:div.bd
  263. {:ref *result-ref
  264. :on-mouse-leave #(reset! *hover nil)}
  265. [:div.search-result
  266. (if (seq result)
  267. [:div.flex.flex-1.flex-col.gap-1
  268. (when (seq (:emojis result))
  269. (emojis-cp (:emojis result) opts))
  270. (when (seq (:icons result))
  271. (icons-cp (:icons result) opts))]
  272. [:div.flex.flex-1.flex-col.gap-1
  273. (case @*tab
  274. :emoji (emojis-cp emojis opts)
  275. :icon (icons-cp (get-tabler-icons) opts)
  276. (all-cp opts))])]]
  277. ;; footer
  278. [:div.ft
  279. (if-not @*hover
  280. ;; tabs
  281. [:div.flex.flex-1.flex-row.items-center.gap-2
  282. (let [tabs [[:all "All"] [:emoji "Emojis"] [:icon "Icons"]]]
  283. (for [[id label] tabs
  284. :let [active? (= @*tab id)]]
  285. (shui/button
  286. {:variant :ghost
  287. :size :sm
  288. :class (util/classnames [{:active active?} "tab-item"])
  289. :on-click #(reset! *tab id)}
  290. label)))]
  291. ;; preview
  292. [:div.hover-preview
  293. [:strong (:name @*hover)]
  294. [:button
  295. {:style {:font-size 30}
  296. :key (:id @*hover)
  297. :title (:name @*hover)}
  298. (if (= :tabler-icon (:type @*hover))
  299. (ui/icon (:icon @*hover) {:size 32})
  300. (:native (first (:skins @*hover))))]])]]))
  301. (rum/defc icon-picker
  302. [icon-value {:keys [disabled? on-chosen icon-props]}]
  303. (let [content-fn
  304. (if config/publishing?
  305. (constantly [])
  306. (fn [{:keys [id]}]
  307. (icon-search
  308. {:on-chosen (fn [e icon-value]
  309. (on-chosen e icon-value)
  310. (shui/popup-hide! id))})))]
  311. ;; trigger
  312. (let [has-icon? (not (nil? icon-value))]
  313. (shui/button
  314. {:variant (if has-icon? :ghost :text)
  315. :size :sm
  316. :class (if has-icon? "px-1 leading-none" "font-normal text-sm px-[0.5px] opacity-50")
  317. :on-click #(when-not disabled?
  318. (shui/popup-show! (.-target %) content-fn
  319. {:as-menu? true
  320. :content-props {:class "w-auto"}}))}
  321. (if has-icon?
  322. (icon icon-value (merge {:size 18} icon-props))
  323. "Empty")))))