| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710 |
- (ns frontend.ui
- (:require [clojure.string :as string]
- [frontend.components.svg :as svg]
- [frontend.context.i18n :as i18n]
- [frontend.handler.notification :as notification-handler]
- [frontend.mixins :as mixins]
- [frontend.modules.shortcut.core :as shortcut]
- [frontend.rum :as r]
- [frontend.state :as state]
- [frontend.ui.date-picker]
- [frontend.util :as util]
- [goog.dom :as gdom]
- [goog.object :as gobj]
- [lambdaisland.glogi :as log]
- [medley.core :as medley]
- ["react-resize-context" :as Resize]
- ["react-textarea-autosize" :as TextareaAutosize]
- ["react-tippy" :as react-tippy]
- ["react-transition-group" :refer [CSSTransition TransitionGroup]]
- ["react-tweet-embed" :as react-tweet-embed]
- [rum.core :as rum]))
- (defonce transition-group (r/adapt-class TransitionGroup))
- (defonce css-transition (r/adapt-class CSSTransition))
- (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
- (def resize-provider (r/adapt-class (gobj/get Resize "ResizeProvider")))
- (def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
- (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
- (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
- (rum/defc ls-textarea < rum/reactive
- [{:keys [on-change] :as props}]
- (let [skip-composition? (or
- (state/sub :editor/show-page-search?)
- (state/sub :editor/show-block-search?)
- (state/sub :editor/show-template-search?))
- on-composition (fn [e]
- (if skip-composition?
- (on-change e)
- (case e.type
- "compositionend" (do
- (state/set-editor-in-composition! false)
- (on-change e))
- (state/set-editor-in-composition! true))))
- props (assoc props
- :on-change (fn [e] (when-not (state/editor-in-composition?)
- (on-change e)))
- :on-composition-start on-composition
- :on-composition-update on-composition
- :on-composition-end on-composition)]
- (textarea props)))
- (rum/defc dropdown-content-wrapper [state content class]
- (let [class (or class
- (util/hiccup->class "origin-top-right.absolute.right-0.mt-2.rounded-md.shadow-lg"))]
- [:div.dropdown-wrapper
- {:class (str class " "
- (case state
- "entering" "transition ease-out duration-100 transform opacity-0 scale-95"
- "entered" "transition ease-out duration-100 transform opacity-100 scale-100"
- "exiting" "transition ease-in duration-75 transform opacity-100 scale-100"
- "exited" "transition ease-in duration-75 transform opacity-0 scale-95"))}
- content]))
- ;; public exports
- (rum/defcs dropdown < (mixins/modal :open?)
- [state content-fn modal-content-fn
- & [{:keys [modal-class z-index]
- :or {z-index 999}
- :as opts}]]
- (let [{:keys [open? toggle-fn]} state
- modal-content (modal-content-fn state)]
- [:div.ml-1.relative {:style {:z-index z-index}}
- (content-fn state)
- (css-transition
- {:in @open? :timeout 0}
- (fn [dropdown-state]
- (when @open?
- (dropdown-content-wrapper dropdown-state modal-content modal-class))))]))
- (rum/defc menu-link
- [options child]
- [:a.block.px-4.py-2.text-sm.transition.ease-in-out.duration-150.cursor.menu-link
- options
- child])
- (rum/defc dropdown-with-links
- [content-fn links {:keys [modal-class links-header links-footer z-index] :as opts}]
- (dropdown
- content-fn
- (fn [{:keys [close-fn] :as state}]
- [:div.py-1.rounded-md.shadow-xs
- (when links-header links-header)
- (for [{:keys [options title icon hr]} (if (fn? links) (links) links)]
- (let [new-options
- (assoc options
- :on-click (fn [e]
- (when-let [on-click-fn (:on-click options)]
- (on-click-fn e))
- (close-fn)))
- child (if hr
- [:hr.my-1]
- [:div
- {:style {:display "flex" :flex-direction "row"}}
- [:div {:style {:margin-right "8px"}} title]])]
- (rum/with-key
- (menu-link new-options child)
- title)))
- (when links-footer links-footer)])
- opts))
- (defn button
- [text & {:keys [background href class intent on-click small?]
- :or {small? false}
- :as option}]
- (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700")
- klass (if background (string/replace klass "indigo" background) klass)
- klass (if small? (str klass ".px-2.py-1") klass)]
- (if href
- [:a.ui__button.is-link
- (merge
- {:type "button"
- :class (str (util/hiccup->class klass) " " class)}
- (dissoc option :background :class :small?))
- text]
- [:button.ui__button
- (merge
- {:type "button"
- :class (str (util/hiccup->class klass) " " class)}
- (dissoc option :background :class :small?))
- text])))
- (rum/defc notification-content
- [state content status uid]
- (when (and content status)
- (let [[color-class svg]
- (case status
- :success
- ["text-gray-900 dark:text-gray-300 "
- [:svg.h-6.w-6.text-green-400
- {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
- [:path
- {:d "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
- :stroke-width "2"
- :stroke-linejoin "round"
- :stroke-linecap "round"}]]]
- :warning
- ["text-gray-900 dark:text-gray-300 "
- [:svg.h-6.w-6.text-yellow-500
- {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
- [:path
- {:d "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
- :stroke-width "2"
- :stroke-linejoin "round"
- :stroke-linecap "round"}]]]
- ["text-red-500"
- [:svg.h-6.w-6.text-red-500
- {:view-box "0 0 20 20", :fill "currentColor"}
- [:path
- {:clip-rule "evenodd"
- :d
- "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
- :fill-rule "evenodd"}]]])]
- [:div.ui__notifications-content
- {:style {:z-index (if (or (= state "exiting")
- (= state "exited"))
- -1
- 99)
- :top "3.2em"}}
- [:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
- {:class (case state
- "entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
- "entered" "transition ease-out duration-300 transform translate-y-0 opacity-100 sm:translate-x-0"
- "exiting" "transition ease-in duration-100 opacity-100"
- "exited" "transition ease-in duration-100 opacity-0")}
- [:div.rounded-lg.shadow-xs {:style {:max-height "calc(100vh - 200px)"
- :overflow-y "scroll"
- :overflow-x "hidden"}}
- [:div.p-4
- [:div.flex.items-start
- [:div.flex-shrink-0
- svg]
- [:div.ml-3.w-0.flex-1
- [:div.text-sm.leading-5.font-medium {:style {:margin 0}
- :class color-class}
- content]]
- [:div.ml-4.flex-shrink-0.flex
- [:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150
- {:on-click (fn []
- (notification-handler/clear! uid))}
- [:svg.h-5.w-5
- {:fill "currentColor", :view-Box "0 0 20 20"}
- [:path
- {:clip-rule "evenodd"
- :d
- "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
- :fill-rule "evenodd"}]]]]]]]]])))
- (rum/defc notification < rum/reactive
- []
- (let [contents (state/sub :notification/contents)]
- (transition-group
- {:class-name "notifications ui__notifications"}
- (doall (map (fn [el]
- (let [k (first el)
- v (second el)]
- (css-transition
- {:timeout 100
- :key (name k)}
- (fn [state]
- (notification-content state (:content v) (:status v) k)))))
- contents)))))
- (defn checkbox
- [option]
- [:input.form-checkbox.h-4.w-4.transition.duration-150.ease-in-out
- (merge {:type "checkbox"} option)])
- (defn badge
- [text option]
- [:span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.leading-4.bg-purple-100.text-purple-800
- option
- text])
- ;; scroll
- (defn get-doc-scroll-top []
- (.-scrollTop js/document.documentElement))
- (defn main-node
- []
- (gdom/getElement "main-content"))
- (defn get-scroll-top []
- (.-scrollTop (main-node)))
- (defn get-dynamic-style-node
- []
- (js/document.getElementById "dynamic-style-scope"))
- (defn inject-document-devices-envs!
- []
- (let [cl (.-classList js/document.documentElement)]
- (when util/mac? (.add cl "is-mac"))
- (when util/win32? (.add cl "is-win32"))
- (when (util/electron?) (.add cl "is-electron"))
- (when (util/ios?) (.add cl "is-ios"))
- (when (util/mobile?) (.add cl "is-mobile"))
- (when (util/safari?) (.add cl "is-safari"))
- (when (util/electron?)
- (js/window.apis.on "full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")))))
- (defn inject-dynamic-style-node!
- []
- (let [style (get-dynamic-style-node)]
- (if (nil? style)
- (let [node (js/document.createElement "style")]
- (set! (.-id node) "dynamic-style-scope")
- (.appendChild js/document.head node))
- style)))
- (defn setup-patch-ios-fixed-bottom-position!
- "fix a common issue about ios webpage viewport
- when soft keyboard setup"
- []
- (when (and
- (util/ios?)
- (not (nil? js/window.visualViewport)))
- (let [viewport js/visualViewport
- style (get-dynamic-style-node)
- sheet (.-sheet style)
- raf-pending? (atom false)
- set-raf-pending! #(reset! raf-pending? %)
- handler
- (fn []
- (when-not @raf-pending?
- (let [f (fn []
- (set-raf-pending! false)
- (let [vh (+ (.-offsetTop viewport) (.-height viewport))
- rule (.. sheet -rules (item 0))
- set-top #(set! (.. rule -style -top) (str % "px"))]
- (set-top vh)))]
- (set-raf-pending! true)
- (js/window.requestAnimationFrame f))))]
- (.insertRule sheet ".fix-ios-fixed-bottom {bottom:unset !important; transform: translateY(-100%); top: 100vh;}")
- (.addEventListener viewport "resize" handler)
- (.addEventListener viewport "scroll" handler)
- (fn []
- (.removeEventListener viewport "resize" handler)
- (.removeEventListener viewport "scroll" handler)))))
- (defn setup-system-theme-effect!
- []
- (let [^js schemaMedia (js/window.matchMedia "(prefers-color-scheme: dark)")]
- (.addEventListener schemaMedia "change" state/sync-system-theme!)
- (state/sync-system-theme!)
- #(.removeEventListener schemaMedia "change" state/sync-system-theme!)))
- (defn set-global-active-keystroke [val]
- (.setAttribute js/document.body "data-active-keystroke" val))
- (defn setup-active-keystroke! []
- (let [active-keystroke (atom #{})
- handle-global-keystroke (fn [down? e]
- (let [handler (if down? conj disj)
- keystroke e.key]
- (swap! active-keystroke handler keystroke))
- (set-global-active-keystroke (apply str (interpose "+" (vec @active-keystroke)))))
- keydown-handler (partial handle-global-keystroke true)
- keyup-handler (partial handle-global-keystroke false)
- clear-all #(do (set-global-active-keystroke "")
- (reset! active-keystroke #{}))]
- (.addEventListener js/window "keydown" keydown-handler)
- (.addEventListener js/window "keyup" keyup-handler)
- (.addEventListener js/window "blur" clear-all)
- (.addEventListener js/window "visibilitychange" clear-all)
- (fn []
- (.removeEventListener js/window "keydown" keydown-handler)
- (.removeEventListener js/window "keyup" keyup-handler)
- (.removeEventListener js/window "blur" clear-all)
- (.removeEventListener js/window "visibilitychange" clear-all))))
- (defn on-scroll
- [node on-load on-top-reached]
- (let [full-height (gobj/get node "scrollHeight")
- scroll-top (gobj/get node "scrollTop")
- client-height (gobj/get node "clientHeight")
- bottom-reached? (<= (- full-height scroll-top client-height) 100)
- top-reached? (= scroll-top 0)]
- (when (and bottom-reached? on-load)
- (on-load))
- (when (and top-reached? on-top-reached)
- (on-top-reached))))
- (defn attach-listeners
- "Attach scroll and resize listeners."
- [state]
- (let [list-element-id (first (:rum/args state))
- opts (-> state :rum/args (nth 2))
- node (js/document.getElementById list-element-id)
- debounced-on-scroll (util/debounce 500 #(on-scroll
- node
- (:on-load opts) ; bottom reached
- (:on-top-reached opts)))]
- (mixins/listen state node :scroll debounced-on-scroll)))
- (rum/defcs infinite-list <
- (mixins/event-mixin attach-listeners)
- "Render an infinite list."
- [state list-element-id body {:keys [on-load has-more on-top-reached]}]
- (rum/with-context [[t] i18n/*tongue-context*]
- (rum/fragment
- body
- (when has-more
- [:a.fade-link.text-link.font-bold.text-4xl
- {:on-click on-load}
- (t :page/earlier)]))))
- (rum/defcs auto-complete <
- (rum/local 0 ::current-idx)
- (shortcut/mixin :shortcut.handler/auto-complete)
- [state
- matched
- {:keys [on-chosen
- on-shift-chosen
- get-group-name
- empty-div
- item-render
- class]}]
- (let [current-idx (get state ::current-idx)]
- [:div#ui__ac {:class class}
- (if (seq matched)
- [:div#ui__ac-inner.hide-scrollbar
- (for [[idx item] (medley/indexed matched)]
- [:<>
- {:key idx}
- (let [item-cp
- [:div {:key idx}
- (let [chosen? (= @current-idx idx)]
- (menu-link
- {:id (str "ac-" idx)
- :class (when chosen? "chosen")
- :on-mouse-enter #(reset! current-idx idx)
- :on-mouse-down (fn [e]
- (util/stop e)
- (if (and (gobj/get e "shiftKey") on-shift-chosen)
- (on-shift-chosen item)
- (on-chosen item)))}
- (if item-render (item-render item chosen?) item)))]]
- (if get-group-name
- (if-let [group-name (get-group-name item)]
- [:div
- [:div.ui__ac-group-name group-name]
- item-cp]
- item-cp)
- item-cp))])]
- (when empty-div
- empty-div))]))
- (def datepicker frontend.ui.date-picker/date-picker)
- (defn toggle
- ([on? on-click] (toggle on? on-click false))
- ([on? on-click small?]
- [:a.ui__toggle {:on-click on-click
- :class (if small? "is-small" "")}
- [:span.wrapper.transition-colors.ease-in-out.duration-200
- {:aria-checked (if on? "true" "false"), :tab-index "0", :role "checkbox"
- :class (if on? "bg-indigo-600" "bg-gray-200")}
- [:span.switcher.transform.transition.ease-in-out.duration-200
- {:class (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
- :aria-hidden "true"}]]]))
- ;; `sequence` can be a list of symbols or strings
- (defn keyboard-shortcut [sequence]
- [:div.keyboard-shortcut
- (map-indexed (fn [i key]
- [:code {:key i}
- ;; Display "cmd" rather than "meta" to the user to describe the Mac
- ;; mod key, because that's what the Mac keyboards actually say.
- (if (or (= :meta key) (= "meta" key))
- (util/meta-key-name)
- (name key))])
- sequence)])
- (defonce modal-show? (atom false))
- (rum/defc modal-overlay
- [state close-fn]
- [:div.ui__modal-overlay
- {:class (case state
- "entering" "ease-out duration-300 opacity-0"
- "entered" "ease-out duration-300 opacity-100"
- "exiting" "ease-in duration-200 opacity-100"
- "exited" "ease-in duration-200 opacity-0")
- :on-click close-fn}
- [:div.absolute.inset-0.opacity-75]])
- (rum/defc modal-panel-content <
- mixins/component-editing-mode
- [panel-content close-fn]
- (panel-content close-fn))
- (rum/defc modal-panel
- [show? panel-content transition-state close-fn fullscreen? close-btn?]
- [:div.ui__modal-panel.transform.transition-all.sm:min-w-lg.sm
- {:class (case transition-state
- "entering" "ease-out duration-300 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
- "entered" "ease-out duration-300 opacity-100 translate-y-0 sm:scale-100"
- "exiting" "ease-in duration-200 opacity-100 translate-y-0 sm:scale-100"
- "exited" "ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95")}
- [:div.absolute.top-0.right-0.pt-2.pr-2
- (when close-btn?
- [:a.ui__modal-close.opacity-60.hover:opacity-100
- {:aria-label "Close"
- :type "button"
- :on-click close-fn}
- [:svg.h-6.w-6
- {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
- [:path
- {:d "M6 18L18 6M6 6l12 12"
- :stroke-width "2"
- :stroke-linejoin "round"
- :stroke-linecap "round"}]]])]
- (when show?
- [:div {:class (if fullscreen? "" "panel-content")}
- (modal-panel-content panel-content close-fn)])])
- (rum/defc modal < rum/reactive
- (mixins/event-mixin
- (fn [state]
- (mixins/hide-when-esc-or-outside
- state
- :on-hide (fn []
- (some->
- (.querySelector (rum/dom-node state) "button.ui__modal-close")
- (.click)))
- :outside? false)
- (mixins/on-key-down
- state
- {;; enter
- 13 (fn [state e]
- (some->
- (.querySelector (rum/dom-node state) "button.ui__modal-enter")
- (.click)))})))
- []
- (let [modal-panel-content (state/sub :modal/panel-content)
- fullscreen? (state/sub :modal/fullscreen?)
- close-btn? (state/sub :modal/close-btn?)
- show? (boolean modal-panel-content)
- close-fn (fn []
- (state/close-modal!)
- (state/close-settings!))
- modal-panel-content (or modal-panel-content (fn [close] [:div]))]
- [:div.ui__modal
- {:style {:z-index (if show? 100 -1)}}
- (css-transition
- {:in show? :timeout 0}
- (fn [state]
- (modal-overlay state close-fn)))
- (css-transition
- {:in show? :timeout 0}
- (fn [state]
- (modal-panel show? modal-panel-content state close-fn fullscreen? close-btn?)))]))
- (defn make-confirm-modal
- [{:keys [tag title sub-title sub-checkbox? on-cancel on-confirm]
- :or {on-cancel #()}
- :as opts}]
- (fn [close-fn]
- (rum/with-context [[t] i18n/*tongue-context*]
- (let [*sub-checkbox-selected (and sub-checkbox? (atom []))]
- [:div.ui__confirm-modal
- {:class (str "is-" tag)}
- [:div.sm:flex.sm:items-start
- [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
- [:svg.h-6.w-6.text-red-600
- {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
- [:path
- {:d
- "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
- :stroke-width "2"
- :stroke-linejoin "round"
- :stroke-linecap "round"}]]]
- [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
- [:h2.headline.text-lg.leading-6.font-medium
- (if (keyword? title) (t title) title)]
- [:label.sublabel
- (when sub-checkbox?
- (checkbox
- {:default-value false
- :on-change (fn [e]
- (let [checked (.. e -target -checked)]
- (reset! *sub-checkbox-selected [checked])))}))
- [:h3.subline.text-gray-400
- (if (keyword? sub-title)
- (t sub-title)
- sub-title)]]]]
- [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
- [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
- [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
- {:type "button"
- :on-click #(and (fn? on-confirm)
- (on-confirm % {:close-fn close-fn
- :sub-selected (and *sub-checkbox-selected @*sub-checkbox-selected)}))}
- (t :yes)]]
- [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
- [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
- {:type "button"
- :on-click (comp on-cancel close-fn)}
- (t :cancel)]]]]))))
- (defn loading
- [content]
- [:div.flex.flex-row.items-center
- [:span.icon.flex.items-center svg/loading]
- [:span.text.pl-2 content]])
- (rum/defc rotating-arrow
- [collapsed?]
- [:span
- {:class (if collapsed? "rotating-arrow collapsed" "rotating-arrow not-collapsed")}
- (svg/caret-right)])
- (rum/defcs foldable <
- (rum/local false ::control?)
- (rum/local false ::collapsed?)
- {:will-mount (fn [state]
- (let [args (:rum/args state)]
- (when (true? (:default-collapsed? (last args)))
- (reset! (get state ::collapsed?) true)))
- state)}
- [state header content {:keys [default-collapsed? title-trigger?]}]
- (let [control? (get state ::control?)
- collapsed? (get state ::collapsed?)
- on-mouse-down (fn [e]
- (util/stop e)
- (swap! collapsed? not))]
- [:div.flex.flex-col
- [:div.content
- [:div.flex-1.flex-row.foldable-title (cond->
- {:on-mouse-over #(reset! control? true)
- :on-mouse-out #(reset! control? false)}
- title-trigger?
- (assoc :on-mouse-down on-mouse-down
- :class "cursor"))
- [:div.flex.flex-row.items-center
- [:a.block-control.opacity-50.hover:opacity-100.mr-2
- (cond->
- {:style {:width 14
- :height 16
- :margin-left -24}}
- (not title-trigger?)
- (assoc :on-mouse-down on-mouse-down))
- [:span {:class (if @control? "control-show" "control-hide")}
- (rotating-arrow @collapsed?)]]
- (if (fn? header)
- (header @collapsed?)
- header)]]]
- [:div {:class (if @collapsed? "hidden" "initial")
- :on-mouse-down (fn [e] (.stopPropagation e))}
- (if (fn? content)
- (if (not @collapsed?) (content) nil)
- content)]]))
- (defn admonition
- [type content]
- (let [type (name type)]
- (when-let [icon (case (string/lower-case type)
- "note" svg/note
- "tip" svg/tip
- "important" svg/important
- "caution" svg/caution
- "warning" svg/warning
- "pinned" svg/pinned
- nil)]
- [:div.flex.flex-row.admonitionblock.align-items {:class type}
- [:div.pr-4.admonition-icon.flex.flex-col.justify-center
- {:title (string/upper-case type)} (icon)]
- [:div.ml-4.text-lg
- content]])))
- (rum/defcs catch-error
- < {:did-catch
- (fn [state error info]
- (js/console.dir error)
- (assoc state ::error error))}
- [{error ::error, c :rum/react-component} error-view view]
- (if (some? error)
- (do
- (log/error :exception error)
- error-view)
- view))
- (rum/defc select
- [options on-change class]
- [:select.mt-1.block.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
- {:class (or class "form-select")
- :style {:padding "0 0 0 12px"}
- :on-change (fn [e]
- (let [value (util/evalue e)]
- (on-change value)))}
- (for [{:keys [label value selected]} options]
- [:option (cond->
- {:key label
- :value (or value label)}
- selected
- (assoc :selected selected))
- label])])
- (rum/defcs tippy < rum/static
- (rum/local false ::mounted?)
- [state {:keys [fixed-position? open?] :as opts} child]
- (let [*mounted? (::mounted? state)
- mounted? @*mounted?
- manual (not= open? nil)]
- (Tippy (->
- (merge {:arrow true
- :sticky true
- :theme "customized"
- :disabled (not (state/enable-tooltip?))
- :unmountHTMLWhenHide true
- :open (if manual open? @*mounted?)
- :trigger (if manual "manual" "mouseenter focus")
- ;; See https://github.com/tvkhoa/react-tippy/issues/13
- :popperOptions (if fixed-position?
- {:modifiers {:flip {:enabled false}
- :hide {:enabled false}
- :preventOverflow {:enabled false}}}
- {})
- :onShow #(reset! *mounted? true)
- :onHide #(reset! *mounted? false)}
- opts)
- (assoc :html (if (or open? mounted?)
- (try
- (when-let [html (:html opts)]
- (if (fn? html)
- (html)
- [:div.pr-3.py-1
- html]))
- (catch js/Error e
- (log/error :exception e)
- [:div]))
- [:div {:key "tippy"} ""])))
- child)))
- (defn slider
- [default-value {:keys [min max on-change]}]
- [:input.cursor-pointer
- {:type "range"
- :value (int default-value)
- :min min
- :max max
- :style {:width "100%"}
- :on-change #(let [value (util/evalue %)]
- (on-change value))}])
- (rum/defcs tweet-embed < (rum/local true :loading?)
- [state id]
- (let [*loading? (:loading? state)]
- [:div [(when @*loading? [:span.flex.items-center [svg/loading " ... loading"]])
- (ReactTweetEmbed
- {:id id
- :class "contents"
- :options {:theme (when (= (state/sub :ui/theme) "dark") "dark")}
- :on-tweet-load-success #(reset! *loading? false)})]]))
|