385 lines
26 KiB
Clojure
385 lines
26 KiB
Clojure
(ns re-com.misc
|
|
(:require-macros [re-com.core :refer [handler-fn]])
|
|
(:require [re-com.util :refer [deref-or-value px]]
|
|
[re-com.popover :refer [popover-tooltip]]
|
|
[re-com.box :refer [h-box v-box box gap line flex-child-style align-style]]
|
|
[re-com.validate :refer [input-status-type? input-status-types-list regex?
|
|
string-or-hiccup? css-style? html-attr? number-or-string?
|
|
string-or-atom? throbber-size? throbber-sizes-list] :refer-macros [validate-args-macro]]
|
|
[reagent.core :as reagent]))
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: throbber
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def throbber-args-desc
|
|
[{:name :size :required false :type "keyword" :default :regular :validate-fn throbber-size? :description [:span "one of " throbber-sizes-list]}
|
|
{:name :color :required false :type "string" :default "#999" :validate-fn string? :description "CSS color"}
|
|
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}])
|
|
|
|
(defn throbber
|
|
"Render an animated throbber using CSS"
|
|
[& {:keys [size color class style attr] :as args}]
|
|
{:pre [(validate-args-macro throbber-args-desc args "throbber")]}
|
|
(let [seg (fn [] [:li (when color {:style {:background-color color}})])]
|
|
[box
|
|
:align :start
|
|
:child [:ul
|
|
(merge {:class (str "rc-throbber loader "
|
|
(case size :regular ""
|
|
:smaller "smaller "
|
|
:small "small "
|
|
:large "large "
|
|
"")
|
|
class)
|
|
:style style}
|
|
attr)
|
|
[seg] [seg] [seg] [seg]
|
|
[seg] [seg] [seg] [seg]]])) ;; Each :li element in [seg] represents one of the eight circles in the throbber
|
|
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: input-text
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def input-text-args-desc
|
|
[{:name :model :required true :type "string | atom" :validate-fn string-or-atom? :description "text of the input (can be atom or value)"}
|
|
{:name :on-change :required true :type "string -> nil" :validate-fn fn? :description [:span [:code ":change-on-blur?"] " controls when it is called. Passed the current input string"] }
|
|
{:name :status :required false :type "keyword" :validate-fn input-status-type? :description [:span "validation status. " [:code "nil/omitted"] " for normal status or one of: " input-status-types-list]}
|
|
{:name :status-icon? :required false :default false :type "boolean" :description [:span "when true, display an icon to match " [:code ":status"] " (no icon for nil)"]}
|
|
{:name :status-tooltip :required false :type "string" :validate-fn string? :description "displayed in status icon's tooltip"}
|
|
{:name :placeholder :required false :type "string" :validate-fn string? :description "background text shown when empty"}
|
|
{:name :width :required false :default "250px" :type "string" :validate-fn string? :description "standard CSS width setting for this input"}
|
|
{:name :height :required false :type "string" :validate-fn string? :description "standard CSS height setting for this input"}
|
|
{:name :rows :required false :default 3 :type "integer | string" :validate-fn number-or-string? :description "ONLY applies to 'input-textarea': the number of rows of text to show"}
|
|
{:name :change-on-blur? :required false :default true :type "boolean | atom" :description [:span "when true, invoke " [:code ":on-change"] " function on blur, otherwise on every change (character by character)"] }
|
|
{:name :validation-regex :required false :type "regex" :validate-fn regex? :description "user input is only accepted if it would result in a string that matches this regular expression"}
|
|
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, the user can't interact (input anything)"}
|
|
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}
|
|
{:name :input-type :required false :type "keyword" :validate-fn keyword? :description [:span "ONLY applies to super function 'base-input-text': either " [:code ":input"] ", " [:code ":password"] " or " [:code ":textarea"]]}])
|
|
|
|
;; Sample regex's:
|
|
;; - #"^(-{0,1})(\d*)$" ;; Signed integer
|
|
;; - #"^(\d{0,2})$|^(\d{0,2}\.\d{0,1})$" ;; Specific numeric value ##.#
|
|
;; - #"^.{0,8}$" ;; 8 chars max
|
|
;; - #"^[0-9a-fA-F]*$" ;; Hex number
|
|
;; - #"^(\d{0,2})()()$|^(\d{0,1})(:{0,1})(\d{0,2})$|^(\d{0,2})(:{0,1})(\d{0,2})$" ;; Time input
|
|
|
|
(defn- input-text-base
|
|
"Returns markup for a basic text input label"
|
|
[& {:keys [model input-type] :as args}]
|
|
{:pre [(validate-args-macro input-text-args-desc args "input-text")]}
|
|
(let [external-model (reagent/atom (deref-or-value model)) ;; Holds the last known external value of model, to detect external model changes
|
|
internal-model (reagent/atom (if (nil? @external-model) "" @external-model))] ;; Create a new atom from the model to be used internally (avoid nil)
|
|
(fn
|
|
[& {:keys [model status status-icon? status-tooltip placeholder width height rows on-change change-on-blur? validation-regex disabled? class style attr]
|
|
:or {change-on-blur? true}
|
|
:as args}]
|
|
{:pre [(validate-args-macro input-text-args-desc args "input-text")]}
|
|
(let [latest-ext-model (deref-or-value model)
|
|
disabled? (deref-or-value disabled?)
|
|
change-on-blur? (deref-or-value change-on-blur?)
|
|
showing? (reagent/atom false)]
|
|
(when (not= @external-model latest-ext-model) ;; Has model changed externally?
|
|
(reset! external-model latest-ext-model)
|
|
(reset! internal-model latest-ext-model))
|
|
[h-box
|
|
:align :start
|
|
:width (if width width "250px")
|
|
:class "rc-input-text "
|
|
:children [[:div
|
|
{:class (str "rc-input-text-inner " ;; form-group
|
|
(case status
|
|
:success "has-success "
|
|
:warning "has-warning "
|
|
:error "has-error "
|
|
"")
|
|
(when (and status status-icon?) "has-feedback"))
|
|
:style (flex-child-style "auto")}
|
|
[(if (= input-type :password) :input input-type)
|
|
(merge
|
|
{:class (str "form-control " class)
|
|
:type (case input-type
|
|
:input "text"
|
|
:password "password"
|
|
nil)
|
|
:rows (when (= input-type :textarea) (if rows rows 3))
|
|
:style (merge
|
|
(flex-child-style "none")
|
|
{:height height
|
|
:padding-right "12px"} ;; override for when icon exists
|
|
style)
|
|
:placeholder placeholder
|
|
:value @internal-model
|
|
:disabled disabled?
|
|
:on-change (handler-fn
|
|
(let [new-val (-> event .-target .-value)]
|
|
(when (and
|
|
on-change
|
|
(not disabled?)
|
|
(if validation-regex (re-find validation-regex new-val) true))
|
|
(reset! internal-model new-val)
|
|
(when-not change-on-blur?
|
|
(on-change @internal-model)))))
|
|
:on-blur (handler-fn
|
|
(when (and
|
|
on-change
|
|
change-on-blur?
|
|
(not= @internal-model @external-model))
|
|
(on-change @internal-model)))
|
|
:on-key-up (handler-fn
|
|
(if disabled?
|
|
(.preventDefault event)
|
|
(case (.-which event)
|
|
13 (when on-change (on-change @internal-model))
|
|
27 (reset! internal-model @external-model)
|
|
true)))
|
|
|
|
}
|
|
attr)]]
|
|
(when (and status-icon? status)
|
|
(let [icon-class (case status :success "zmdi-check-circle" :warning "zmdi-alert-triangle" :error "zmdi-alert-circle zmdi-spinner" :validating "zmdi-hc-spin zmdi-rotate-right zmdi-spinner")]
|
|
(if status-tooltip
|
|
[popover-tooltip
|
|
:label status-tooltip
|
|
:position :right-center
|
|
:status status
|
|
;:width "200px"
|
|
:showing? showing?
|
|
:anchor (if (= :validating status)
|
|
[throbber
|
|
:size :regular
|
|
:class "smaller"
|
|
:attr {:on-mouse-over (handler-fn (when (and status-icon? status) (reset! showing? true)))
|
|
:on-mouse-out (handler-fn (reset! showing? false))}]
|
|
[:i {:class (str "zmdi zmdi-hc-fw " icon-class " form-control-feedback")
|
|
:style {:position "static"
|
|
:height "auto"
|
|
:opacity (if (and status-icon? status) "1" "0")}
|
|
:on-mouse-over (handler-fn (when (and status-icon? status) (reset! showing? true)))
|
|
:on-mouse-out (handler-fn (reset! showing? false))}])
|
|
:style (merge (flex-child-style "none")
|
|
(align-style :align-self :center)
|
|
{:font-size "130%"
|
|
:margin-left "4px"})]
|
|
(if (= :validating status)
|
|
[throbber :size :regular :class "smaller"]
|
|
[:i {:class (str "zmdi zmdi-hc-fw " icon-class " form-control-feedback")
|
|
:style (merge (flex-child-style "none")
|
|
(align-style :align-self :center)
|
|
{:position "static"
|
|
:font-size "130%"
|
|
:margin-left "4px"
|
|
:opacity (if (and status-icon? status) "1" "0")
|
|
:height "auto"})
|
|
:title status-tooltip}]))))]]))))
|
|
|
|
|
|
(defn input-text
|
|
[& args]
|
|
(apply input-text-base :input-type :input args))
|
|
|
|
|
|
(defn input-password
|
|
[& args]
|
|
(apply input-text-base :input-type :password args))
|
|
|
|
|
|
(defn input-textarea
|
|
[& args]
|
|
(apply input-text-base :input-type :textarea args))
|
|
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: checkbox
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def checkbox-args-desc
|
|
[{:name :model :required true :type "boolean | atom" :description "holds state of the checkbox when it is called"}
|
|
{:name :on-change :required true :type "boolean -> nil" :validate-fn fn? :description "called when the checkbox is clicked. Passed the new value of the checkbox"}
|
|
{:name :label :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "the label shown to the right"}
|
|
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, user interaction is disabled"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "the CSS style style map"}
|
|
{:name :label-style :required false :type "CSS style map" :validate-fn css-style? :description "the CSS class applied overall to the component"}
|
|
{:name :label-class :required false :type "string" :validate-fn string? :description "the CSS class applied to the label"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}])
|
|
|
|
;; TODO: when disabled?, should the text appear "disabled".
|
|
(defn checkbox
|
|
"I return the markup for a checkbox, with an optional RHS label"
|
|
[& {:keys [model on-change label disabled? style label-class label-style attr]
|
|
:as args}]
|
|
{:pre [(validate-args-macro checkbox-args-desc args "checkbox")]}
|
|
(let [cursor "default"
|
|
model (deref-or-value model)
|
|
disabled? (deref-or-value disabled?)
|
|
callback-fn #(when (and on-change (not disabled?))
|
|
(on-change (not model)))] ;; call on-change with either true or false
|
|
[h-box
|
|
:align :start
|
|
:class "noselect"
|
|
:children [[:input
|
|
(merge
|
|
{:class "rc-checkbox"
|
|
:type "checkbox"
|
|
:style (merge (flex-child-style "none")
|
|
{:cursor cursor}
|
|
style)
|
|
:disabled disabled?
|
|
:checked (boolean model)
|
|
:on-change (handler-fn (callback-fn))}
|
|
attr)]
|
|
(when label
|
|
[:span
|
|
{:on-click (handler-fn (callback-fn))
|
|
:class label-class
|
|
:style (merge (flex-child-style "none")
|
|
{:padding-left "8px"
|
|
:cursor cursor}
|
|
label-style)}
|
|
label])]]))
|
|
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: radio-button
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def radio-button-args-desc
|
|
[{:name :model :required true :type "anything | atom" :description [:span "selected value of the radio button group. See also " [:code ":value"]] }
|
|
{:name :value :required false :type "anything" :description [:span "if " [:code ":model"] " equals " [:code ":value"] " then this radio button is selected"] }
|
|
{:name :on-change :required true :type "anything -> nil" :validate-fn fn? :description [:span "called when the radio button is clicked. Passed " [:code ":value"]]}
|
|
{:name :label :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "the label shown to the right"}
|
|
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, the user can't click the radio button"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "radio button style map"}
|
|
{:name :label-style :required false :type "CSS style map" :validate-fn css-style? :description "the CSS class applied overall to the component"}
|
|
{:name :label-class :required false :type "string" :validate-fn string? :description "the CSS class applied to the label"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}])
|
|
|
|
(defn radio-button
|
|
"I return the markup for a radio button, with an optional RHS label"
|
|
[& {:keys [model on-change value label disabled? style label-class label-style attr]
|
|
:as args}]
|
|
{:pre [(validate-args-macro radio-button-args-desc args "radio-button")]}
|
|
(let [cursor "default"
|
|
model (deref-or-value model)
|
|
disabled? (deref-or-value disabled?)
|
|
callback-fn #(when (and on-change (not disabled?))
|
|
(on-change value))] ;; call on-change with the :value arg
|
|
[h-box
|
|
:align :start
|
|
:class "noselect"
|
|
:children [[:input
|
|
(merge
|
|
{:class "rc-radio-button"
|
|
:type "radio"
|
|
:style (merge
|
|
(flex-child-style "none")
|
|
{:cursor cursor}
|
|
style)
|
|
:disabled disabled?
|
|
:checked (= model value)
|
|
:on-change (handler-fn (callback-fn))}
|
|
attr)]
|
|
(when label
|
|
[:span
|
|
{:on-click (handler-fn (callback-fn))
|
|
:class label-class
|
|
:style (merge (flex-child-style "none")
|
|
{:padding-left "8px"
|
|
:cursor cursor}
|
|
label-style)}
|
|
label])]]))
|
|
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: slider
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def slider-args-desc
|
|
[{:name :model :required true :type "double | string | atom" :validate-fn number-or-string? :description "current value of the slider"}
|
|
{:name :on-change :required true :type "double -> nil" :validate-fn fn? :description "called when the slider is moved. Passed the new value of the slider"}
|
|
{:name :min :required false :default 0 :type "double | string | atom" :validate-fn number-or-string? :description "the minimum value of the slider"}
|
|
{:name :max :required false :default 100 :type "double | string | atom" :validate-fn number-or-string? :description "the maximum value of the slider"}
|
|
{:name :step :required false :default 1 :type "double | string | atom" :validate-fn number-or-string? :description "step value between min and max"}
|
|
{:name :width :required false :default "400px" :type "string" :validate-fn string? :description "standard CSS width setting for the slider"}
|
|
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, the user can't change the slider"}
|
|
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}])
|
|
|
|
(defn slider
|
|
"Returns markup for an HTML5 slider input"
|
|
[]
|
|
(fn
|
|
[& {:keys [model min max step width on-change disabled? class style attr]
|
|
:or {min 0 max 100}
|
|
:as args}]
|
|
{:pre [(validate-args-macro slider-args-desc args "slider")]}
|
|
(let [model (deref-or-value model)
|
|
min (deref-or-value min)
|
|
max (deref-or-value max)
|
|
step (deref-or-value step)
|
|
disabled? (deref-or-value disabled?)]
|
|
[box
|
|
:align :start
|
|
:child [:input
|
|
(merge
|
|
{:class (str "rc-slider " class)
|
|
:type "range"
|
|
;:orient "vertical" ;; Make Firefox slider vertical (doesn't work because React ignores it, I think)
|
|
:style (merge
|
|
(flex-child-style "none")
|
|
{;:-webkit-appearance "slider-vertical" ;; TODO: Make a :orientation (:horizontal/:vertical) option
|
|
;:writing-mode "bt-lr" ;; Make IE slider vertical
|
|
:width (if width width "400px")
|
|
:cursor (if disabled? "not-allowed" "default")}
|
|
style)
|
|
:min min
|
|
:max max
|
|
:step step
|
|
:value model
|
|
:disabled disabled?
|
|
:on-change (handler-fn (on-change (js/Number (-> event .-target .-value))))}
|
|
attr)]])))
|
|
|
|
|
|
;; ------------------------------------------------------------------------------------
|
|
;; Component: progress-bar
|
|
;; ------------------------------------------------------------------------------------
|
|
|
|
(def progress-bar-args-desc
|
|
[{:name :model :required true :type "double | string | atom" :validate-fn number-or-string? :description "current value of the slider. A number between 0 and 100"}
|
|
{:name :width :required false :type "string" :default "100%" :validate-fn string? :description "a CSS width"}
|
|
{:name :striped? :required false :type "boolean" :default false :description "when true, the progress section is a set of animated stripes"}
|
|
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated"}
|
|
{:name :bar-class :required false :type "string" :validate-fn string? :description "CSS class name(s) for the actual progress bar itself, space separated"}
|
|
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override"}
|
|
{:name :attr :required false :type "HTML attr map" :validate-fn html-attr? :description [:span "HTML attributes, like " [:code ":on-mouse-move"] [:br] "No " [:code ":class"] " or " [:code ":style"] "allowed"]}])
|
|
|
|
(defn progress-bar
|
|
"Render a bootstrap styled progress bar"
|
|
[& {:keys [model width striped? class bar-class style attr]
|
|
:or {width "100%"}
|
|
:as args}]
|
|
{:pre [(validate-args-macro progress-bar-args-desc args "progress-bar")]}
|
|
(let [model (deref-or-value model)]
|
|
[box
|
|
:align :start
|
|
:child [:div
|
|
(merge
|
|
{:class (str "rc-progress-bar progress " class)
|
|
:style (merge (flex-child-style "none")
|
|
{:width width}
|
|
style)}
|
|
attr)
|
|
[:div
|
|
{:class (str "progress-bar " (when striped? "progress-bar-striped active ") bar-class)
|
|
:role "progressbar"
|
|
:style {:width (str model "%")
|
|
:transition "none"}} ;; Default BS transitions cause the progress bar to lag behind
|
|
(str model "%")]]]))
|