| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- (ns frontend.components.icon
- (:require ["@emoji-mart/data" :as emoji-data]
- ["emoji-mart" :refer [SearchIndex]]
- [promesa.core :as p]
- [cljs-bean.core :as bean]
- [camel-snake-kebab.core :as csk]
- [clojure.string :as string]
- [frontend.search :as search]
- [frontend.storage :as storage]
- [rum.core :as rum]
- [frontend.ui :as ui]
- [logseq.shui.ui :as shui]
- [frontend.util :as util]
- [goog.object :as gobj]
- [goog.functions :refer [debounce]]
- [frontend.config :as config]
- [frontend.handler.property.util :as pu]))
- (defn icon
- [icon & [opts]]
- (cond
- (and (= :emoji (:type icon)) (:id icon))
- [:em-emoji (merge {:id (:id icon)}
- opts)]
- (and (= :tabler-icon (:type icon)) (:id icon))
- (ui/icon (:id icon) opts)))
- (defn get-page-icon
- [page-entity opts]
- (let [default-icon (ui/icon "page" (merge opts {:extension? true}))
- page-icon (pu/get-block-property-value page-entity :icon)]
- (or
- (when-not (string/blank? page-icon)
- (icon page-icon opts))
- default-icon)))
- (defn- search-emojis
- [q]
- (p/let [result (.search SearchIndex q)]
- (bean/->clj result)))
- (defonce *tabler-icons (atom nil))
- (defn- get-tabler-icons
- []
- (if @*tabler-icons
- @*tabler-icons
- (let [result (->> (keys (bean/->clj js/tablerIcons))
- (map (fn [k]
- (-> (string/replace (csk/->Camel_Snake_Case (name k)) "_" " ")
- (string/replace-first "Icon " ""))))
- ;; FIXME: somehow those icons don't work
- (remove #{"Ab" "Ab 2" "Ab Off"}))]
- (reset! *tabler-icons result)
- result)))
- (defonce emojis
- (vals (bean/->clj (gobj/get emoji-data "emojis"))))
- (defn- search-tabler-icons
- [q]
- (search/fuzzy-search (get-tabler-icons) q :limit 100))
- (defn- search
- [q tab]
- (p/let [icons (when (not= tab :emoji) (search-tabler-icons q))
- emojis (when (not= tab :icon) (search-emojis q))]
- {:icons icons
- :emojis emojis}))
- (rum/defc pane-block
- [label items]
- [:div.pane-block
- [:div.hd.px-1.pb-1.leading-none
- [:strong.text-xs.font-medium.text-gray-07.dark:opacity-80 label]]
- [:div.its items]])
- (rum/defc emoji-cp < rum/static
- [{:keys [id name] :as emoji} {:keys [on-chosen hover]}]
- [:button.text-2xl.w-9.h-9.transition-opacity
- (cond->
- {:tabIndex "0"
- :title name
- :on-click (fn [e]
- (on-chosen e (assoc emoji :type :emoji)))}
- (not (nil? hover))
- (assoc :on-mouse-over #(reset! hover emoji)
- :on-mouse-out #()))
- [:em-emoji {:id id}]])
- (rum/defc emojis-cp < rum/static
- [emojis opts]
- (pane-block
- (util/format "Emojis (%s)" (count emojis))
- (for [emoji emojis]
- (rum/with-key (emoji-cp emoji opts) (:id emoji)))))
- (rum/defc icon-cp < rum/static
- [icon {:keys [on-chosen hover]}]
- [:button.w-9.h-9.transition-opacity
- {:key icon
- :tabIndex "0"
- :title icon
- :on-click (fn [e]
- (on-chosen e {:type :tabler-icon
- :id icon
- :name icon}))
- :on-mouse-over #(reset! hover {:type :tabler-icon
- :id icon
- :name icon
- :icon icon})
- :on-mouse-out #()}
- (ui/icon icon {:size 24})])
- (rum/defc icons-cp < rum/static
- [icons opts]
- (pane-block
- (util/format "Icons (%s)" (count icons))
- (for [icon icons]
- (icon-cp icon opts))))
- (defn get-used-items
- []
- (storage/get :ui/ls-icons-used))
- (defn add-used-item!
- [m]
- (let [s (some->> (or (get-used-items) [])
- (take 24)
- (filter #(not= m %))
- (cons m))]
- (storage/set :ui/ls-icons-used s)))
- (rum/defc all-cp
- [opts]
- (let [used-items (get-used-items)
- emoji-items (take 32 emojis)
- icon-items (take 48 (get-tabler-icons))
- item-cp (fn [d]
- (if (or (string? d)
- (= :tabler-icon (:type d)))
- (icon-cp (if (string? d) d (:id d)) opts)
- (emoji-cp d opts)))]
- [:div.all-pane.pb-10
- (when (count used-items)
- (pane-block "Frequently used"
- (->> used-items (map item-cp))))
- (pane-block (util/format "Emojis (%s)" (count emojis))
- (->> emoji-items (map item-cp)))
- (pane-block (util/format "Icons (%s)" (count (get-tabler-icons)))
- (->> icon-items (map item-cp)))]))
- (rum/defc tab-observer
- [tab {:keys [reset-q!]}]
- (rum/use-effect!
- #(reset-q!)
- [tab])
- nil)
- (rum/defc select-observer
- [*input-ref]
- (let [*el-ref (rum/use-ref nil)
- *items-ref (rum/use-ref [])
- *current-ref (rum/use-ref [-1])
- set-current! (fn [idx node] (set! (. *current-ref -current) [idx node]))
- get-cnt #(some-> (rum/deref *el-ref) (.closest ".cp__emoji-icon-picker"))
- focus! (fn [idx dir]
- (let [items (rum/deref *items-ref)
- ^js popup (some-> (get-cnt) (.-parentNode))
- idx (loop [n idx]
- (if (false? (nth items n nil))
- (recur (+ n (if (= dir :prev) -1 1))) n))]
- (if-let [node (nth items idx nil)]
- (do (.focus node #js {:preventScroll true :focusVisible true})
- (.scrollIntoView node #js {:block "center"})
- (when popup (set! (. popup -scrollTop) 0))
- (set-current! idx node))
- (do (.focus (rum/deref *input-ref)) (set-current! -1 nil)))))
- down-handler!
- (rum/use-callback
- (fn [^js e]
- (let []
- (if (= 13 (.-keyCode e))
- ;; enter
- (some-> (second (rum/deref *current-ref)) (.click))
- (let [[idx _node] (rum/deref *current-ref)]
- (case (.-keyCode e)
- ;;left
- 37 (focus! (dec idx) :prev)
- ;; tab & right
- (9 39) (focus! (inc idx) :next)
- ;; up
- 38 (do (focus! (- idx 9) :prev) (util/stop e))
- ;; down
- 40 (do (focus! (+ idx 9) :next) (util/stop e))
- :dune))))) [])]
- (rum/use-effect!
- (fn []
- ;; calculate items
- (let [^js blocks (.querySelectorAll (get-cnt) ".pane-block")
- items (map #(some-> (.querySelectorAll % ".its > button") (js/Array.from) (js->clj)) blocks)
- step 9
- items (map #(let [count (count %)
- m (mod count step)]
- (if (> m 0) (concat % (repeat (- step m) false)) %)) items)]
- (set! (. *items-ref -current) (flatten items))
- (focus! 0 :next))
- ;; handlers
- (let [^js cnt (get-cnt)]
- (.addEventListener cnt "keydown" down-handler! false)
- #(.removeEventListener cnt "keydown" down-handler!)))
- [])
- [:span.absolute.hidden {:ref *el-ref}]))
- (rum/defcs ^:large-vars/cleanup-todo icon-search <
- (rum/local "" ::q)
- (rum/local nil ::result)
- (rum/local false ::select-mode?)
- (rum/local :all ::tab)
- (rum/local nil ::hover)
- [state {:keys [on-chosen] :as opts}]
- (let [*q (::q state)
- *result (::result state)
- *tab (::tab state)
- *hover (::hover state)
- *input-ref (rum/create-ref)
- *result-ref (rum/create-ref)
- result @*result
- opts (assoc opts :hover *hover
- :on-chosen (fn [e m]
- (and on-chosen (on-chosen e m))
- (when (:type m) (add-used-item! m))))
- *select-mode? (::select-mode? state)
- reset-q! #(when-let [^js input (rum/deref *input-ref)]
- (reset! *q "")
- (reset! *result {})
- (reset! *select-mode? false)
- (set! (. input -value) "")
- (js/setTimeout
- (fn [] (.focus input)
- (util/scroll-to (rum/deref *result-ref) 0 false))
- 64))]
- [:div.cp__emoji-icon-picker
- ;; header
- [:div.hd
- (tab-observer @*tab {:reset-q! reset-q!})
- (when @*select-mode?
- (select-observer *input-ref))
- [:div.search-input
- (shui/tabler-icon "search" {:size 16})
- [:input.form-input
- {:auto-focus true
- :ref *input-ref
- :placeholder (util/format "Search %s items" (string/lower-case (name @*tab)))
- :default-value ""
- :on-focus #(reset! *select-mode? false)
- :on-key-down (fn [^js e]
- (case (.-keyCode e)
- ;; esc
- 27 (do (util/stop e)
- (if (string/blank? @*q)
- (some-> (rum/deref *input-ref) (.blur))
- (reset-q!)))
- 38 (do (util/stop e))
- (9 40) (do
- (reset! *select-mode? true)
- (util/stop e))
- :dune))
- :on-change (debounce
- (fn [e]
- (reset! *q (util/evalue e))
- (reset! *select-mode? false)
- (if (string/blank? @*q)
- (reset! *result {})
- (p/let [result (search @*q @*tab)]
- (reset! *result result))))
- 200)}]
- (when-not (string/blank? @*q)
- [:a.x {:on-click reset-q!} (shui/tabler-icon "x" {:size 14})])]]
- ;; body
- [:div.bd
- {:ref *result-ref
- :on-mouse-leave #(reset! *hover nil)}
- [:div.search-result
- (if (seq result)
- [:div.flex.flex-1.flex-col.gap-1
- (when (seq (:emojis result))
- (emojis-cp (:emojis result) opts))
- (when (seq (:icons result))
- (icons-cp (:icons result) opts))]
- [:div.flex.flex-1.flex-col.gap-1
- (case @*tab
- :emoji (emojis-cp emojis opts)
- :icon (icons-cp (get-tabler-icons) opts)
- (all-cp opts))])]]
- ;; footer
- [:div.ft
- (if-not @*hover
- ;; tabs
- [:div.flex.flex-1.flex-row.items-center.gap-2
- (let [tabs [[:all "All"] [:emoji "Emojis"] [:icon "Icons"]]]
- (for [[id label] tabs
- :let [active? (= @*tab id)]]
- (shui/button
- {:variant :ghost
- :size :sm
- :class (util/classnames [{:active active?} "tab-item"])
- :on-click #(reset! *tab id)}
- label)))]
- ;; preview
- [:div.hover-preview
- [:strong (:name @*hover)]
- [:button
- {:style {:font-size 30}
- :key (:id @*hover)
- :title (:name @*hover)}
- (if (= :tabler-icon (:type @*hover))
- (ui/icon (:icon @*hover) {:size 32})
- (:native (first (:skins @*hover))))]])]]))
- (rum/defc icon-picker
- [icon-value {:keys [disabled? on-chosen icon-props]}]
- (let [content-fn
- (if config/publishing?
- (constantly [])
- (fn [{:keys [id]}]
- (icon-search
- {:on-chosen (fn [e icon-value]
- (on-chosen e icon-value)
- (shui/popup-hide! id))})))]
- ;; trigger
- (let [has-icon? (not (nil? icon-value))]
- (shui/button
- {:variant (if has-icon? :ghost :text)
- :size :sm
- :class (if has-icon? "px-1 leading-none" "font-normal text-sm px-[0.5px] opacity-50")
- :on-click #(when-not disabled?
- (shui/popup-show! (.-target %) content-fn
- {:as-menu? true
- :content-props {:class "w-auto"}}))}
- (if has-icon?
- (icon icon-value (merge {:size 18} icon-props))
- "Empty")))))
|