(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 "%")]]]))