Added compiled JavaScript to repository for GitHub pages

This feels like a mistake...
This commit is contained in:
Simon Brooke 2020-10-20 14:44:11 +01:00
parent 3d5a2fb322
commit dc226b1f25
468 changed files with 212152 additions and 2 deletions

View file

@ -0,0 +1,116 @@
(ns re-com.alert
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [deref-or-value]]
[re-com.buttons :refer [button]]
[re-com.box :refer [h-box v-box box scroller border flex-child-style]]
[re-com.validate :refer [string-or-hiccup? alert-type? alert-types-list
vector-of-maps? css-style? html-attr?] :refer-macros [validate-args-macro]]))
;;--------------------------------------------------------------------------------------------------
;; Component: alert
;;--------------------------------------------------------------------------------------------------
(def alert-box-args-desc
[{:name :id :required false :type "anything" :description [:span "a unique identifier, usually an integer or string."]}
{:name :alert-type :required false :default :info :type "keyword" :validate-fn alert-type? :description [:span "one of " alert-types-list]}
{:name :heading :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description [:span "displayed as a larger heading. One of " [:code ":heading"] " or " [:code ":body"] " should be provided"]}
{:name :body :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "displayed within the body of the alert"}
{:name :padding :required false :default "15px" :type "string" :validate-fn string? :description "padding surounding the alert"}
{:name :closeable? :required false :default false :type "boolean" :description [:span "if true, render a close button. " [:code ":on-close"] " should be supplied"]}
{:name :on-close :required false :type ":id -> nil" :validate-fn fn? :description [:span "called when the user clicks the close 'X' button. Passed the " [:code ":id"] " of the alert to close"]}
{:name :class :required false :type "string" :validate-fn string? :description "CSS classes (whitespace separated). Applied to outer container"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles. Applied to outer container"}
{: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. Applied to outer container"]}])
(defn alert-box
"Displays one alert box. A close button allows the message to be removed"
[& {:keys [id alert-type heading body padding closeable? on-close class style attr]
:or {alert-type :info}
:as args}]
{:pre [(validate-args-macro alert-box-args-desc args "alert-box")]}
(let [close-button [button
:label [:i {:class "zmdi created zmdi-hc-fw-rc zmdi-close"
:style {:font-size "20px"}}] ;"×"
:on-click (handler-fn (on-close id))
:class "close"]
alert-class (alert-type {:none ""
:info "alert-success"
:warning "alert-warning"
:danger "alert-danger"})]
[:div
(merge {:class (str "rc-alert alert fade in " alert-class " " class)
:style (merge (flex-child-style "none")
{:padding (when padding padding)}
style)}
attr)
(when heading
[h-box
:justify :between
:align :center
:style {:margin-bottom (if body "10px" "0px")}
:children [[:h4
{:style {:margin-bottom "0px"}} ;; Override h4
heading]
(when (and closeable? on-close)
close-button)]])
(when body
[h-box
:justify :between
:align :center
:children [[:div body]
(when (and (not heading) closeable? on-close)
close-button)]])]))
;;--------------------------------------------------------------------------------------------------
;; Component: alert-list
;;--------------------------------------------------------------------------------------------------
(def alert-list-args-desc
[{:name :alerts :required true :type "vector of maps | atom" :validate-fn vector-of-maps? :description "alerts to render (in the order supplied). Can also be a list of maps"}
{:name :on-close :required true :type ":id -> nil" :validate-fn fn? :description [:span "called when the user clicks the close 'X' button. Passed the alert's " [:code ":id"]]}
{:name :max-height :required false :type "string" :validate-fn string? :description "CSS style for maximum list height. By default, it grows forever"}
{:name :padding :required false :default "4px" :type "string" :validate-fn string? :description "CSS padding within the alert"}
{:name :border-style :required false :default "1px solid lightgrey" :type "string" :validate-fn string? :description "CSS border style surrounding the list"}
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated. Applied to outer container"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles. Applied to outer container"}
{: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. Applied to outer container"]}])
(defn alert-list
"Displays a list of alert-box components in a v-box. Sample alerts object:
[{:id 2
:alert-type :warning
:heading \"Heading\"
:body \"Body\"
:padding \"8px\"
:closeable? true}
{:id 1
:alert-type :info
:heading \"Heading\"
:body \"Body\"}]"
[& {:keys [alerts on-close max-height padding border-style class style attr]
:or {padding "4px"}
:as args}]
{:pre [(validate-args-macro alert-list-args-desc args "alert-list")]}
(let [alerts (deref-or-value alerts)]
[box
:child [border
:padding padding
:border border-style
:class class
:style style
:attr attr
:child [scroller
:v-scroll :auto
:style {:max-height max-height}
:child [v-box
:size "auto"
:children [(for [alert alerts]
(let [{:keys [id alert-type heading body padding closeable?]} alert]
^{:key id} [alert-box
:id id
:alert-type alert-type
:heading heading
:body body
:padding padding
:closeable? closeable?
:on-close on-close]))]]]]]))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,505 @@
(ns re-com.box
(:require [clojure.string :as string]
[re-com.validate :refer [justify-style? justify-options-list align-style? align-options-list
scroll-style? scroll-options-list string-or-hiccup? css-style? html-attr?] :refer-macros [validate-args-macro]]))
(def debug false)
;; ------------------------------------------------------------------------------------
;; Private Helper functions
;; ------------------------------------------------------------------------------------
(defn flex-child-style
"Determines the value for the 'flex' attribute (which has grow, shrink and basis), based on the :size parameter.
IMPORTANT: The term 'size' means width of the item in the case of flex-direction 'row' OR height of the item in the case of flex-direction 'column'.
Flex property explanation:
- grow Integer ratio (used with other siblings) to determined how a flex item grows it's size if there is extra space to distribute. 0 for no growing.
- shrink Integer ratio (used with other siblings) to determined how a flex item shrinks it's size if space needs to be removed. 0 for no shrinking.
- basis Initial size (width, actually) of item before any growing or shrinking. Can be any size value, e.g. 60%, 100px, auto
Note: auto will cause the initial size to be calculated to take up as much space as possible, in conjunction with it's siblings :flex settings.
Supported values:
- initial '0 1 auto' - Use item's width/height for dimensions (or content dimensions if w/h not specifed). Never grow. Shrink (to min-size) if necessary.
Good for creating boxes with fixed maximum size, but that can shrink to a fixed smaller size (min-width/height) if space becomes tight.
NOTE: When using initial, you should also set a width/height value (depending on flex-direction) to specify it's default size
and an optional min-width/height value to specify the size it can shrink to.
- auto '1 1 auto' - Use item's width/height for dimensions. Grow if necessary. Shrink (to min-size) if necessary.
Good for creating really flexible boxes that will gobble as much available space as they are allowed or shrink as much as they are forced to.
- none '0 0 auto' - Use item's width/height for dimensions (or content dimensions if not specifed). Never grow. Never shrink.
Good for creating rigid boxes that stick to their width/height if specified, otherwise their content size.
- 100px '0 0 100px' - Non flexible 100px size (in the flex direction) box.
Good for fixed headers/footers and side bars of an exact size.
- 60% '60 1 0px' - Set the item's size (it's width/height depending on flex-direction) to be 60% of the parent container's width/height.
NOTE: If you use this, then all siblings with percentage values must add up to 100%.
- 60 '60 1 0px' - Same as percentage above.
- grow shrink basis 'grow shrink basis' - If none of the above common valaues above meet your needs, this gives you precise control.
If number of words is not 1 or 3, an exception is thrown.
Reference: http://www.w3.org/TR/css3-flexbox/#flexibility
Diagram: http://www.w3.org/TR/css3-flexbox/#flex-container
Regex101 testing: ^(initial|auto|none)|(\\d+)(px|%|em)|(\\d+)\\w(\\d+)\\w(.*) - remove double backslashes"
[size]
;; TODO: Could make initial/auto/none into keywords???
(let [split-size (string/split (string/trim size) #"\s+") ;; Split into words separated by whitespace
split-count (count split-size)
_ (assert (contains? #{1 3} split-count) "Must pass either 1 or 3 words to flex-child-style")
size-only (when (= split-count 1) (first split-size)) ;; Contains value when only one word passed (e.g. auto, 60px)
split-size-only (when size-only (string/split size-only #"(\d+)(.*)")) ;; Split into number + string
[_ num units] (when size-only split-size-only) ;; grab number and units
pass-through? (nil? num) ;; If we can't split, then we'll pass this straign through
grow-ratio? (or (= units "%") (= units "") (nil? units)) ;; Determine case for using grow ratio
grow (if grow-ratio? num "0") ;; Set grow based on percent or integer, otherwise no grow
shrink (if grow-ratio? "1" "0") ;; If grow set, then set shrink to even shrinkage as well
basis (if grow-ratio? "0px" size) ;; If grow set, then even growing, otherwise set basis size to the passed in size (e.g. 100px, 5em)
flex (if (and size-only (not pass-through?))
(str grow " " shrink " " basis)
size)]
{:-webkit-flex flex
:flex flex}))
(defn flex-flow-style
"A cross-browser helper function to output flex-flow with all it's potential browser prefixes"
[flex-flow]
{:-webkit-flex-flow flex-flow
:flex-flow flex-flow})
(defn justify-style
"Determines the value for the flex 'justify-content' attribute.
This parameter determines how children are aligned along the main axis.
The justify parameter is a keyword.
Reference: http://www.w3.org/TR/css3-flexbox/#justify-content-property"
[justify]
(let [js (case justify
:start "flex-start"
:end "flex-end"
:center "center"
:between "space-between"
:around "space-around")]
{:-webkit-justify-content js
:justify-content js}))
(defn align-style
"Determines the value for the flex align type attributes.
This parameter determines how children are aligned on the cross axis.
The justify parameter is a keyword.
Reference: http://www.w3.org/TR/css3-flexbox/#align-items-property"
[attribute align]
(let [attribute-wk (->> attribute name (str "-webkit-") keyword)
as (case align
:start "flex-start"
:end "flex-end"
:center "center"
:baseline "baseline"
:stretch "stretch")]
{attribute-wk as
attribute as}))
(defn scroll-style
"Determines the value for the 'overflow' attribute.
The scroll parameter is a keyword.
Because we're translating scroll into overflow, the keyword doesn't appear to match the attribute value"
[attribute scroll]
{attribute (case scroll
:auto "auto"
:off "hidden"
:on "scroll"
:spill "visible")})
;; ------------------------------------------------------------------------------------
;; Private Component: box-base (debug color: lightblue)
;; ------------------------------------------------------------------------------------
(defn- box-base
"This should generally NOT be used as it is the basis for the box, scroller and border components"
[& {:keys [size scroll h-scroll v-scroll width height min-width min-height max-width max-height justify align align-self
margin padding border l-border r-border t-border b-border radius bk-color child class-name class style attr]}]
(let [s (merge
(flex-flow-style "inherit")
(flex-child-style size)
(when scroll (scroll-style :overflow scroll))
(when h-scroll (scroll-style :overflow-x h-scroll))
(when v-scroll (scroll-style :overflow-y v-scroll))
(when width {:width width})
(when height {:height height})
(when min-width {:min-width min-width})
(when min-height {:min-height min-height})
(when max-width {:max-width max-width})
(when max-height {:max-height max-height})
(when justify (justify-style justify))
(when align (align-style :align-items align))
(when align-self (align-style :align-self align-self))
(when margin {:margin margin}) ;; margin and padding: "all" OR "top&bottom right&left" OR "top right bottom left"
(when padding {:padding padding})
(when border {:border border})
(when l-border {:border-left l-border})
(when r-border {:border-right r-border})
(when t-border {:border-top t-border})
(when b-border {:border-bottom b-border})
(when radius {:border-radius radius})
(if bk-color
{:background-color bk-color}
(if debug {:background-color "lightblue"} {}))
style)]
[:div
(merge
{:class (str class-name "display-flex " class) :style s}
attr)
child]))
;; ------------------------------------------------------------------------------------
;; Component: gap (debug color: chocolate)
;; ------------------------------------------------------------------------------------
(def gap-args-desc
[{:name :size :required true :type "string" :validate-fn string? :description "the length of the whitespace. Typically, an absolute CSS length like 10px or 10em, but can be a stretchy proportional amount like 2"}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS width style"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS height style"}
{: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 gap
"Returns a component which produces a gap between children in a v-box/h-box along the main axis"
[& {:keys [size width height class style attr]
:as args}]
{:pre [(validate-args-macro gap-args-desc args "gap")]}
(let [s (merge
(when size (flex-child-style size))
(when width {:width width})
(when height {:height height})
(when debug {:background-color "chocolate"})
style)]
[:div
(merge
{:class (str "rc-gap " class) :style s}
attr)]))
;; ------------------------------------------------------------------------------------
;; Component: line
;; ------------------------------------------------------------------------------------
(def line-args-desc
[{:name :size :required false :default "1px" :type "string" :validate-fn string? :description "a CSS style for the thickness of the line. Usually px, % or em"}
{:name :color :required false :default "lightgray" :type "string" :validate-fn string? :description "a 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 line
"Returns a component which produces a line between children in a v-box/h-box along the main axis.
Specify size in pixels and a stancard CSS color. Defaults to a 1px lightgray line"
[& {:keys [size color class style attr]
:or {size "1px" color "lightgray"}
:as args}]
{:pre [(validate-args-macro line-args-desc args "line")]}
(let [s (merge
(flex-child-style (str "0 0 " size))
{:background-color color}
style)]
[:div
(merge
{:class (str "rc-line " class) :style s}
attr)]))
;; ------------------------------------------------------------------------------------
;; Component: h-box (debug color: gold)
;; ------------------------------------------------------------------------------------
(def h-box-args-desc
[{:name :children :required true :type "vector" :validate-fn sequential? :description "a vector (or list) of components"}
{:name :size :required false :default "none" :type "string" :validate-fn string? :description [:span "equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS width style"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS height style"}
{:name :min-width :required false :type "string" :validate-fn string? :description "a CSS width style. The minimum width to which the box can shrink"}
{:name :min-height :required false :type "string" :validate-fn string? :description "a CSS height style. The minimum height to which the box can shrink"}
{:name :max-width :required false :type "string" :validate-fn string? :description "a CSS width style. The maximum width to which the box can grow"}
{:name :max-height :required false :type "string" :validate-fn string? :description "a CSS height style. The maximum height to which the box can grow"}
{:name :justify :required false :default :start :type "keyword" :validate-fn justify-style? :description [:span "equivalent to CSS style " [:span.bold "justify-content"] "." [:br] "One of " justify-options-list]}
{:name :align :required false :default :stretch :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-items"] "." [:br] " One of " align-options-list]}
{:name :align-self :required false :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-self"] "." [:br] "Used when a child must override the parent's align-items setting."]}
{:name :margin :required false :type "string" :validate-fn string? :description "a CSS margin style"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS padding style"}
{:name :gap :required false :type "string" :validate-fn string? :description "the amount of whitespace to put between each child. Typically, an absolute CSS length like 10px or 10em, but can be a stretchy proportional amount like 2"}
{: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 h-box
"Returns hiccup which produces a horizontal box.
It's primary role is to act as a container for components and lays it's children from left to right.
By default, it also acts as a child under it's parent"
[& {:keys [size width height min-width min-height max-width max-height justify align align-self margin padding gap children class style attr]
:or {size "none" justify :start align :stretch}
:as args}]
{:pre [(validate-args-macro h-box-args-desc args "h-box")]}
(let [s (merge
(flex-flow-style "row nowrap")
(flex-child-style size)
(when width {:width width})
(when height {:height height})
(when min-width {:min-width min-width})
(when min-height {:min-height min-height})
(when max-width {:max-width max-width})
(when max-height {:max-height max-height})
(justify-style justify)
(align-style :align-items align)
(when align-self (align-style :align-self align-self))
(when margin {:margin margin}) ;; margin and padding: "all" OR "top&bottom right&left" OR "top right bottom left"
(when padding {:padding padding})
(when debug {:background-color "gold"})
style)
gap-form (when gap [re-com.box/gap
:size gap
:width gap]) ;; TODO: required to get around a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=423112. Remove once fixed.
children (if gap
(interpose gap-form (filter identity children)) ;; filter is to remove possible nils so we don't add unwanted gaps
children)]
(into [:div
(merge
{:class (str "rc-h-box display-flex " class) :style s}
attr)]
children)))
;; ------------------------------------------------------------------------------------
;; Component: v-box (debug color: antiquewhite)
;; ------------------------------------------------------------------------------------
(def v-box-args-desc
[{:name :children :required true :type "vector" :validate-fn sequential? :description "a vector (or list) of components"}
{:name :size :required false :default "none" :type "string" :validate-fn string? :description [:span "equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS width style"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS height style"}
{:name :min-width :required false :type "string" :validate-fn string? :description "a CSS width style. The minimum width to which the box can shrink"}
{:name :min-height :required false :type "string" :validate-fn string? :description "a CSS height style. The minimum height to which the box can shrink"}
{:name :max-width :required false :type "string" :validate-fn string? :description "a CSS width style. The maximum width to which the box can grow"}
{:name :max-height :required false :type "string" :validate-fn string? :description "a CSS height style. The maximum height to which the box can grow"}
{:name :justify :required false :default :start :type "keyword" :validate-fn justify-style? :description [:span "equivalent to CSS style " [:span.bold "justify-content"] "." [:br] "One of " justify-options-list]}
{:name :align :required false :default :stretch :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-items"] "." [:br] " One of " align-options-list]}
{:name :align-self :required false :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-self"] "." [:br] "Used when a child must override the parent's align-items setting."]}
{:name :margin :required false :type "string" :validate-fn string? :description "a CSS margin style"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS padding style"}
{:name :gap :required false :type "string" :validate-fn string? :description "the amount of whitespace to put between each child. Typically, an absolute CSS length like 10px or 10em, but can be a stretchy proportional amount like 2"}
{: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 v-box
"Returns hiccup which produces a vertical box.
It's primary role is to act as a container for components and lays it's children from top to bottom.
By default, it also acts as a child under it's parent"
[& {:keys [size width height min-width min-height max-width max-height justify align align-self margin padding gap children class style attr]
:or {size "none" justify :start align :stretch}
:as args}]
{:pre [(validate-args-macro v-box-args-desc args "v-box")]}
(let [s (merge
(flex-flow-style "column nowrap")
(flex-child-style size)
(when width {:width width})
(when height {:height height})
(when min-width {:min-width min-width})
(when min-height {:min-height min-height})
(when max-width {:max-width max-width})
(when max-height {:max-height max-height})
(justify-style justify)
(align-style :align-items align)
(when align-self (align-style :align-self align-self))
(when margin {:margin margin}) ;; margin and padding: "all" OR "top&bottom right&left" OR "top right bottom left"
(when padding {:padding padding})
(when debug {:background-color "antiquewhite"})
style)
gap-form (when gap [re-com.box/gap
:size gap
:height gap]) ;; TODO: required to get around a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=423112. Remove once fixed.
children (if gap
(interpose gap-form (filter identity children)) ;; filter is to remove possible nils so we don't add unwanted gaps
children)]
(into [:div
(merge
{:class (str "rc-v-box display-flex " class) :style s}
attr)]
children)))
;; ------------------------------------------------------------------------------------
;; Component: box
;; ------------------------------------------------------------------------------------
(def box-args-desc
[{:name :child :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "a component (or string)"}
{:name :size :required false :default "none" :type "string" :validate-fn string? :description [:span "equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS width style"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS height style"}
{:name :min-width :required false :type "string" :validate-fn string? :description "a CSS width style. The minimum width to which the box can shrink"}
{:name :min-height :required false :type "string" :validate-fn string? :description "a CSS height style. The minimum height to which the box can shrink"}
{:name :max-width :required false :type "string" :validate-fn string? :description "a CSS width style. The maximum width to which the box can grow"}
{:name :max-height :required false :type "string" :validate-fn string? :description "a CSS height style. The maximum height to which the box can grow"}
{:name :justify :required false :default :start :type "keyword" :validate-fn justify-style? :description [:span "equivalent to CSS style " [:span.bold "justify-content"] "." [:br] "One of " justify-options-list]}
{:name :align :required false :default :stretch :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-items"] "." [:br] " One of " align-options-list]}
{:name :align-self :required false :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-self"] "." [:br] "Used when a child must override the parent's align-items setting."]}
{:name :margin :required false :type "string" :validate-fn string? :description "a CSS margin style"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS padding style"}
{: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 box
"Returns hiccup which produces a box, which is generally used as a child of a v-box or an h-box.
By default, it also acts as a container for further child compenents, or another h-box or v-box"
[& {:keys [size width height min-width min-height max-width max-height justify align align-self margin padding child class style attr]
:or {size "none"}
:as args}]
{:pre [(validate-args-macro box-args-desc args "box")]}
(box-base :size size
:width width
:height height
:min-width min-width
:min-height min-height
:max-width max-width
:max-height max-height
:justify justify
:align align
:align-self align-self
:margin margin
:padding padding
:child child
:class-name "rc-box "
:class class
:style style
:attr attr))
;; ------------------------------------------------------------------------------------
;; Component: scroller
;; ------------------------------------------------------------------------------------
(def scroller-args-desc
[{:name :child :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "a component (or string)"}
{:name :size :required false :default "auto" :type "string" :validate-fn string? :description [:span "equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :scroll :required false :default "auto" :type "keyword" :validate-fn scroll-style? :description [:span "Sets both h-scroll and v-scroll at once: " [:br]
[:code ":auto"] ": only show scroll bar(s) if the content is larger than the scroller" [:br]
[:code ":on"] ": always show scroll bars" [:br]
[:code ":off"] ": never show scroll bar(s). Content which is not in the bounds of the scroller can not be seen" [:br]
[:code ":spill"] ": never show scroll bar(s). Content which is not in the bounds of the scroller spills all over the place"]}
{:name :h-scroll :required false :type "keyword" :validate-fn scroll-style? :description [:span "see " [:code ":scroll"] ". Overrides that setting"]}
{:name :v-scroll :required false :type "keyword" :validate-fn scroll-style? :description [:span "see " [:code ":scroll"] ". Overrides that setting"]}
{:name :width :required false :type "string" :validate-fn string? :description "initial width"}
{:name :height :required false :type "string" :validate-fn string? :description "initial height"}
{:name :min-width :required false :type "string" :validate-fn string? :description "a CSS width style. The minimum width to which the box can shrink"}
{:name :min-height :required false :type "string" :validate-fn string? :description "a CSS height style. The minimum height to which the box can shrink"}
{:name :max-width :required false :type "string" :validate-fn string? :description "a CSS width style. The maximum width to which the box can grow"}
{:name :max-height :required false :type "string" :validate-fn string? :description "a CSS height style. The maximum height to which the box can grow"}
{:name :justify :required false :default :start :type "keyword" :validate-fn justify-style? :description [:span "equivalent to CSS style " [:span.bold "justify-content"] "." [:br] "One of " justify-options-list]}
{:name :align :required false :default :stretch :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-items"] "." [:br] " One of " align-options-list]}
{:name :align-self :required false :type "keyword" :validate-fn align-style? :description [:span "equivalent to CSS style " [:span.bold "align-self"] "." [:br] "Used when a child must override the parent's align-items setting."]}
{:name :margin :required false :type "string" :validate-fn string? :description "a CSS margin style"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS padding style"}
{: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 scroller
"Returns hiccup which produces a scoller component.
This is the way scroll bars are added to boxes, in favour of adding the scroll attributes directly to the boxes themselves.
IMPORTANT: Because this component becomes the flex child in place of the component it is wrapping, you must copy the size attibutes to this componenet.
There are three scroll types:
- h-scroll Determines how the horizontal scroll bar will be displayed.
- v-scroll Determines how the vertical scroll bar will be displayed.
- scroll Sets both h-scroll and v-scroll at once.
Syntax: :auto [DEFAULT] Only show scroll bar(s) if the content is larger than the scroller.
:on Always show scroll bar(s).
:off Never show scroll bar(s). Content which is not in the bounds of the scroller can not be seen.
:spill Never show scroll bar(s). Content which is not in the bounds of the scroller spills all over the place.
Note: If scroll is set, then setting h-scroll or v-scroll overrides the scroll value"
[& {:keys [size scroll h-scroll v-scroll width height min-width min-height max-width max-height justify align align-self margin padding child class style attr]
:or {size "auto"}
:as args}]
{:pre [(validate-args-macro scroller-args-desc args "scroller")]}
(let [not-v-or-h (and (nil? v-scroll) (nil? h-scroll))
scroll (if (and (nil? scroll) not-v-or-h) :auto scroll)]
(box-base :size size
:scroll scroll
:h-scroll h-scroll
:v-scroll v-scroll
:width width
:height height
:min-width min-width
:min-height min-height
:max-width max-width
:max-height max-height
:justify justify
:align align
:align-self align-self
:margin margin
:padding padding
:child child
:class-name "rc-scroller "
:class class
:style style
:attr attr)))
;; ------------------------------------------------------------------------------------
;; Component: border
;; ------------------------------------------------------------------------------------
(def border-args-desc
[{:name :child :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "a component (or string)"}
{:name :border :required false :default "1px solid lightgrey" :type "string" :validate-fn string? :description "a CSS border style. A convenience to describe all borders in one parameter"}
{:name :l-border :required false :type "string" :validate-fn string? :description [:span "a CSS border style for the left border. Overrides " [:code ":border"]]}
{:name :r-border :required false :type "string" :validate-fn string? :description [:span "a CSS border style for the right border. Overrides " [:code ":border"]]}
{:name :t-border :required false :type "string" :validate-fn string? :description [:span "a CSS border style for the top border. Overrides " [:code ":border"]]}
{:name :b-border :required false :type "string" :validate-fn string? :description [:span "a CSS border style for the bottom. Overrides " [:code ":border"]]}
{:name :radius :required false :type "string" :validate-fn string? :description "a CSS radius style eg.\"2px\""}
{:name :size :required false :default "none" :type "string" :validate-fn string? :description [:span "equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS style describing the initial width"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS style describing the initial height"}
{:name :min-width :required false :type "string" :validate-fn string? :description "a CSS width style. The minimum width to which the box can shrink"}
{:name :min-height :required false :type "string" :validate-fn string? :description "a CSS height style. The minimum height to which the box can shrink"}
{:name :max-width :required false :type "string" :validate-fn string? :description "a CSS width style. The maximum width to which the box can grow"}
{:name :max-height :required false :type "string" :validate-fn string? :description "a CSS height style. The maximum height to which the box can grow"}
{:name :margin :required false :type "string" :validate-fn string? :description "a CSS margin style"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS padding style"}
{: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 border
"Returns hiccup which produces a border component.
This is the way borders are added to boxes, in favour of adding the border attributes directly to the boxes themselves.
border property syntax: '<border-width> || <border-style> || <color>'
- border-width: thin, medium, thick or standard CSS size (e.g. 2px, 0.5em)
- border-style: none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset
- color: standard CSS color (e.g. grey #88ffee)"
[& {:keys [size width height min-width min-height max-width max-height margin padding border l-border r-border t-border b-border radius child class style attr]
:or {size "none"}
:as args}]
{:pre [(validate-args-macro border-args-desc args "border")]}
(let [no-border (every? nil? [border l-border r-border t-border b-border])
default-border "1px solid lightgrey"]
(box-base :size size
:width width
:height height
:min-width min-width
:min-height min-height
:max-width max-width
:max-height max-height
:margin margin
:padding padding
:border (if no-border default-border border)
:l-border l-border
:r-border r-border
:t-border t-border
:b-border b-border
:radius radius
:child child
:class-name "rc-border "
:class class
:style style
:attr attr)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,404 @@
(ns re-com.buttons
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [deref-or-value px]]
[re-com.validate :refer [position? position-options-list button-size? button-sizes-list
string-or-hiccup? css-style? html-attr? string-or-atom?] :refer-macros [validate-args-macro]]
[re-com.popover :refer [popover-tooltip]]
[re-com.box :refer [h-box v-box box gap line flex-child-style]]
[reagent.core :as reagent]))
;; ------------------------------------------------------------------------------------
;; Component: button
;; ------------------------------------------------------------------------------------
(def button-args-desc
[{:name :label :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "label for the button"}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the button is clicked"}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, the user can't click the button"}
{: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"}
{: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 button
"Returns the markup for a basic button"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [label on-click tooltip tooltip-position disabled? class style attr]
:or {class "btn-default"}
:as args}]
{:pre [(validate-args-macro button-args-desc args "button")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [disabled? (deref-or-value disabled?)
the-button [:button
(merge
{:class (str "rc-button btn " class)
:style (merge
(flex-child-style "none")
style)
:disabled disabled?
:on-click (handler-fn
(when (and on-click (not disabled?))
(on-click event)))}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))})
attr)
label]]
(when disabled?
(reset! showing? false))
[box
:class "display-inline-flex"
:align :start
:child (if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)]))))
;;--------------------------------------------------------------------------------------------------
;; Component: md-circle-icon-button
;;--------------------------------------------------------------------------------------------------
(def md-circle-icon-button-args-desc
[{:name :md-icon-name :required true :default "zmdi-plus" :type "string" :validate-fn string? :description [:span "the name of the icon." [:br] "For example, " [:code "\"zmdi-plus\""] " or " [:code "\"zmdi-undo\""]] }
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the button is clicked"}
{:name :size :required false :default :regular :type "keyword" :validate-fn button-size? :description [:span "one of " button-sizes-list]}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :emphasise? :required false :default false :type "boolean" :description "if true, use emphasised styling so the button really stands out"}
{:name :disabled? :required false :default false :type "boolean" :description "if true, the user can't click the button"}
{: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 md-circle-icon-button
"a circular button containing a material design icon"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [md-icon-name on-click size tooltip tooltip-position emphasise? disabled? class style attr]
:or {md-icon-name "zmdi-plus"}
:as args}]
{:pre [(validate-args-macro md-circle-icon-button-args-desc args "md-circle-icon-button")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [the-button [:div
(merge
{:class (str
"rc-md-circle-icon-button noselect "
(case size
:smaller "rc-circle-smaller "
:larger "rc-circle-larger "
" ")
(when emphasise? "rc-circle-emphasis ")
(when disabled? "rc-circle-disabled ")
class)
:style (merge
{:cursor (when-not disabled? "pointer")}
style)
:on-click (handler-fn
(when (and on-click (not disabled?))
(on-click event)))}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))})
attr)
[:i {:class (str "zmdi zmdi-hc-fw-rc " md-icon-name)}]]]
(if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)))))
;;--------------------------------------------------------------------------------------------------
;; Component: md-icon-button
;;--------------------------------------------------------------------------------------------------
(def md-icon-button-args-desc
[{:name :md-icon-name :required true :default "zmdi-plus" :type "string" :validate-fn string? :description [:span "the name of the icon." [:br] "For example, " [:code "\"zmdi-plus\""] " or " [:code "\"zmdi-undo\""]]}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the button is clicked"}
{:name :size :required false :default :regular :type "keyword" :validate-fn button-size? :description [:span "one of " button-sizes-list]}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :emphasise? :required false :default false :type "boolean" :description "if true, use emphasised styling so the button really stands out"}
{:name :disabled? :required false :default false :type "boolean" :description "if true, the user can't click the button"}
{: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 md-icon-button
"a square button containing a material design icon"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [md-icon-name on-click size tooltip tooltip-position emphasise? disabled? class style attr]
:or {md-icon-name "zmdi-plus"}
:as args}]
{:pre [(validate-args-macro md-icon-button-args-desc args "md-icon-button")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [the-button [:div
(merge
{:class (str
"rc-md-icon-button noselect "
(case size
:smaller "rc-icon-smaller "
:larger "rc-icon-larger "
" ")
(when emphasise? "rc-icon-emphasis ")
(when disabled? "rc-icon-disabled ")
class)
:style (merge
{:cursor (when-not disabled? "pointer")}
style)
:on-click (handler-fn
(when (and on-click (not disabled?))
(on-click event)))}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))})
attr)
[:i {:class (str "zmdi zmdi-hc-fw-rc " md-icon-name)}]]]
(if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)))))
;;--------------------------------------------------------------------------------------------------
;; Component: info-button
;;--------------------------------------------------------------------------------------------------
(def info-button-args-desc
[{:name :info :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "what's shown in the popover"}
{:name :position :required false :default :right-below :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :width :required false :default "250px" :type "string" :validate-fn string? :description "width in px"}
{: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 info-button
"A tiny light grey button, with an 'i' in it. Meant to be unobrusive.
When pressed, displays a popup assumidly containing helpful information.
Primarily designed to be nestled against the label of an input field, explaining the purpose of that field.
Create a very small \"i\" icon via SVG"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [info position width class style attr] :as args}]
{:pre [(validate-args-macro info-button-args-desc args "info-button")]}
[popover-tooltip
:label info
:status :info
:position (if position position :right-below)
:width (if width width "250px")
:showing? showing?
:on-cancel #(swap! showing? not)
:anchor [:div
(merge
{:class (str "rc-info-button noselect " class)
:style (merge {:cursor "pointer"} style)
:on-click (handler-fn (swap! showing? not))}
attr)
[:svg {:width "11" :height "11"}
[:circle {:cx "5.5" :cy "5.5" :r "5.5"}]
[:circle {:cx "5.5" :cy "2.5" :r "1.4" :fill "white"}]
[:line {:x1 "5.5" :y1 "5.2" :x2 "5.5" :y2 "9.7" :stroke "white" :stroke-width "2.5"}]]]])))
;;--------------------------------------------------------------------------------------------------
;; Component: row-button
;;--------------------------------------------------------------------------------------------------
(def row-button-args-desc
[{:name :md-icon-name :required true :default "zmdi-plus" :type "string" :validate-fn string? :description [:span "the name of the icon." [:br] "For example, " [:code "\"zmdi-plus\""] " or " [:code "\"zmdi-undo\""]]}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the button is clicked"}
{:name :mouse-over-row? :required false :default false :type "boolean" :description "true if the mouse is hovering over the row"}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :disabled? :required false :default false :type "boolean" :description "if true, the user can't click the button"}
{: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 row-button
"a small button containing a material design icon"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [md-icon-name on-click mouse-over-row? tooltip tooltip-position disabled? class style attr]
:or {md-icon-name "zmdi-plus"}
:as args}]
{:pre [(validate-args-macro row-button-args-desc args "row-button")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [the-button [:div
(merge
{:class (str
"rc-row-button noselect "
(when mouse-over-row? "rc-row-mouse-over-row ")
(when disabled? "rc-row-disabled ")
class)
:style style
:on-click (handler-fn
(when (and on-click (not disabled?))
(on-click event)))}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))}) ;; Need to return true to ALLOW default events to be performed
attr)
[:i {:class (str "zmdi zmdi-hc-fw-rc " md-icon-name)}]]]
(if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)))))
;;--------------------------------------------------------------------------------------------------
;; Component: hyperlink
;;--------------------------------------------------------------------------------------------------
(def hyperlink-args-desc
[{:name :label :required true :type "string | hiccup | atom" :validate-fn string-or-hiccup? :description "label/hiccup for the button"}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the button is clicked"}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, the user can't click the button"}
{: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 hyperlink
"Renders an underlined text hyperlink component.
This is very similar to the button component above but styled to looks like a hyperlink.
Useful for providing button functionality for less important functions, e.g. Cancel"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [label on-click tooltip tooltip-position disabled? class style attr] :as args}]
{:pre [(validate-args-macro hyperlink-args-desc args "hyperlink")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [label (deref-or-value label)
disabled? (deref-or-value disabled?)
the-button [box
:align :start
:child [:a
(merge
{:class (str "rc-hyperlink noselect " class)
:style (merge
(flex-child-style "none")
{:cursor (if disabled? "not-allowed" "pointer")
:color (when disabled? "grey")}
style)
:on-click (handler-fn
(when (and on-click (not disabled?))
(on-click event)))}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))})
attr)
label]]]
(if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)))))
;;--------------------------------------------------------------------------------------------------
;; Component: hyperlink-href
;;--------------------------------------------------------------------------------------------------
(def hyperlink-href-args-desc
[{:name :label :required true :type "string | hiccup | atom" :validate-fn string-or-hiccup? :description "label/hiccup for the button"}
{:name :href :required true :type "string | atom" :validate-fn string-or-atom? :description "if specified, the link target URL"}
{:name :target :required false :default "_self" :type "string | atom" :validate-fn string-or-atom? :description "one of \"_self\" or \"_blank\""}
{:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"}
{:name :tooltip-position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{: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 hyperlink-href
"Renders an underlined text hyperlink component.
This is very similar to the button component above but styled to looks like a hyperlink.
Useful for providing button functionality for less important functions, e.g. Cancel"
[]
(let [showing? (reagent/atom false)]
(fn
[& {:keys [label href target tooltip tooltip-position class style attr] :as args}]
{:pre [(validate-args-macro hyperlink-href-args-desc args "hyperlink-href")]}
(when-not tooltip (reset! showing? false)) ;; To prevent tooltip from still showing after button drag/drop
(let [label (deref-or-value label)
href (deref-or-value href)
target (deref-or-value target)
the-button [:a
(merge {:class (str "rc-hyperlink-href noselect " class)
:style (merge (flex-child-style "none")
style)
:href href
:target target}
(when tooltip
{:on-mouse-over (handler-fn (reset! showing? true))
:on-mouse-out (handler-fn (reset! showing? false))})
attr)
label]]
(if tooltip
[popover-tooltip
:label tooltip
:position (if tooltip-position tooltip-position :below-center)
:showing? showing?
:anchor the-button]
the-button)))))
;; TODO: Eventually remove
;;----------------------------------------------------------------------
;; Round button with no dependencies - for use in re-frame demo
;;----------------------------------------------------------------------
#_(defn round-button
"a circular button containing a material design icon"
[]
(let [mouse-over? (reagent/atom false)]
(fn
[& {:keys [md-icon-name on-click disabled? style attr]
:or {md-icon-name "md-add"}}]
[:div
(merge
{:style (merge
{:cursor (when-not disabled? "pointer")
:font-size "24px"
:width "40px"
:height "40px"
:line-height "44px"
:text-align "center"
:-webkit-user-select "none"}
(if disabled?
{:color "lightgrey"}
{:border (str "1px solid " (if @mouse-over? "#428bca" "lightgrey"))
:color (when @mouse-over? "#428bca")
:border-radius "50%"})
style)
:on-mouse-over #(do (reset! mouse-over? true) nil)
:on-mouse-out #(do (reset! mouse-over? false) nil)
:on-click #(when (and on-click (not disabled?))
(on-click %)
nil)}
attr)
[:i {:class md-icon-name}]])))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,89 @@
(ns re-com.core
(:require [re-com.alert :as alert]
[re-com.box :as box]
[re-com.buttons :as buttons]
[re-com.datepicker :as datepicker]
[re-com.dropdown :as dropdown]
[re-com.typeahead :as typeahead]
[re-com.input-time :as input-time]
[re-com.splits :as splits]
[re-com.misc :as misc]
[re-com.modal-panel :as modal-panel]
[re-com.popover :as popover]
[re-com.selection-list :as selection-list]
[re-com.tabs :as tabs]
[re-com.text :as text]
[re-com.tour :as tour]))
;; -----------------------------------------------------------------------------
;; re-com public API (see also re-com.util)
;; -----------------------------------------------------------------------------
(def alert-box alert/alert-box)
(def alert-list alert/alert-list)
(def flex-child-style box/flex-child-style)
(def flex-flow-style box/flex-flow-style)
(def justify-style box/justify-style)
(def align-style box/align-style)
(def scroll-style box/scroll-style)
(def h-box box/h-box)
(def v-box box/v-box)
(def box box/box)
(def line box/line)
(def gap box/gap)
(def scroller box/scroller)
(def border box/border)
(def button buttons/button)
(def md-circle-icon-button buttons/md-circle-icon-button)
(def md-icon-button buttons/md-icon-button)
(def info-button buttons/info-button)
(def row-button buttons/row-button)
(def hyperlink buttons/hyperlink)
(def hyperlink-href buttons/hyperlink-href)
(def datepicker datepicker/datepicker)
(def datepicker-dropdown datepicker/datepicker-dropdown)
(def single-dropdown dropdown/single-dropdown)
(def typeahead typeahead/typeahead)
(def input-time input-time/input-time)
(def h-split splits/h-split)
(def v-split splits/v-split)
(def input-text misc/input-text)
(def input-password misc/input-password)
(def input-textarea misc/input-textarea)
(def checkbox misc/checkbox)
(def radio-button misc/radio-button)
(def slider misc/slider)
(def progress-bar misc/progress-bar)
(def throbber misc/throbber)
(def modal-panel modal-panel/modal-panel)
(def popover-content-wrapper popover/popover-content-wrapper)
(def popover-anchor-wrapper popover/popover-anchor-wrapper)
(def popover-border popover/popover-border)
(def popover-tooltip popover/popover-tooltip)
(def selection-list selection-list/selection-list)
(def horizontal-tabs tabs/horizontal-tabs)
(def horizontal-bar-tabs tabs/horizontal-bar-tabs)
(def vertical-bar-tabs tabs/vertical-bar-tabs)
(def horizontal-pill-tabs tabs/horizontal-pill-tabs)
(def vertical-pill-tabs tabs/vertical-pill-tabs)
(def label text/label)
(def title text/title)
(def p text/p)
(def make-tour tour/make-tour)
(def start-tour tour/start-tour)
(def make-tour-nav tour/make-tour-nav)

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,73 @@
// Compiled by ClojureScript 1.9.229 {}
goog.provide('re_com.core');
goog.require('cljs.core');
goog.require('re_com.modal_panel');
goog.require('re_com.text');
goog.require('re_com.popover');
goog.require('re_com.box');
goog.require('re_com.buttons');
goog.require('re_com.splits');
goog.require('re_com.selection_list');
goog.require('re_com.tabs');
goog.require('re_com.typeahead');
goog.require('re_com.datepicker');
goog.require('re_com.alert');
goog.require('re_com.dropdown');
goog.require('re_com.input_time');
goog.require('re_com.tour');
goog.require('re_com.misc');
re_com.core.alert_box = re_com.alert.alert_box;
re_com.core.alert_list = re_com.alert.alert_list;
re_com.core.flex_child_style = re_com.box.flex_child_style;
re_com.core.flex_flow_style = re_com.box.flex_flow_style;
re_com.core.justify_style = re_com.box.justify_style;
re_com.core.align_style = re_com.box.align_style;
re_com.core.scroll_style = re_com.box.scroll_style;
re_com.core.h_box = re_com.box.h_box;
re_com.core.v_box = re_com.box.v_box;
re_com.core.box = re_com.box.box;
re_com.core.line = re_com.box.line;
re_com.core.gap = re_com.box.gap;
re_com.core.scroller = re_com.box.scroller;
re_com.core.border = re_com.box.border;
re_com.core.button = re_com.buttons.button;
re_com.core.md_circle_icon_button = re_com.buttons.md_circle_icon_button;
re_com.core.md_icon_button = re_com.buttons.md_icon_button;
re_com.core.info_button = re_com.buttons.info_button;
re_com.core.row_button = re_com.buttons.row_button;
re_com.core.hyperlink = re_com.buttons.hyperlink;
re_com.core.hyperlink_href = re_com.buttons.hyperlink_href;
re_com.core.datepicker = re_com.datepicker.datepicker;
re_com.core.datepicker_dropdown = re_com.datepicker.datepicker_dropdown;
re_com.core.single_dropdown = re_com.dropdown.single_dropdown;
re_com.core.typeahead = re_com.typeahead.typeahead;
re_com.core.input_time = re_com.input_time.input_time;
re_com.core.h_split = re_com.splits.h_split;
re_com.core.v_split = re_com.splits.v_split;
re_com.core.input_text = re_com.misc.input_text;
re_com.core.input_password = re_com.misc.input_password;
re_com.core.input_textarea = re_com.misc.input_textarea;
re_com.core.checkbox = re_com.misc.checkbox;
re_com.core.radio_button = re_com.misc.radio_button;
re_com.core.slider = re_com.misc.slider;
re_com.core.progress_bar = re_com.misc.progress_bar;
re_com.core.throbber = re_com.misc.throbber;
re_com.core.modal_panel = re_com.modal_panel.modal_panel;
re_com.core.popover_content_wrapper = re_com.popover.popover_content_wrapper;
re_com.core.popover_anchor_wrapper = re_com.popover.popover_anchor_wrapper;
re_com.core.popover_border = re_com.popover.popover_border;
re_com.core.popover_tooltip = re_com.popover.popover_tooltip;
re_com.core.selection_list = re_com.selection_list.selection_list;
re_com.core.horizontal_tabs = re_com.tabs.horizontal_tabs;
re_com.core.horizontal_bar_tabs = re_com.tabs.horizontal_bar_tabs;
re_com.core.vertical_bar_tabs = re_com.tabs.vertical_bar_tabs;
re_com.core.horizontal_pill_tabs = re_com.tabs.horizontal_pill_tabs;
re_com.core.vertical_pill_tabs = re_com.tabs.vertical_pill_tabs;
re_com.core.label = re_com.text.label;
re_com.core.title = re_com.text.title;
re_com.core.p = re_com.text.p;
re_com.core.make_tour = re_com.tour.make_tour;
re_com.core.start_tour = re_com.tour.start_tour;
re_com.core.make_tour_nav = re_com.tour.make_tour_nav;
//# sourceMappingURL=core.js.map?rel=1603199197089

View file

@ -0,0 +1 @@
{"version":3,"file":"\/Users\/simon\/workspace\/swinging-needle-meter\/docs\/js\/compiled\/out\/re_com\/core.js","sources":["core.cljs?rel=1603199197090"],"lineCount":73,"mappings":";AAAA;;;;;;;;;;;;;;;;;AAqBA,AAAKA,wBAA2BC;AAChC,AAAKC,yBAA2BC;AAEhC,AAAKC,+BAA2BC;AAChC,AAAKC,8BAA2BC;AAChC,AAAKC,4BAA2BC;AAChC,AAAKC,0BAA2BC;AAChC,AAAKC,2BAA2BC;AAEhC,AAAKC,oBAA2BC;AAChC,AAAKC,oBAA2BC;AAChC,AAAKC,kBAA2BC;AAChC,AAAKC,mBAA2BC;AAChC,AAAKC,kBAA2BC;AAChC,AAAKC,uBAA2BC;AAChC,AAAKC,qBAA2BC;AAEhC,AAAKC,qBAA2BC;AAChC,AAAKC,oCAA2BC;AAChC,AAAKC,6BAA2BC;AAChC,AAAKC,0BAA2BC;AAChC,AAAKC,yBAA2BC;AAChC,AAAKC,wBAA2BC;AAChC,AAAKC,6BAA2BC;AAEhC,AAAKC,yBAA2BC;AAChC,AAAKC,kCAA2BC;AAEhC,AAAKC,8BAA2BC;AAEhC,AAAKC,wBAA2BC;AAEhC,AAAKC,yBAA2BC;AAEhC,AAAKC,sBAA2BC;AAChC,AAAKC,sBAA2BC;AAEhC,AAAKC,yBAA2BC;AAChC,AAAKC,6BAA2BC;AAChC,AAAKC,6BAA2BC;AAChC,AAAKC,uBAA2BC;AAChC,AAAKC,2BAA2BC;AAChC,AAAKC,qBAA2BC;AAChC,AAAKC,2BAA2BC;AAChC,AAAKC,uBAA2BC;AAEhC,AAAKC,0BAA2BC;AAEhC,AAAKC,sCAA2BC;AAChC,AAAKC,qCAA2BC;AAChC,AAAKC,6BAA2BC;AAChC,AAAKC,8BAA2BC;AAEhC,AAAKC,6BAA2BC;AAEhC,AAAKC,8BAA2BC;AAChC,AAAKC,kCAA2BC;AAChC,AAAKC,gCAA2BC;AAChC,AAAKC,mCAA2BC;AAChC,AAAKC,iCAA2BC;AAEhC,AAAKC,oBAA2BC;AAChC,AAAKC,oBAA2BC;AAChC,AAAKC,gBAA2BC;AAEhC,AAAKC,wBAA2BC;AAChC,AAAKC,yBAA2BC;AAChC,AAAKC,4BAA2BC","names":["re-com.core\/alert-box","re-com.alert\/alert-box","re-com.core\/alert-list","re-com.alert\/alert-list","re-com.core\/flex-child-style","re-com.box\/flex-child-style","re-com.core\/flex-flow-style","re-com.box\/flex-flow-style","re-com.core\/justify-style","re-com.box\/justify-style","re-com.core\/align-style","re-com.box\/align-style","re-com.core\/scroll-style","re-com.box\/scroll-style","re-com.core\/h-box","re-com.box\/h-box","re-com.core\/v-box","re-com.box\/v-box","re-com.core\/box","re-com.box\/box","re-com.core\/line","re-com.box\/line","re-com.core\/gap","re-com.box\/gap","re-com.core\/scroller","re-com.box\/scroller","re-com.core\/border","re-com.box\/border","re-com.core\/button","re-com.buttons\/button","re-com.core\/md-circle-icon-button","re-com.buttons\/md-circle-icon-button","re-com.core\/md-icon-button","re-com.buttons\/md-icon-button","re-com.core\/info-button","re-com.buttons\/info-button","re-com.core\/row-button","re-com.buttons\/row-button","re-com.core\/hyperlink","re-com.buttons\/hyperlink","re-com.core\/hyperlink-href","re-com.buttons\/hyperlink-href","re-com.core\/datepicker","re-com.datepicker\/datepicker","re-com.core\/datepicker-dropdown","re-com.datepicker\/datepicker-dropdown","re-com.core\/single-dropdown","re-com.dropdown\/single-dropdown","re-com.core\/typeahead","re-com.typeahead\/typeahead","re-com.core\/input-time","re-com.input-time\/input-time","re-com.core\/h-split","re-com.splits\/h-split","re-com.core\/v-split","re-com.splits\/v-split","re-com.core\/input-text","re-com.misc\/input-text","re-com.core\/input-password","re-com.misc\/input-password","re-com.core\/input-textarea","re-com.misc\/input-textarea","re-com.core\/checkbox","re-com.misc\/checkbox","re-com.core\/radio-button","re-com.misc\/radio-button","re-com.core\/slider","re-com.misc\/slider","re-com.core\/progress-bar","re-com.misc\/progress-bar","re-com.core\/throbber","re-com.misc\/throbber","re-com.core\/modal-panel","re-com.modal-panel\/modal-panel","re-com.core\/popover-content-wrapper","re-com.popover\/popover-content-wrapper","re-com.core\/popover-anchor-wrapper","re-com.popover\/popover-anchor-wrapper","re-com.core\/popover-border","re-com.popover\/popover-border","re-com.core\/popover-tooltip","re-com.popover\/popover-tooltip","re-com.core\/selection-list","re-com.selection-list\/selection-list","re-com.core\/horizontal-tabs","re-com.tabs\/horizontal-tabs","re-com.core\/horizontal-bar-tabs","re-com.tabs\/horizontal-bar-tabs","re-com.core\/vertical-bar-tabs","re-com.tabs\/vertical-bar-tabs","re-com.core\/horizontal-pill-tabs","re-com.tabs\/horizontal-pill-tabs","re-com.core\/vertical-pill-tabs","re-com.tabs\/vertical-pill-tabs","re-com.core\/label","re-com.text\/label","re-com.core\/title","re-com.text\/title","re-com.core\/p","re-com.text\/p","re-com.core\/make-tour","re-com.tour\/make-tour","re-com.core\/start-tour","re-com.tour\/start-tour","re-com.core\/make-tour-nav","re-com.tour\/make-tour-nav"]}

View file

@ -0,0 +1,290 @@
(ns re-com.datepicker
(:require-macros [re-com.core :refer [handler-fn]])
(:require
[reagent.core :as reagent]
[cljs-time.core :refer [now minus plus months days year month day day-of-week first-day-of-the-month before? after?]]
[re-com.validate :refer [goog-date? css-style? html-attr?] :refer-macros [validate-args-macro]]
[cljs-time.predicates :refer [sunday?]]
[cljs-time.format :refer [parse unparse formatters formatter]]
[re-com.box :refer [border h-box flex-child-style]]
[re-com.util :refer [deref-or-value now->utc]]
[re-com.popover :refer [popover-anchor-wrapper popover-content-wrapper]]))
;; Loosely based on ideas: https://github.com/dangrossman/bootstrap-daterangepicker
;; --- cljs-time facades ------------------------------------------------------
(def ^:const month-format (formatter "MMMM yyyy"))
(def ^:const week-format (formatter "ww"))
(def ^:const date-format (formatter "yyyy MMM dd"))
(defn iso8601->date [iso8601]
(when (seq iso8601)
(parse (formatters :basic-date) iso8601)))
(defn- month-label [date] (unparse month-format date))
(defn- dec-month [date] (minus date (months 1)))
(defn- inc-month [date] (plus date (months 1)))
(defn- inc-date [date n] (plus date (days n)))
(defn previous
"If date fails pred, subtract period until true, otherwise answer date"
;; date - a goog.date.UtcDateTime now if ommited
;; pred - can be one of cljs-time.predicate e.g. sunday? but anything that can deal with goog.date.UtcDateTime
;; period - a period which will be subtracted see cljs-time.core periods
;; Note: If period and pred do not represent same granularity, some steps may be skipped
; e.g Pass a Wed date, specify sunday? as pred and a period (days 2) will skip one Sunday.
([pred]
(previous pred (now->utc)))
([pred date]
(previous pred date (days 1)))
([pred date period]
(if (pred date)
date
(recur pred (minus date period) period))))
(defn- =date [date1 date2]
(and
(= (year date1) (year date2))
(= (month date1) (month date2))
(= (day date1) (day date2))))
(defn- <=date [date1 date2]
(or (=date date1 date2) (before? date1 date2)))
(defn- >=date [date1 date2]
(or (=date date1 date2) (after? date1 date2)))
(def ^:private days-vector
[{:key :Mo :short-name "M" :name "MON"}
{:key :Tu :short-name "T" :name "TUE"}
{:key :We :short-name "W" :name "WED"}
{:key :Th :short-name "T" :name "THU"}
{:key :Fr :short-name "F" :name "FRI"}
{:key :Sa :short-name "S" :name "SAT"}
{:key :Su :short-name "S" :name "SUN"}])
(defn- rotate
[n coll]
(let [c (count coll)]
(take c (drop (mod n c) (cycle coll)))))
(defn- is-day-pred [d]
#(= (day-of-week %) (inc d)))
;; ----------------------------------------------------------------------------
(defn- main-div-with
[table-div hide-border? class style attr]
;;extra h-box is currently necessary so that calendar & border do not strecth to width of any containing v-box
[h-box
:children [[border
:radius "4px"
:size "none"
:border (when hide-border? "none")
:child [:div
(merge
{:class (str "rc-datepicker datepicker noselect " class)
;; override inherrited body larger 14px font-size
;; override position from css because we are inline
:style (merge {:font-size "13px"
:position "static"}
style)}
attr)
table-div]]]])
(defn- table-thead
"Answer 2 x rows showing month with nav buttons and days NOTE: not internationalized"
[display-month {show-weeks? :show-weeks? minimum :minimum maximum :maximum start-of-week :start-of-week}]
(let [prev-date (dec-month @display-month)
;prev-enabled? (if minimum (after? prev-date (dec-month minimum)) true)
prev-enabled? (if minimum (after? prev-date minimum) true)
next-date (inc-month @display-month)
next-enabled? (if maximum (before? next-date maximum) true)
template-row (if show-weeks? [:tr [:th]] [:tr])]
[:thead
(conj template-row
[:th {:class (str "prev " (if prev-enabled? "available selectable" "disabled"))
:style {:padding "0px"}
:on-click (handler-fn (when prev-enabled? (reset! display-month prev-date)))}
[:i.zmdi.zmdi-chevron-left
{:style {:font-size "24px"}}]]
[:th {:class "month" :col-span "5"} (month-label @display-month)]
[:th {:class (str "next " (if next-enabled? "available selectable" "disabled"))
:style {:padding "0px"}
:on-click (handler-fn (when next-enabled? (reset! display-month next-date)))}
[:i.zmdi.zmdi-chevron-right
{:style {:font-size "24px"}}]])
(conj template-row
(for [day (rotate start-of-week days-vector)]
^{:key (:key day)} [:th {:class "day-enabled"} (str (:name day))]))]))
(defn- selection-changed
[selection change-callback]
(change-callback selection))
(defn- table-td
[date focus-month selected today {minimum :minimum maximum :maximum :as attributes} disabled? on-change]
;;following can be simplified and terse
(let [enabled-min (if minimum (>=date date minimum) true)
enabled-max (if maximum (<=date date maximum) true)
enabled-day (and enabled-min enabled-max)
disabled-day? (if enabled-day
(not ((:selectable-fn attributes) date))
true)
classes (cond disabled? "off"
disabled-day? "off"
(= focus-month (month date)) "available"
:else "available off")
classes (cond (and selected (=date selected date)) (str classes " active start-date end-date")
(and today (=date date today)) (str classes " today")
:else classes)
on-click #(when-not (or disabled? disabled-day?) (selection-changed date on-change))]
[:td {:class classes
:on-click (handler-fn (on-click))} (day date)]))
(defn- week-td [date]
[:td {:class "week"} (unparse week-format date)])
(defn- table-tr
"Return 7 columns of date cells from date inclusive"
[date focus-month selected attributes disabled? on-change]
; {:pre [(sunday? date)]}
(let [table-row (if (:show-weeks? attributes) [:tr (week-td date)] [:tr])
row-dates (map #(inc-date date %) (range 7))
today (if (:show-today? attributes) (:today attributes) nil)]
(into table-row (map #(table-td % focus-month selected today attributes disabled? on-change) row-dates))))
(defn- table-tbody
"Return matrix of 6 rows x 7 cols table cells representing 41 days from start-date inclusive"
[display-month selected attributes disabled? on-change]
(let [start-of-week (:start-of-week attributes)
current-start (previous (is-day-pred start-of-week) display-month)
focus-month (month display-month)
row-start-dates (map #(inc-date current-start (* 7 %)) (range 6))]
(into [:tbody] (map #(table-tr % focus-month selected attributes disabled? on-change) row-start-dates))))
(defn- configure
"Augment passed attributes with extra info/defaults"
[attributes]
(let [selectable-fn (if (-> attributes :selectable-fn fn?)
(:selectable-fn attributes)
(fn [date] true))]
(merge attributes {:selectable-fn selectable-fn :today (now->utc)})))
(def datepicker-args-desc
[{:name :model :required false :type "goog.date.UtcDateTime | atom" :validate-fn goog-date? :description "the selected date. If provided, should pass pred :selectable-fn"}
{:name :on-change :required true :type "goog.date.UtcDateTime -> nil" :validate-fn fn? :description "called when a new selection is made"}
{:name :disabled? :required false :default false :type "boolean | atom" :description "when true, the can't select dates but can navigate"}
{:name :selectable-fn :required false :default "(fn [date] true)" :type "pred" :validate-fn fn? :description "Predicate is passed a date. If it answers false, day will be shown disabled and can't be selected."}
{:name :show-weeks? :required false :default false :type "boolean" :description "when true, week numbers are shown to the left"}
{:name :show-today? :required false :default false :type "boolean" :description "when true, today's date is highlighted"}
{:name :minimum :required false :type "goog.date.UtcDateTime" :validate-fn goog-date? :description "no selection or navigation before this date"}
{:name :maximum :required false :type "goog.date.UtcDateTime" :validate-fn goog-date? :description "no selection or navigation after this date"}
{:name :start-of-week :required false :default 6 :type "int" :description "first day of week (Monday = 0 ... Sunday = 6)"}
{:name :hide-border? :required false :default false :type "boolean" :description "when true, the border is not displayed"}
{: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 datepicker
[& {:keys [model] :as args}]
{:pre [(validate-args-macro datepicker-args-desc args "datepicker")]}
(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 @external-model) ;; Create a new atom from the model to be used internally
display-month (reagent/atom (first-day-of-the-month (or @internal-model (now))))]
(fn datepicker-component
[& {:keys [model disabled? hide-border? on-change start-of-week class style attr]
:or {start-of-week 6} ;; Default to Sunday
:as properties}]
{:pre [(validate-args-macro datepicker-args-desc properties "datepicker")]}
(let [latest-ext-model (deref-or-value model)
props-with-defaults (merge properties {:start-of-week start-of-week})
configuration (configure props-with-defaults)]
(when (not= @external-model latest-ext-model) ;; Has model changed externally?
(reset! external-model latest-ext-model)
(reset! internal-model latest-ext-model)
(reset! display-month (first-day-of-the-month (or @internal-model (now)))))
[main-div-with
[:table {:class "table-condensed"}
[table-thead display-month configuration]
[table-tbody
@display-month
@internal-model
configuration
(if (nil? disabled?) false (deref-or-value disabled?))
on-change]]
hide-border?
class
style
attr]))))
(defn- anchor-button
"Provide clickable field with current date label and dropdown button e.g. [ 2014 Sep 17 | # ]"
[shown? model format]
[:div {:class "input-group display-flex noselect"
:style (flex-child-style "none")
:on-click (handler-fn (swap! shown? not))}
[h-box
:align :center
:class "noselect"
:min-width "10em"
:children [[:label {:class "form-control dropdown-button"}
(if (instance? js/goog.date.Date (deref-or-value model))
(unparse (if (seq format) (formatter format) date-format) (deref-or-value model))
"")]
[:span.dropdown-button.activator.input-group-addon
{:style {:padding "3px 0px 0px 0px"}}
[:i.zmdi.zmdi-apps {:style {:font-size "24px"}}]]]]])
(def datepicker-dropdown-args-desc
(conj datepicker-args-desc
{:name :format :required false :default "yyyy MMM dd" :type "string" :description "[datepicker-dropdown only] a represenatation of a date format. See cljs_time.format"}
{:name :no-clip? :required false :default true :type "boolean" :description "[datepicker-dropdown only] when an anchor is in a scrolling region (e.g. scroller component), the popover can sometimes be clipped. When this parameter is true (which is the default), re-com will use a different CSS method to show the popover. This method is slightly inferior because the popover can't track the anchor if it is repositioned"}))
(defn datepicker-dropdown
[& {:as args}]
{:pre [(validate-args-macro datepicker-dropdown-args-desc args "datepicker-dropdown")]}
(let [shown? (reagent/atom false)
cancel-popover #(reset! shown? false)
position :below-left]
(fn
[& {:keys [model show-weeks? on-change format no-clip?]
:or {no-clip? true}
:as passthrough-args}]
(let [collapse-on-select (fn [new-model]
(reset! shown? false)
(when on-change (on-change new-model))) ;; wrap callback to collapse popover
passthrough-args (dissoc passthrough-args :format :no-clip?) ;; :format and :no-clip? only valid at this API level
passthrough-args (->> (assoc passthrough-args :on-change collapse-on-select)
(merge {:hide-border? true}) ;; apply defaults
vec
flatten)]
[popover-anchor-wrapper
:showing? shown?
:position position
:anchor [anchor-button shown? model format]
:popover [popover-content-wrapper
:position-offset (if show-weeks? 43 44)
:no-clip? no-clip?
:arrow-length 0
:arrow-width 0
:arrow-gap 3
:padding "0px"
:on-cancel cancel-popover
:body (into [datepicker] passthrough-args)]]))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,324 @@
(ns re-com.dropdown
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [deref-or-value position-for-id item-for-id]]
[re-com.box :refer [align-style flex-child-style]]
[re-com.validate :refer [vector-of-maps? css-style? html-attr? number-or-string?] :refer-macros [validate-args-macro]]
[clojure.string :as string]
[reagent.core :as reagent]))
;; Inspiration: http://alxlit.name/bootstrap-chosen
;; Alternative: http://silviomoreto.github.io/bootstrap-select
(defn- move-to-new-choice
"In a vector of maps (where each map has an :id), return the id of the choice offset posititions away
from id (usually +1 or -1 to go to next/previous). Also accepts :start and :end"
[choices id-fn id offset]
(let [current-index (position-for-id id choices :id-fn id-fn)
new-index (cond
(= offset :start) 0
(= offset :end) (dec (count choices))
(nil? current-index) 0
:else (mod (+ current-index offset) (count choices)))]
(when (and new-index (pos? (count choices)))
(id-fn (nth choices new-index)))))
(defn- choices-with-group-headings
"If necessary, inserts group headings entries into the choices"
[opts group-fn]
(let [groups (partition-by group-fn opts)
group-headers (->> groups
(map first)
(map group-fn)
(map #(hash-map :id (gensym) :group %)))]
[group-headers groups]))
(defn- filter-choices
"Filter a list of choices based on a filter string using plain string searches (case insensitive). Less powerful
than regex's but no confusion with reserved characters"
[choices group-fn label-fn filter-text]
(let [lower-filter-text (string/lower-case filter-text)
filter-fn (fn [opt]
(let [group (if (nil? (group-fn opt)) "" (group-fn opt))
label (str (label-fn opt))]
(or
(>= (.indexOf (string/lower-case group) lower-filter-text) 0)
(>= (.indexOf (string/lower-case label) lower-filter-text) 0))))]
(filter filter-fn choices)))
(defn- filter-choices-regex
"Filter a list of choices based on a filter string using regex's (case insensitive). More powerful but can cause
confusion for users entering reserved characters such as [ ] * + . ( ) etc."
[choices group-fn label-fn filter-text]
(let [re (try
(js/RegExp. filter-text "i")
(catch js/Object e nil))
filter-fn (partial (fn [re opt]
(when-not (nil? re)
(or (.test re (group-fn opt)) (.test re (label-fn opt)))))
re)]
(filter filter-fn choices)))
(defn filter-choices-by-keyword
"Filter a list of choices extra data within the choices vector"
[choices keyword value]
(let [filter-fn (fn [opt] (>= (.indexOf (keyword opt) value) 0))]
(filter filter-fn choices)))
(defn show-selected-item
[node]
(let [item-offset-top (.-offsetTop node)
item-offset-bottom (+ item-offset-top (.-clientHeight node))
parent (.-parentNode node)
parent-height (.-clientHeight parent)
parent-visible-top (.-scrollTop parent)
parent-visible-bottom (+ parent-visible-top parent-height)
new-scroll-top (cond
(> item-offset-bottom parent-visible-bottom) (max (- item-offset-bottom parent-height) 0)
(< item-offset-top parent-visible-top) item-offset-top)]
(when new-scroll-top (set! (.-scrollTop parent) new-scroll-top))))
(defn- make-group-heading
"Render a group heading"
[m]
^{:key (:id m)} [:li.group-result
(:group m)])
(defn- choice-item
"Render a choice item and set up appropriate mouse events"
[id label on-click internal-model]
(let [mouse-over? (reagent/atom false)]
(reagent/create-class
{:component-did-mount
(fn [this]
(let [node (reagent/dom-node this)
selected (= @internal-model id)]
(when selected (show-selected-item node))))
:component-did-update
(fn [this]
(let [node (reagent/dom-node this)
selected (= @internal-model id)]
(when selected (show-selected-item node))))
:display-name "choice-item"
:reagent-render
(fn
[id label on-click internal-model]
(let [selected (= @internal-model id)
class (if selected
"highlighted"
(when @mouse-over? "mouseover"))]
[:li
{:class (str "active-result group-option " class)
:on-mouse-over (handler-fn (reset! mouse-over? true))
:on-mouse-out (handler-fn (reset! mouse-over? false))
:on-mouse-down (handler-fn (on-click id))}
label]))})))
(defn make-choice-item
[id-fn render-fn callback internal-model opt]
(let [id (id-fn opt)
markup (render-fn opt)]
^{:key (str id)} [choice-item id markup callback internal-model]))
(defn- filter-text-box-base
"Base function (before lifecycle metadata) to render a filter text box"
[]
(fn [filter-box? filter-text key-handler drop-showing?]
[:div.chosen-search
[:input
{:type "text"
:auto-complete "off"
:style (when-not filter-box? {:position "absolute" ;; When no filter box required, use it but hide it off screen
:width "0px" ;; The rest of these styles make the textbox invisible
:padding "0px"
:border "none"})
:value @filter-text
:on-change (handler-fn (reset! filter-text (-> event .-target .-value)))
:on-key-down (handler-fn (when-not (key-handler event)
(.preventDefault event))) ;; When key-handler returns false, preventDefault
:on-blur (handler-fn (reset! drop-showing? false))}]]))
(def ^:private filter-text-box
"Render a filter text box"
(with-meta filter-text-box-base
{:component-did-mount #(let [node (.-firstChild (reagent/dom-node %))]
(.focus node))
:component-did-update #(let [node (.-firstChild (reagent/dom-node %))]
(.focus node))}))
(defn- dropdown-top
"Render the top part of the dropdown, with the clickable area and the up/down arrow"
[]
(let [ignore-click (atom false)]
(fn
[internal-model choices id-fn label-fn tab-index placeholder dropdown-click key-handler filter-box? drop-showing? title?]
(let [_ (reagent/set-state (reagent/current-component) {:filter-box? filter-box?})
text (if @internal-model
(label-fn (item-for-id @internal-model choices :id-fn id-fn))
placeholder)]
[:a.chosen-single.chosen-default
{:href "javascript:" ;; Required to make this anchor appear in the tab order
:tab-index (when tab-index tab-index)
:on-click (handler-fn
(if @ignore-click
(reset! ignore-click false)
(dropdown-click)))
:on-mouse-down (handler-fn
(when @drop-showing?
(reset! ignore-click true))) ;; TODO: Hmmm, have a look at calling preventDefault (and stopProp?) and removing the ignore-click stuff
:on-key-down (handler-fn
(key-handler event)
(when (= (.-which event) 13) ;; Pressing enter on an anchor also triggers click event, which we don't want
(reset! ignore-click true))) ;; TODO: Hmmm, have a look at calling preventDefault (and stopProp?) and removing the ignore-click stuff
}
[:span (when title?
{:title text})
text]
[:div [:b]]])))) ;; This odd bit of markup produces the visual arrow on the right
;;--------------------------------------------------------------------------------------------------
;; Component: single-dropdown
;;--------------------------------------------------------------------------------------------------
(def single-dropdown-args-desc
[{:name :choices :required true :type "vector of choices | atom" :validate-fn vector-of-maps? :description [:span "Each is expected to have an id, label and, optionally, a group, provided by " [:code ":id-fn"] ", " [:code ":label-fn"] " & " [:code ":group-fn"]]}
{:name :model :required true :type "the id of a choice | atom" :description [:span "the id of the selected choice. If nil, " [:code ":placeholder"] " text is shown"]}
{:name :on-change :required true :type "id -> nil" :validate-fn fn? :description [:span "called when a new choice is selected. Passed the id of new choice"] }
{:name :id-fn :required false :default :id :type "choice -> anything" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns its unique identifier (aka id)"]}
{:name :label-fn :required false :default :label :type "choice -> string" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns its displayable label."]}
{:name :group-fn :required false :default :group :type "choice -> anything" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns its group identifier"]}
{:name :render-fn :required false :type "choice -> string | hiccup" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns the markup that will be rendered for that choice. Defaults to the label if no custom markup is required."]}
{:name :disabled? :required false :default false :type "boolean | atom" :description "if true, no user selection is allowed"}
{:name :filter-box? :required false :default false :type "boolean" :description "if true, a filter text field is placed at the top of the dropdown"}
{:name :regex-filter? :required false :default false :type "boolean | atom" :description "if true, the filter text field will support JavaScript regular expressions. If false, just plain text"}
{:name :placeholder :required false :type "string" :validate-fn string? :description "background text when no selection"}
{:name :title? :required false :default false :type "boolean" :description "if true, allows the title for the selected dropdown to be displayed via a mouse over. Handy when dropdown width is small and text is truncated"}
{:name :width :required false :default "100%" :type "string" :validate-fn string? :description "the CSS width. e.g.: \"500px\" or \"20em\""}
{:name :max-height :required false :default "240px" :type "string" :validate-fn string? :description "the maximum height of the dropdown part"}
{:name :tab-index :required false :type "integer | string" :validate-fn number-or-string? :description "component's tabindex. A value of -1 removes from order"}
{: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 single-dropdown
"Render a single dropdown component which emulates the bootstrap-choosen style. Sample choices object:
[{:id \"AU\" :label \"Australia\" :group \"Group 1\"}
{:id \"US\" :label \"United States\" :group \"Group 1\"}
{:id \"GB\" :label \"United Kingdom\" :group \"Group 1\"}
{:id \"AF\" :label \"Afghanistan\" :group \"Group 2\"}]"
[& {:keys [model] :as args}]
{:pre [(validate-args-macro single-dropdown-args-desc args "single-dropdown")]}
(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 @external-model) ;; Create a new atom from the model to be used internally
drop-showing? (reagent/atom false)
filter-text (reagent/atom "")]
(fn [& {:keys [choices model on-change disabled? filter-box? regex-filter? placeholder width max-height tab-index id-fn label-fn group-fn render-fn class style attr title?]
:or {id-fn :id label-fn :label group-fn :group render-fn label-fn}
:as args}]
{:pre [(validate-args-macro single-dropdown-args-desc args "single-dropdown")]}
(let [choices (deref-or-value choices)
disabled? (deref-or-value disabled?)
regex-filter? (deref-or-value regex-filter?)
latest-ext-model (reagent/atom (deref-or-value model))
_ (when (not= @external-model @latest-ext-model) ;; Has model changed externally?
(reset! external-model @latest-ext-model)
(reset! internal-model @latest-ext-model))
changeable? (and on-change (not disabled?))
callback #(do
(reset! internal-model %)
(when (and changeable? (not= @internal-model @latest-ext-model))
(on-change @internal-model))
(swap! drop-showing? not) ;; toggle to allow opening dropdown on Enter key
(reset! filter-text ""))
cancel #(do
(reset! drop-showing? false)
(reset! filter-text "")
(reset! internal-model @external-model))
dropdown-click #(when-not disabled?
(swap! drop-showing? not))
filtered-choices (if regex-filter?
(filter-choices-regex choices group-fn label-fn @filter-text)
(filter-choices choices group-fn label-fn @filter-text))
press-enter (fn []
(if disabled?
(cancel)
(callback @internal-model))
true)
press-escape (fn []
(cancel)
true)
press-tab (fn []
(if disabled?
(cancel)
(do ;; Was (callback @internal-model) but needed a customised version
(when changeable? (on-change @internal-model))
(reset! drop-showing? false)
(reset! filter-text "")))
(reset! drop-showing? false)
true)
press-up (fn []
(if @drop-showing? ;; Up arrow
(reset! internal-model (move-to-new-choice filtered-choices id-fn @internal-model -1))
(reset! drop-showing? true))
true)
press-down (fn []
(if @drop-showing? ;; Down arrow
(reset! internal-model (move-to-new-choice filtered-choices id-fn @internal-model 1))
(reset! drop-showing? true))
true)
press-home (fn []
(reset! internal-model (move-to-new-choice filtered-choices id-fn @internal-model :start))
true)
press-end (fn []
(reset! internal-model (move-to-new-choice filtered-choices id-fn @internal-model :end))
true)
key-handler #(if disabled?
false
(case (.-which %)
13 (press-enter)
27 (press-escape)
9 (press-tab)
38 (press-up)
40 (press-down)
36 (press-home)
35 (press-end)
filter-box?))] ;; Use this boolean to allow/prevent the key from being processed by the text box
[:div
(merge
{:class (str "rc-dropdown chosen-container chosen-container-single noselect " (when @drop-showing? "chosen-container-active chosen-with-drop ") class)
:style (merge (flex-child-style (if width "0 0 auto" "auto"))
(align-style :align-self :start)
{:width (when width width)}
style)}
attr) ;; Prevent user text selection
[dropdown-top internal-model choices id-fn label-fn tab-index placeholder dropdown-click key-handler filter-box? drop-showing? title?]
(when (and @drop-showing? (not disabled?))
[:div.chosen-drop
[filter-text-box filter-box? filter-text key-handler drop-showing?]
[:ul.chosen-results
(when max-height {:style {:max-height max-height}})
(if (-> filtered-choices count pos?)
(let [[group-names group-opt-lists] (choices-with-group-headings filtered-choices group-fn)
make-a-choice (partial make-choice-item id-fn render-fn callback internal-model)
make-choices #(map make-a-choice %1)
make-h-then-choices (fn [h opts]
(cons (make-group-heading h)
(make-choices opts)))
has-no-group-names? (nil? (:group (first group-names)))]
(if (and (= 1 (count group-opt-lists)) has-no-group-names?)
(make-choices (first group-opt-lists)) ;; one group means no headings
(apply concat (map make-h-then-choices group-names group-opt-lists))))
[:li.no-results (str "No results match \"" @filter-text "\"")])]])]))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,189 @@
(ns re-com.input-time
(:require-macros [re-com.core :refer [handler-fn]])
(:require [reagent.core :as reagent]
[re-com.validate :refer [css-style? html-attr? number-or-string?] :refer-macros [validate-args-macro]]
[re-com.text :refer [label]]
[re-com.box :refer [h-box gap]]
[re-com.util :refer [pad-zero-number deref-or-value]]))
(defn- time->mins
[time]
(rem time 100))
(defn- time->hrs
[time]
(quot time 100))
(defn- to-int
"Parse the string 's' to a valid int. On parse failure, return 0"
[s]
(let [val (js/parseInt s)]
(if (js/isNaN val) 0 val)))
(defn- triple->time
"Return a time integer from a triple int vector of form [H _ M]"
[[hr _ mi]]
(+ (* hr 100) mi)) ;; a four digit integer: HHMM
;; This regular expression matchs all valid forms of time entry, including partial
;; forms which happen during user entry.
;; It is composed of 3 'or' options, separated by '|', and within each, is a sub-re which
;; attempts to match the HH ':' MM parts.
;; So any attempt to match against this re, using "re-matches" will return
;; a vector of 10 items:
;; - the 1st item will be the entire string matched
;; - followed by 3 groups of 3.
(def ^{:private true}
triple-seeking-re #"^(\d{0,2})()()$|^(\d{0,1})(:{0,1})(\d{0,2})$|^(\d{0,2})(:{0,1})(\d{0,2})$")
(defn- extract-triple-from-text
[text]
(->> text
(re-matches triple-seeking-re) ;; looks for different ways of matching triples H : M
(rest) ;; get rid of the first value. It is the entire matched string.
(filter (comp not nil?)))) ;; of the 9 items, there should be only 3 non-nil matches coresponding to H : M
(defn- text->time
"return as a time int, the contents of 'text'"
[text]
(->> text
extract-triple-from-text
(map to-int) ;; make them ints (or 0)
triple->time)) ;; turn the triple of values into a single int
(defn- time->text
"return a string of format HH:MM for 'time'"
[time]
(let [hrs (time->hrs time)
mins (time->mins time)]
(str (pad-zero-number hrs 2) ":" (pad-zero-number mins 2))))
(defn- valid-text?
"Return true if text passes basic time validation.
Can't do to much validation because user input might not be finished.
Why? On the way to entering 6:30, you must pass through the invalid state of '63'.
So we only really check against the triple-extracting regular expression"
[text]
(= 3 (count (extract-triple-from-text text))))
(defn- valid-time?
[time]
(cond
(nil? time) false ;; can't be nil
(> 0 time) false ;; must be a poistive number
(< 60 (time->mins time)) false ;; too many mins
:else true))
(defn- validate-arg-times
[model minimum maximum]
(assert (and (number? model) (valid-time? model)) (str "[input-time] given an invalid :model - " model))
(assert (and (number? minimum) (valid-time? minimum)) (str "[input-time] given an invalid :minimum - " minimum))
(assert (and (number? maximum) (valid-time? maximum)) (str "[input-time] given an invalid :maximum - " maximum))
(assert (<= minimum maximum) (str "[input-time] :minimum " minimum " > :maximum " maximum))
true)
(defn- force-valid-time
"Validate the time supplied.
Return either the time or, if it is invalid, return something valid"
[time min max previous]
(cond
(nil? time) previous
(not (valid-time? time)) previous
(< time min) min
(< max time) max
:else time))
(defn- on-new-keypress
"Called each time the <input> field gets a keypress, or paste operation.
Rests the text-model only if the new text is valid"
[event text-model]
(let [current-text (-> event .-target .-value)] ;; gets the current input field text
(when (valid-text? current-text)
(reset! text-model current-text))))
(defn- lose-focus-if-enter
"When Enter is pressed, force the component to lose focus"
[ev]
(when (= (.-keyCode ev) 13)
(-> ev .-target .blur)
true))
(defn- on-defocus
"Called when the field looses focus.
Re-validate what has been entered, comparing to mins and maxs.
Invoke the callback as necessary"
[text-model min max callback previous-val]
(let [time (text->time @text-model)
time (force-valid-time time min max previous-val)]
(reset! text-model (time->text time))
(when (and callback (not= time previous-val))
(callback time))))
(def input-time-args-desc
[{:name :model :required true :type "integer | string | atom" :validate-fn number-or-string? :description "a time in integer form. e.g. '09:30am' is 930"}
{:name :on-change :required true :type "integer -> nil" :validate-fn fn? :description "called when user entry completes and value is new. Passed new value as integer"}
{:name :minimum :required false :default 0 :type "integer | string" :validate-fn number-or-string? :description "user can't enter a time less than this value"}
{:name :maximum :required false :default 2359 :type "integer | string" :validate-fn number-or-string? :description "user can't enter a time more than this value"}
{:name :disabled? :required false :default false :type "boolean | atom" :description "when true, user input is disabled"}
{:name :show-icon? :required false :default false :type "boolean" :description "when true, a clock icon will be displayed to the right of input field"}
{:name :hide-border? :required false :default false :type "boolean" :description "when true, input filed is displayed without a border"}
{:name :width :required false :type "string" :validate-fn string? :description "standard CSS width setting for width of the input box (excluding the icon if present)"}
{:name :height :required false :type "string" :validate-fn string? :description "standard CSS height setting"}
{: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 style. e.g. {:color \"red\" :width \"50px\"}" }
{: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 input-time
"I return the markup for an input box which will accept and validate times.
Parameters - refer input-time-args above"
[& {:keys [model minimum maximum on-change class style attr] :as args
:or {minimum 0 maximum 2359}}]
{:pre [(validate-args-macro input-time-args-desc args "input-time")
(validate-arg-times (deref-or-value model) minimum maximum)]}
(let [deref-model (deref-or-value model)
text-model (reagent/atom (time->text deref-model))
previous-model (reagent/atom deref-model)]
(fn
[& {:keys [model minimum maximum width height disabled? hide-border? show-icon?] :as args
:or {minimum 0 maximum 2359}}]
{:pre [(validate-args-macro input-time-args-desc args "input-time")
(validate-arg-times (deref-or-value model) minimum maximum)]}
(let [style (merge (when hide-border? {:border "none"})
style)
new-val (deref-or-value model)
new-val (if (< new-val minimum) minimum new-val)
new-val (if (> new-val maximum) maximum new-val)]
;; if the model is different to that currently shown in text, then reset the text to match
;; other than that we want to keep the current text, because the user is probably typing
(when (not= @previous-model new-val)
(reset! text-model (time->text new-val))
(reset! previous-model new-val))
[h-box
:class "rc-input-time"
:style (merge {:height height}
style)
:children [[:input
(merge
{:type "text"
:class (str "time-entry " class)
:style (merge {:width width}
style)
:value @text-model
:disabled (deref-or-value disabled?)
:on-change (handler-fn (on-new-keypress event text-model))
:on-blur (handler-fn (on-defocus text-model minimum maximum on-change @previous-model))
:on-key-up (handler-fn (lose-focus-if-enter event))}
attr)]
(when show-icon?
#_[:div.time-icon ;; TODO: Remove
[:span.glyphicon.glyphicon-time
{:style {:position "static" :margin "auto"}}]]
[:div.time-icon
[:i.zmdi.zmdi-hc-fw-rc.zmdi-time
{:style {:position "static" :margin "auto"}}]])]]))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,384 @@
(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 "%")]]]))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,54 @@
(ns re-com.modal-panel
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.validate :refer [string-or-hiccup? number-or-string? css-style? html-attr?] :refer-macros [validate-args-macro]]))
;; ------------------------------------------------------------------------------------
;; modal-panel
;; ------------------------------------------------------------------------------------
(def modal-panel-args-desc
[{:name :child :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "hiccup to be centered within in the browser window"}
{:name :wrap-nicely? :required false :default true :type "boolean" :description [:span "if true, wrap " [:code ":child"] " in a white, rounded panel"]}
{:name :backdrop-color :required false :default "black" :type "string" :validate-fn string? :description "CSS color of backdrop"}
{:name :backdrop-opacity :required false :default 0.6 :type "double | string" :validate-fn number-or-string? :description [:span "opacity of backdrop from:" [:br] "0.0 (transparent) to 1.0 (opaque)"]}
{:name :backdrop-on-click :required false :default nil :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the backdrop is clicked"}
{: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 modal-panel
"Renders a modal window centered on screen. A dark transparent backdrop sits between this and the underlying
main window to prevent UI interactivity and place user focus on the modal window.
Parameters:
- child: The message to display in the modal (a string or a hiccup vector or function returning a hiccup vector)"
[& {:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click]
:or {wrap-nicely? true backdrop-color "black" backdrop-opacity 0.6}
:as args}]
{:pre [(validate-args-macro modal-panel-args-desc args "modal-panel")]}
[:div ;; Containing div
(merge {:class (str "rc-modal-panel display-flex " class)
:style (merge {:position "fixed"
:left "0px"
:top "0px"
:width "100%"
:height "100%"
:z-index 1020}
style)}
attr)
[:div ;; Backdrop
{:style {:position "fixed"
:width "100%"
:height "100%"
:background-color backdrop-color
:opacity backdrop-opacity
:z-index 1}
:on-click (handler-fn (when backdrop-on-click (backdrop-on-click))
(.preventDefault event)
(.stopPropagation event))}]
[:div ;; Child container
{:style (merge {:margin "auto"
:z-index 2}
(when wrap-nicely? {:background-color "white"
:padding "16px"
:border-radius "6px"}))}
child]])

View file

@ -0,0 +1 @@
{:rename-macros {}, :renames {}, :use-macros {validate-args-macro re-com.validate, handler-fn re-com.core}, :excludes #{}, :name re-com.modal-panel, :imports nil, :requires {re-com.validate re-com.validate}, :uses {html-attr? re-com.validate, css-style? re-com.validate, string-or-hiccup? re-com.validate, number-or-string? re-com.validate}, :defs {modal-panel-args-desc {:name re-com.modal-panel/modal-panel-args-desc, :file "docs/js/compiled/out/re_com/modal_panel.cljs", :line 9, :column 1, :end-line 9, :end-column 27, :meta {:file "/Users/simon/workspace/swinging-needle-meter/docs/js/compiled/out/re_com/modal_panel.cljs", :line 9, :column 6, :end-line 9, :end-column 27}}, modal-panel {:protocol-inline nil, :meta {:file "/Users/simon/workspace/swinging-needle-meter/docs/js/compiled/out/re_com/modal_panel.cljs", :line 19, :column 7, :end-line 19, :end-column 18, :arglists (quote ([& {:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args}])), :doc "Renders a modal window centered on screen. A dark transparent backdrop sits between this and the underlying\n main window to prevent UI interactivity and place user focus on the modal window.\n Parameters:\n - child: The message to display in the modal (a string or a hiccup vector or function returning a hiccup vector)", :top-fn {:variadic true, :max-fixed-arity 0, :method-params [({:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args})], :arglists ([& {:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args}]), :arglists-meta (nil)}}, :name re-com.modal-panel/modal-panel, :variadic true, :file "docs/js/compiled/out/re_com/modal_panel.cljs", :end-column 18, :top-fn {:variadic true, :max-fixed-arity 0, :method-params [({:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args})], :arglists ([& {:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args}]), :arglists-meta (nil)}, :method-params [({:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args})], :protocol-impl nil, :arglists-meta (nil), :column 1, :line 19, :end-line 19, :max-fixed-arity 0, :fn-var true, :arglists ([& {:keys [child wrap-nicely? backdrop-color backdrop-opacity class style attr backdrop-on-click], :or {wrap-nicely? true, backdrop-color "black", backdrop-opacity 0.6}, :as args}]), :doc "Renders a modal window centered on screen. A dark transparent backdrop sits between this and the underlying\n main window to prevent UI interactivity and place user focus on the modal window.\n Parameters:\n - child: The message to display in the modal (a string or a hiccup vector or function returning a hiccup vector)"}}, :require-macros {re-com.core re-com.core, re-com.validate re-com.validate}, :cljs.analyzer/constants {:seen #{:description :wrap-nicely? :backdrop-opacity :top :default :name :background-color :width :type :backdrop-color :on-click :style :div :z-index :opacity :class :padding :code :backdrop-on-click :position :validate-fn :br :child :border-radius :required :height :left :span :margin :attr}, :order [:name :required :type :validate-fn :description :child :default :wrap-nicely? :span :code :backdrop-color :backdrop-opacity :br :backdrop-on-click :class :style :attr :div :position :left :top :width :height :z-index :on-click :background-color :opacity :margin :padding :border-radius]}, :doc nil}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"\/Users\/simon\/workspace\/swinging-needle-meter\/docs\/js\/compiled\/out\/re_com\/modal_panel.js","sources":["modal_panel.cljs?rel=1603199189410"],"lineCount":72,"mappings":";AAAA;;;AAQA,2CAAA,mFAAA,2CAAA,qDAAA,sDAAA,6DAAA,KAAA,qDAAA,kBAAA,2GAAA,oEAAA,6DAAA,2CAAA,qDAAA,mEAAA,6DAAA,MAAA,4DAAA,KAAA,qDAAA,UAAA,oEAAA,mFAAA,qDAAA,iBAAA,mFAAA,qDAAA,iBAAA,6CAAA,2CAAA,qDAAA,yEAAA,6DAAA,MAAA,4DAAA,QAAA,qDAAA,SAAA,2FAAA,oEAAA,gCAAA,2CAAA,qDAAA,6EAAA,6DAAA,MAAA,4DAAA,IAAA,qDAAA,kBAAA,2GAAA,oEAAA,mFAAA,qDAAA,4BAAA,mFAAA,wDAAA,oDAAA,2CAAA,qDAAA,gFAAA,6DAAA,MAAA,4DAAA,KAAA,qDAAA,SAAA,uFAAA,oEAAA,oGAAA,2CAAA,qDAAA,wDAAA,6DAAA,MAAA,qDAAA,SAAA,2FAAA,oEAAA,2CAAA,2CAAA,qDAAA,uDAAA,6DAAA,MAAA,qDAAA,gBAAA,oGAAA,oEAAA,wCAAA,2CAAA,qDAAA,qDAAA,6DAAA,MAAA,qDAAA,gBAAA,oGAAA,oEAAA,mFAAA,qDAAA,yBAAA,mFAAA,qDAAA,yBAAA,mFAAA,wDAAA,MAAA,mFAAA,qDAAA,iBAAA,OAAA,mFAAA,qDAAA,iBAAA,5lLAAKA,geAC8FC,m2CAEAC,sjBACAC,o4BACAC,whBACAF,yeACAG,6eACAC;AAEnG,AAAA;;;;;;iCAAA,yCAAAC,1EAAMM;AAAN,AAAA,IAAAL,sBAAA;AAAA,AAAA,IAAAC,2BAAA,AAAA;AAAA,AAAA,IAAAC,yBAAA;;AAAA,AAAA,GAAA,CAAAA,yBAAAD;AAAA,AAAA,AAAAD,yBAAA,CAAA,UAAAE;;AAAA,eAAA,CAAAA,yBAAA;;;;AAAA;;;;AAAA,IAAAC,wBAAA,EAAA,CAAA,MAAA,AAAAH,6BAAA,AAAA,KAAAI,qBAAA,AAAAJ,0BAAA,KAAA,IAAA,OAAA;AAAA,AAAA,OAAAK,oEAAAF;;;AAAA,AAAA,AAAA,AAAAE,sEAAA,WAAAC;AAAA,AAAA,IAAAC,aAAAD;IAAAC,iBAAA,EAAA,EAAA,EAAA,CAAAA,cAAA,QAAA,EAAA,CAAA,CAAA,AAAAA,iDAAA,WAAA,AAAAA,6BAAA,KAAA,OAAA,QAAA,AAAAC,0BAAAC,mBAAAF,YAAAA;WAAAA,PAOYe;YAPZ,AAAAZ,wBAAAH,eAAA,\/CAKaO;yBALb,AAAAJ,wBAAAH,eAAA,mEAAA,\/HAKmBQ;qBALnB,AAAAL,wBAAAH,eAAA,yEAAA,jIAKgCS;uBALhC,AAAAN,wBAAAH,eAAA,6EAAA,vIAK+CU;aAL\/C,AAAAP,wBAAAH,eAAA,hDAKgEW;YALhE,AAAAR,wBAAAH,eAAA,\/CAKsEY;WALtE,AAAAT,wBAAAH,eAAA,9CAK4Ea;wBAL5E,AAAAV,wBAAAH,eAAA,3DAKiFc;AALjF,AAAA,oBAQS,EAAA,EAAAE,aAAA,KAAA,AAAAC,wCAAA,AAAAC,0FAAA,\/CAAqBjC,0CAAsB8B;AARpD;AAAA,AAAA,MAAA,KAAAX,MAAA;;;AAAA,0FAAA,mxBAAA,mFAAA,mDAAA,2CAAA,uDAAA,2CAAA,8DAAA,QAAA,uDAAA,OAAA,yDAAA,OAAA,2FAAA,2EAAA,2DAAA,YAAA,p6CAUG,0BAAA,2CAAA,uIAAA,5MAACe,6HAAe,eAAA,8CAAoCR,yEACrC,0BAAA,2CAAA,8DAAA,QAAA,qDAAA,MAAA,oDAAA,MAAA,uDAAA,OAAA,yDAAA,OAAA,2DAAA,zbAACQ,wcAMMP,eACfC,yiBAKwBJ,yEACAC,qJAEnB;kBAAAU;AAAA,AAAY,oBAAMN;AAAN,AAAwB,AAACA;;AAAzB;;AACA,AAAiBM;;AACjB,AAAkBA;;AAF9B;;iBA1Bf,mFAAA,mDAAA,2CAAA,uDA8BY,0BAAA,2CAAA,yDAAA,OAAA,2DAAA,hMAACD,4MAEM,sCAAA,AAAA,2CAAA,4EAAA,QAAA,2DAAA,OAAA,sEAAA,cAAA,zSAAMX,wTAGrBD;;;AAnCJ,AAAA,AAAAT,yDAAA;;AAAA,AAAA,AAAAA,mDAAA,WAAAO;AAAA,AAAA,OAAAP,oEAAA,AAAAQ,wBAAAD;;;AAAA","names":["re-com.modal-panel\/modal-panel-args-desc","re-com.validate\/string-or-hiccup?","cljs.core\/string?","re-com.validate\/number-or-string?","cljs.core\/fn?","re-com.validate\/css-style?","re-com.validate\/html-attr?","var_args","args__26212__auto__","len__26205__auto__","i__26206__auto__","argseq__26213__auto__","cljs.core\/IndexedSeq","re-com.modal-panel\/modal-panel","p__27087","map__27088","cljs.core\/apply","cljs.core\/hash-map","cljs.core\/get","js\/Error","seq27086","cljs.core\/seq","child","wrap-nicely?","backdrop-color","backdrop-opacity","class","style","attr","backdrop-on-click","args","js\/goog.DEBUG","re-com.validate\/validate-args","re-com.validate\/extract-arg-data","cljs.core\/merge","event"]}

View file

@ -0,0 +1,520 @@
(ns re-com.popover
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [get-element-by-id px deref-or-value sum-scroll-offsets]]
[re-com.box :refer [h-box v-box flex-child-style flex-flow-style align-style]]
[re-com.validate :refer [position? position-options-list popover-status-type? popover-status-types-list number-or-string?
string-or-hiccup? string-or-atom? vector-of-maps? css-style? html-attr?] :refer-macros [validate-args-macro]]
[clojure.string :as string]
[reagent.core :as reagent]
[reagent.ratom :refer-macros [reaction]]))
(defn point
[x y]
(str x "," y " "))
(defn- split-keyword
"Return the vector of the two keywords formed by splitting another keyword 'kw' on an internal delimiter (usually '-')
(split-keyword :above-left \"-\") => [:above :left]"
[kw delimiter]
(let [keywords (string/split (str kw) (re-pattern (str "[" delimiter ":]")))]
[(keyword (keywords 1)) (keyword (keywords 2))]))
(defn- close-button
"A button with a big X in it, placed to the right of the popover title"
[showing? close-callback style]
;; Can't use [button] because [button] already uses [popover] which would be a circular dependency.
[:button
{:on-click (handler-fn
(if close-callback
(close-callback)
(reset! showing? false)))
:class "close"
:style (merge {:width "34px"
:font-size "26px"
:position "absolute"
:top "4px"
:right "2px"}
style)}
[:i {:class "zmdi zmdi-hc-fw-rc zmdi-close"}]])
(defn- calc-popover-pos
"Determine values for :left :right :top :bottom CSS styles.
- pop-orient What side of the anchor the popover will be attached to. One of :above :below :left :right
- p-width The px width of the popover after it has been rendered
- p-height The px height of the popover after it has been rendered
- pop-offset The number of pixels the popover is offset from it's natural position in relation to the popover-arrow (ugh, hard to explain)
- arrow-length The px length of the arrow (from the point to middle of arrow base)
- arrow-gap The px distance between the anchor and the arrow tip. Positive numbers push the popover away from the anchor
"
[pop-orient p-width p-height pop-offset arrow-length arrow-gap]
(let [total-offset (+ arrow-length arrow-gap)
popover-left (case pop-orient
:left "initial" ;; TODO: Ultimately remove this (must have NO :left which is in Bootstrap .popover class)
:right (px total-offset)
(:above :below) (px (if pop-offset pop-offset (/ p-width 2)) :negative))
popover-top (case pop-orient
(:left :right) (px (if pop-offset pop-offset (/ p-height 2)) :negative)
:above "initial"
:below (px total-offset))
popover-right (case pop-orient
:left (px total-offset)
:right nil
:above nil
:below nil)
popover-bottom (case pop-orient
:left nil
:right nil
:above (px total-offset)
:below nil)]
{:left popover-left :top popover-top :right popover-right :bottom popover-bottom}))
(defn calculate-optimal-position
"Calculate the optimal :position value that results in the least amount of clipping by the screen edges
Taken from: https://github.com/Lambda-X/cljs-repl-web/blob/0.3.1/src/cljs/cljs_repl_web/views/utils.cljs#L52
Thanks to @richiardiandrea and @tomek for this code"
[[x y]]
(let [w (.-innerWidth js/window) ;; Width/height of the browser window viewport including, if rendered, the vertical scrollbar
h (.-innerHeight js/window)
h-threshold-left (quot w 3)
h-threshold-cent (* 2 h-threshold-left)
h-position (cond
(< x h-threshold-left) "right"
(< x h-threshold-cent) "center"
:else "left")
v-threshold (quot h 2)
v-position (if (< y v-threshold) "below" "above")]
(keyword (str v-position \- h-position))))
(defn calc-element-midpoint
"Given a node reference, calculate the absolute x and y coordinates of the node's midpoint"
[node]
(let [bounding-rect (.getBoundingClientRect node)]
[(/ (+ (.-right bounding-rect) (.-left bounding-rect)) 2) ;; x
(/ (+ (.-bottom bounding-rect) (.-top bounding-rect)) 2)])) ;; y
(defn- popover-arrow
"Render the triangle which connects the popover to the anchor (using SVG)"
[orientation pop-offset arrow-length arrow-width grey-arrow? no-border? popover-color]
(let [half-arrow-width (/ arrow-width 2)
arrow-shape {:left (str (point 0 0) (point arrow-length half-arrow-width) (point 0 arrow-width))
:right (str (point arrow-length 0) (point 0 half-arrow-width) (point arrow-length arrow-width))
:above (str (point 0 0) (point half-arrow-width arrow-length) (point arrow-width 0))
:below (str (point 0 arrow-length) (point half-arrow-width 0) (point arrow-width arrow-length))}]
[:svg {:class "popover-arrow"
:style {:position "absolute"
(case orientation ;; Connect arrow to edge of popover
:left :right
:right :left
:above :bottom
:below :top) (px arrow-length :negative)
(case orientation ;; Position the arrow at the top/left, center or bottom/right of the popover
(:left :right) :top
(:above :below) :left) (if (nil? pop-offset) "50%" (px pop-offset))
(case orientation ;; Adjust the arrow position so it's center is attached to the desired position set above
(:left :right) :margin-top
(:above :below) :margin-left) (px half-arrow-width :negative)
:width (px (case orientation ;; Arrow is rendered in a rectangle so choose the correct edge length
(:left :right) arrow-length
(:above :below) arrow-width))
:height (px (case orientation ;; Same as :width comment above
(:left :right) arrow-width
(:above :below) arrow-length))}}
[:polyline {:points (arrow-shape orientation)
:style {:fill (if popover-color
popover-color
(if grey-arrow? "#f7f7f7" "white"))
:stroke (when-not no-border? "rgba(0, 0, 0, .2)")
:stroke-width "1"}}]]))
;;--------------------------------------------------------------------------------------------------
;; Component: backdrop
;;--------------------------------------------------------------------------------------------------
(def backdrop-args-desc
[{:name :opacity :required false :default 0.0 :type "double | string" :validate-fn number-or-string? :description [:span "opacity of backdrop from:" [:br] "0.0 (transparent) to 1.0 (opaque)"]}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the backdrop is clicked"}])
(defn- backdrop
"Renders a backdrop dive which fills the entire page and responds to clicks on it. Can also specify how tranparent it should be"
[& {:keys [opacity on-click] :as args}]
{:pre [(validate-args-macro backdrop-args-desc args "backdrop")]}
[:div {:class "rc-backdrop noselect"
:style {:position "fixed"
:left "0px"
:top "0px"
:width "100%"
:height "100%"
:background-color "black"
:opacity (if opacity opacity 0.0)}
:on-click (handler-fn (on-click))}])
;;--------------------------------------------------------------------------------------------------
;; Component: popover-title
;;--------------------------------------------------------------------------------------------------
(def popover-title-args-desc
[{:name :showing? :required true :type "boolean atom" :description "an atom. When the value is true, the popover shows."}
{:name :title :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "describes the title of the popover. Default font size is 18px to make it stand out"}
{:name :close-button? :required false :default true :type "boolean" :description "when true, displays the close button"}
{:name :close-callback :required false :type "-> nil" :validate-fn fn? :description [:span "a function which takes no params and returns nothing. Called when the close button is pressed. Not required if " [:code ":showing?"] " atom passed in OR " [:code ":close-button?"] " is set to false"]}])
(defn- popover-title
"Renders a title at the top of a popover with an optional close button on the far right"
[& {:keys [title showing? close-button? close-callback]
:as args}]
{:pre [(validate-args-macro popover-title-args-desc args "popover-title")]}
(assert (or ((complement nil?) showing?) ((complement nil?) close-callback)) "Must specify either showing? OR close-callback")
(let [close-button? (if (nil? close-button?) true close-button?)]
[:h3.popover-title {:style (merge (flex-child-style "inherit")
{:font-size "18px"})}
[h-box
:justify :between
:align :center
:children [title
(when close-button? [close-button showing? close-callback])]]]))
;;--------------------------------------------------------------------------------------------------
;; Component: popover-border
;;--------------------------------------------------------------------------------------------------
(defn next-even-integer
[num]
(-> num inc (/ 2) int (* 2)))
(defn calc-pop-offset
[arrow-pos position-offset p-width p-height]
(case arrow-pos
:center nil
:right (+ 20 position-offset)
:below (+ 20 position-offset)
:left (if p-width (- (- p-width 25) position-offset) p-width)
:above (if p-height (- (- p-height 25) position-offset) p-height)))
(defn popover-clipping
[node]
(let [viewport-width (.-innerWidth js/window) ;; Width (in pixels) of the browser window viewport including, if rendered, the vertical scrollbar.
viewport-height (.-innerHeight js/window) ;; Height (in pixels) of the browser window viewport including, if rendered, the horizontal scrollbar.
bounding-rect (.getBoundingClientRect node)
left (.-left bounding-rect)
right (.-right bounding-rect)
top (.-top bounding-rect)
bottom (.-bottom bounding-rect)
clip-left (when (< left 0) (- left))
clip-right (when (> right viewport-width) (- right viewport-width))
clip-top (when (< top 0) (- top))
clip-bottom (when (> bottom viewport-height) (- bottom viewport-height))]
#_(when (or (some? clip-left) (some? clip-right) (some? clip-top) (some? clip-bottom)) ;; Return full clipping details (or nil if not clipped)
{:left clip-left :right clip-right :top clip-top :bottom clip-bottom})
(or (some? clip-left) (some? clip-right) (some? clip-top) (some? clip-bottom)))) ;; Return boolean
(def popover-border-args-desc
[{:name :children :required true :type "vector" :validate-fn sequential? :description "a vector of component markups"}
{:name :position :required true :type "keyword atom" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :position-offset :required false :type "integer" :validate-fn number? :description [:span "px offset of the arrow from its default " [:code ":position"] " along the popover border. Is ignored when " [:code ":position"] " is one of the " [:code ":xxx-center"] " variants. Positive numbers slide the popover toward its center"]}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS style describing the popover width"}
{:name :height :required false :default "auto" :type "string" :validate-fn string? :description "a CSS style describing the popover height"}
{:name :popover-color :required false :default "white" :type "string" :validate-fn string? :description "fill color of the popover"}
{:name :arrow-length :required false :default 11 :type "integer | string" :validate-fn number-or-string? :description "the length in pixels of the arrow (from pointy part to middle of arrow base)"}
{:name :arrow-width :required false :default 22 :type "integer | string" :validate-fn number-or-string? :description "the width in pixels of arrow base"}
{:name :arrow-gap :required false :default -1 :type "integer" :validate-fn number? :description "px gap between the anchor and the arrow tip. Positive numbers push the popover away from the anchor"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS style which overrides the inner padding of the popover"}
{:name :margin-left :required false :type "string" :validate-fn string? :description "a CSS style describing the horiztonal offset from anchor after position"}
{:name :margin-top :required false :type "string" :validate-fn string? :description "a CSS style describing the vertical offset from anchor after position"}
{:name :tooltip-style? :required false :default false :type "boolean" :description "setup popover styles for a tooltip"}
{:name :title :required false :type "string | markup" :description "describes a title"}])
(defn popover-border
"Renders an element or control along with a Bootstrap popover"
[& {:keys [children position position-offset width height popover-color arrow-length arrow-width arrow-gap padding margin-left margin-top tooltip-style? title]
:or {arrow-length 11 arrow-width 22 arrow-gap -1}
:as args}]
{:pre [(validate-args-macro popover-border-args-desc args "popover-border")]}
(let [pop-id (gensym "popover-")
rendered-once (reagent/atom false) ;; The initial render is off screen because rendering it in place does not render at final width, and we need to measure it to be able to place it properly
ready-to-show? (reagent/atom false) ;; This is used by the optimal position code to avoid briefly seeing it in its intended position before quickly moving to the optimal position
p-width (reagent/atom 0)
p-height (reagent/atom 0)
pop-offset (reagent/atom 0)
found-optimal (reagent/atom false)
calc-metrics (fn [position]
(let [popover-elem (get-element-by-id pop-id)
[orientation arrow-pos] (split-keyword position "-")
grey-arrow? (and title (or (= orientation :below) (= arrow-pos :below)))]
(reset! p-width (if popover-elem (next-even-integer (.-clientWidth popover-elem)) 0)) ;; next-even-integer required to avoid wiggling popovers (width/height appears to prefer being even and toggles without this call)
(reset! p-height (if popover-elem (next-even-integer (.-clientHeight popover-elem)) 0))
(reset! pop-offset (calc-pop-offset arrow-pos position-offset @p-width @p-height))
[orientation grey-arrow?]))]
(reagent/create-class
{:display-name "popover-border"
:component-did-mount
(fn []
(reset! rendered-once true))
:component-did-update
(fn [this]
(let [pop-border-node (reagent/dom-node this)
clipped? (popover-clipping pop-border-node)
anchor-node (-> pop-border-node .-parentNode .-parentNode .-parentNode)] ;; Get reference to rc-point-wrapper node
(when (and clipped? (not @found-optimal))
(reset! position (calculate-optimal-position (calc-element-midpoint anchor-node)))
(reset! found-optimal true))
(calc-metrics @position)
(reset! ready-to-show? true)))
:reagent-render
(fn
[& {:keys [children position position-offset width height popover-color arrow-length arrow-width arrow-gap padding margin-left margin-top tooltip-style? title]
:or {arrow-length 11 arrow-width 22 arrow-gap -1}
:as args}]
{:pre [(validate-args-macro popover-border-args-desc args "popover-border")]}
(let [[orientation grey-arrow?] (calc-metrics @position)]
[:div.popover.fade.in
{:id pop-id
:style (merge (if @rendered-once
(when pop-id (calc-popover-pos orientation @p-width @p-height @pop-offset arrow-length arrow-gap))
{:top "-10000px" :left "-10000px"})
(if width {:width width})
(if height {:height height})
(if popover-color {:background-color popover-color})
(when tooltip-style?
{:border-radius "4px"
:box-shadow "none"
:border "none"})
;; The popover point is zero width, therefore its absolute children will consider this width when deciding their
;; natural size and in particular, how they natually wrap text. The right hand side of the popover is used as a
;; text wrapping point so it will wrap, depending on where the child is positioned. The margin is also taken into
;; consideration for this point so below, we set the margins to negative a lot to prevent
;; this annoying wrapping phenomenon.
(case orientation
:left {:margin-left "-2000px"}
(:right :above :below) {:margin-right "-2000px"})
;; optional override offsets
(when margin-left {:margin-left margin-left})
(when margin-top {:margin-top margin-top})
;; make it visible and turn off Bootstrap max-width and remove Bootstrap padding which adds an internal white border
{:display "block"
:opacity (if @ready-to-show? "1" "0")
:max-width "none"
:padding "0px"})}
[popover-arrow orientation @pop-offset arrow-length arrow-width grey-arrow? tooltip-style? popover-color]
(when title title)
(into [:div.popover-content {:style {:padding padding}}] children)]))})))
;;--------------------------------------------------------------------------------------------------
;; Component: popover-content-wrapper
;;--------------------------------------------------------------------------------------------------
(def popover-content-wrapper-args-desc
[{:name :showing-injected? :required true :type "boolean atom" :description [:span "an atom. When the value is true, the popover shows." [:br] [:strong "NOTE: "] "When used as direct " [:code ":popover"] " arg in popover-anchor-wrapper, this arg will be injected automatically by popover-anchor-wrapper. If using your own popover function, you must add this yourself"]}
{:name :position-injected :required true :type "keyword atom" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list [:br] [:strong "NOTE: "] "See above NOTE for " [:code ":showing-injected?"] ". Same applies" ]}
{:name :position-offset :required false :type "integer" :validate-fn number? :description [:span "px offset of the arrow from its default " [:code ":position"] " along the popover border. Is ignored when " [:code ":position"] " is one of the " [:code ":xxx-center"] " variants. Positive numbers slide the popover toward its center"]}
{:name :no-clip? :required false :default false :type "boolean" :description "when an anchor is in a scrolling region (e.g. scroller component), the popover can sometimes be clipped. By passing true for this parameter, re-com will use a different CSS method to show the popover. This method is slightly inferior because the popover can't track the anchor if it is repositioned"}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS style representing the popover width"}
{:name :height :required false :type "string" :validate-fn string? :description "a CSS style representing the popover height"}
{:name :backdrop-opacity :required false :default 0.0 :type "double | string" :validate-fn number-or-string? :description "indicates the opacity of the backdrop where 0.0=transparent, 1.0=opaque"}
{:name :on-cancel :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the popover is cancelled (e.g. user clicks away)"}
{:name :title :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "describes the title of the popover. The default font size is 18px to make it stand out"}
{:name :close-button? :required false :default true :type "boolean" :description "when true, displays the close button"}
{:name :body :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "describes the popover body. Must be a single component"}
{:name :tooltip-style? :required false :default false :type "boolean" :description "setup popover styles for a tooltip"}
{:name :popover-color :required false :default "white" :type "string" :validate-fn string? :description "fill color of the popover"}
{:name :arrow-length :required false :default 11 :type "integer | string" :validate-fn number-or-string? :description "the length in pixels of the arrow (from pointy part to middle of arrow base)"}
{:name :arrow-width :required false :default 22 :type "integer | string" :validate-fn number-or-string? :description "the width in pixels of arrow base"}
{:name :arrow-gap :required false :default -1 :type "integer" :validate-fn number? :description "px gap between the anchor and the arrow tip. Positive numbers push the popover away from the anchor"}
{:name :padding :required false :type "string" :validate-fn string? :description "a CSS style which overrides the inner padding of the popover"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "override component style(s) with a style map, only use in case of emergency"}])
(defn popover-content-wrapper
"Abstracts several components to handle the 90% of cases for general popovers and dialog boxes"
[& {:keys [showing-injected? position-injected position-offset no-clip? width height backdrop-opacity on-cancel title close-button? body tooltip-style? popover-color arrow-length arrow-width arrow-gap padding style]
:or {arrow-length 11 arrow-width 22 arrow-gap -1}
:as args}]
{:pre [(validate-args-macro popover-content-wrapper-args-desc args "popover-content-wrapper")]}
(let [left-offset (reagent/atom 0)
top-offset (reagent/atom 0)
position-no-clip-popover (fn position-no-clip-popover
[this]
(when no-clip?
(let [node (reagent/dom-node this)
popover-point-node (.-parentNode node) ;; Get reference to rc-popover-point node
bounding-rect (.getBoundingClientRect popover-point-node)] ;; The modern magical way of getting offsetLeft and offsetTop. Returns this: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIDOMClientRect
(reset! left-offset (.-left bounding-rect))
(reset! top-offset (.-top bounding-rect)))))]
(reagent/create-class
{:display-name "popover-content-wrapper"
:component-did-mount
(fn [this]
(position-no-clip-popover this))
:component-did-update
(fn [this]
(position-no-clip-popover this))
:reagent-render
(fn
[& {:keys [showing-injected? position-injected position-offset no-clip? width height backdrop-opacity on-cancel title close-button? body tooltip-style? popover-color arrow-length arrow-width arrow-gap padding style]
:or {arrow-length 11 arrow-width 22 arrow-gap -1}
:as args}]
{:pre [(validate-args-macro popover-content-wrapper-args-desc args "popover-content-wrapper")]}
@position-injected ;; Dereference this atom. Although nothing here needs its value explicitly, the calculation of left-offset and top-offset are affected by it for :no-clip? true
[:div
{:class "popover-content-wrapper"
:style (merge (flex-child-style "inherit")
(when no-clip? {:position "fixed"
:left (px @left-offset)
:top (px @top-offset)})
style)}
(when (and @showing-injected? on-cancel)
[backdrop
:opacity backdrop-opacity
:on-click on-cancel])
[popover-border
:position position-injected
:position-offset position-offset
:width width
:height height
:tooltip-style? tooltip-style?
:popover-color popover-color
:arrow-length arrow-length
:arrow-width arrow-width
:arrow-gap arrow-gap
:padding padding
:title (when title [popover-title
:title title
:showing? showing-injected?
:close-button? close-button?
:close-callback on-cancel])
:children [body]]])})))
;;--------------------------------------------------------------------------------------------------
;; Component: popover-anchor-wrapper
;;--------------------------------------------------------------------------------------------------
(def popover-anchor-wrapper-args-desc
[{:name :showing? :required true :type "boolean atom" :description "an atom. When the value is true, the popover shows"}
{:name :position :required true :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :anchor :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "the component the popover is attached to"}
{:name :popover :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "the popover body component"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "override component style(s) with a style map, only use in case of emergency"}])
(defn popover-anchor-wrapper
"Renders an element or control along with a Bootstrap popover"
[& {:keys [showing? position anchor popover style] :as args}]
{:pre [(validate-args-macro popover-anchor-wrapper-args-desc args "popover-anchor-wrapper")]}
(let [external-position (reagent/atom position)
internal-position (reagent/atom @external-position)
reset-on-hide (reaction (when-not @showing? (reset! internal-position @external-position)))]
(reagent/create-class
{:display-name "popover-anchor-wrapper"
:reagent-render
(fn
[& {:keys [showing? position anchor popover style] :as args}]
{:pre [(validate-args-macro popover-anchor-wrapper-args-desc args "popover-anchor-wrapper")]}
@reset-on-hide ;; Dereference this reaction, otherwise it won't be set up. The reaction is set to run whenever the popover closes
(when (not= @external-position position) ;; Has position changed externally?
(reset! external-position position)
(reset! internal-position @external-position))
(let [[orientation arrow-pos] (split-keyword @internal-position "-") ;; only need orientation here
place-anchor-before? (case orientation (:left :above) false true)
flex-flow (case orientation (:left :right) "row" "column")]
[:div {:class "rc-popover-anchor-wrapper display-inline-flex"
:style (merge (flex-child-style "inherit")
style)}
[:div ;; Wrapper around the anchor and the "point"
{:class "rc-point-wrapper display-inline-flex"
:style (merge (flex-child-style "auto")
(flex-flow-style flex-flow)
(align-style :align-items :center))}
(when place-anchor-before? anchor)
(when @showing?
[:div ;; The "point" that connects the anchor to the popover
{:class "rc-popover-point display-inline-flex"
:style (merge (flex-child-style "auto")
{:position "relative"
:z-index 4})}
(into popover [:showing-injected? showing? :position-injected internal-position])]) ;; NOTE: Inject showing? and position to the popover
(when-not place-anchor-before? anchor)]]))})))
;;--------------------------------------------------------------------------------------------------
;; Component: popover-tooltip
;;--------------------------------------------------------------------------------------------------
(def popover-tooltip-args-desc
[{:name :label :required true :type "string | hiccup | atom" :validate-fn string-or-hiccup? :description "the text (or component) for the tooltip"}
{:name :showing? :required true :type "boolean atom" :description "an atom. When the value is true, the tooltip shows"}
{:name :on-cancel :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the popover is cancelled (e.g. user clicks away)"}
{:name :close-button? :required false :default false :type "boolean" :description "when true, displays the close button"}
{:name :status :required false :type "keyword" :validate-fn popover-status-type? :description [:span "controls background color of the tooltip. " [:code "nil/omitted"] " for black or one of " popover-status-types-list " (although " [:code ":validating"] " is only used by the input-text component)"]}
{:name :anchor :required true :type "hiccup" :validate-fn string-or-hiccup? :description "the component the tooltip is attached to"}
{:name :position :required false :default :below-center :type "keyword" :validate-fn position? :description [:span "relative to this anchor. One of " position-options-list]}
{:name :no-clip? :required false :default true :type "boolean" :description "when an anchor is in a scrolling region (e.g. scroller component), the popover can sometimes be clipped. When this parameter is true (which is the default), re-com will use a different CSS method to show the popover. This method is slightly inferior because the popover can't track the anchor if it is repositioned"}
{:name :width :required false :type "string" :validate-fn string? :description "specifies width of the tooltip"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "override component style(s) with a style map, only use in case of emergency"}])
(defn popover-tooltip
"Renders text as a tooltip in Bootstrap popover style"
[& {:keys [label showing? on-cancel close-button? status anchor position no-clip? width style]
:or {no-clip? true}
:as args}]
{:pre [(validate-args-macro popover-tooltip-args-desc args "popover-tooltip")]}
(let [label (deref-or-value label)
popover-color (case status
:warning "#f57c00"
:error "#d50000"
:info "#333333"
:success "#13C200"
"black")]
[popover-anchor-wrapper
:showing? showing?
:position (or position :below-center)
:anchor anchor
:style style
:popover [popover-content-wrapper
:no-clip? no-clip?
:on-cancel on-cancel
:width width
:tooltip-style? true
:popover-color popover-color
:padding "3px 8px"
:arrow-length 6
:arrow-width 12
:arrow-gap 4
:body [v-box
:style (if (= status :info)
{:color "white"
:font-size "14px"
:padding "4px"}
{:color "white"
:font-size "12px"
:font-weight "bold"
:text-align "center"})
:children [label (when close-button?
[close-button showing? on-cancel {:font-size "20px"
:color "white"
:text-shadow "none"
:right "1px"}])]]]]))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,154 @@
(ns re-com.selection-list
(:require-macros [re-com.core :refer [handler-fn]])
(:require
[re-com.text :refer [label]]
[re-com.misc :refer [checkbox radio-button]]
[re-com.box :refer [box border h-box v-box]]
[re-com.validate :refer [vector-of-maps? string-or-atom? set-or-atom?] :refer-macros [validate-args-macro]]
[re-com.util :refer [fmap deref-or-value]]))
;; ----------------------------------------------------------------------------
(defn label-style
([selected? as-exclusions?]
(label-style selected? as-exclusions? nil))
([selected? as-exclusions? selected-color]
;;TODO: margin-top required because currently checkbox & radio-button don't center label
(let [base-style {:margin-top "1px"}
base-style (if (and selected? as-exclusions?)
(merge base-style {:text-decoration "line-through"})
base-style)
base-style (if (and selected? selected-color)
(merge base-style {:color selected-color})
base-style)]
base-style)))
(defn- check-clicked
[selections item-id ticked? required?]
(let [num-selected (count selections)
only-item (when (= 1 num-selected) (first selections))]
(if (and required? (= only-item item-id))
selections ;; prevent unselect of last item
(if ticked? (conj selections item-id) (disj selections item-id)))))
(defn- as-checked
[item id-fn selections on-change disabled? label-fn required? as-exclusions?]
;;TODO: Do we really need an anchor now that bootstrap styles not realy being used ?
(let [item-id (id-fn item)]
[box
:class "list-group-item compact"
:attr {:on-click (handler-fn (when-not disabled?
(on-change (check-clicked selections item-id (not (selections item-id)) required?))))}
:child [checkbox
:model (some? (selections item-id))
:on-change #() ;; handled by enclosing box
:disabled? disabled?
:label-style (label-style (selections item-id) as-exclusions?)
:label (label-fn item)]]))
(defn- radio-clicked
[selections item-id required?]
(if (and required? (selections item-id))
selections ;; prevent unselect of radio
(if (selections item-id) #{} #{item-id})))
(defn- as-radio
[item id-fn selections on-change disabled? label-fn required? as-exclusions?]
(let [item-id (id-fn item)]
[box
:class "list-group-item compact"
:attr {:on-click (handler-fn (when-not disabled?
(on-change (radio-clicked selections item-id required?))))}
:child [radio-button
:model (first selections)
:value item-id
:on-change #() ;; handled by enclosing box
:disabled? disabled?
:label-style (label-style (selections item-id) as-exclusions?)
:label (label-fn item)]]))
(def ^:const list-style
;;TODO: These should be in CSS resource
{:overflow-x "hidden"
:overflow-y "auto"}) ;;TODO this should be handled by scroller later
(def ^:const spacing-bordered
{:padding-top "0px"
:padding-bottom "0px"
:padding-left "5px"
:padding-right "5px"
:margin-top "5px"
:margin-bottom "5px"})
(def ^:const spacing-unbordered
{:padding-left "0px"
:padding-right "5px"
:padding-top "0px"
:padding-bottom "0px"
:margin-top "0px"
:margin-bottom "0px"})
(def selection-list-args-desc
[{:name :choices :required true :type "vector of choices | atom" :validate-fn vector-of-maps? :description [:span "the selectable items. Elements can be strings or more interesting data items like {:label \"some name\" :sort 5}. Also see " [:code ":label-fn"] " below (list of maps also allowed)"]}
{:name :model :required true :type "set of :ids within :choices | atom" :validate-fn set-or-atom? :description "the currently selected items. Note: items are considered distinct"}
{:name :on-change :required true :type "set of :ids -> nil | atom" :validate-fn fn? :description [:span "a callback which will be passed set of the ids (as defined by " [:code ":id-fn"] ") of the selected items"]}
{:name :id-fn :required false :default :id :type "choice -> anything" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns its unique identifier (aka id)"]}
{:name :label-fn :required false :default :label :type "choice -> anything" :validate-fn ifn? :description [:span "given an element of " [:code ":choices"] ", returns its displayable label"]}
{:name :multi-select? :required false :default true :type "boolean | atom" :description "when true, use check boxes, otherwise radio buttons"}
{:name :as-exclusions? :required false :default false :type "boolean | atom" :description "when true, selected items are shown with struck-out labels"}
{:name :required? :required false :default false :type "boolean | atom" :description "when true, at least one item must be selected. Note: being able to un-select a radio button is not a common use case, so this should probably be set to true when in single select mode"}
{:name :width :required false :type "string | atom" :validate-fn string-or-atom? :description "a CSS style e.g. \"250px\". When specified, item labels may be clipped. Otherwise based on widest label"}
{:name :height :required false :type "string | atom" :validate-fn string-or-atom? :description "a CSS style e.g. \"150px\". Size beyond which items will scroll"}
{:name :max-height :required false :type "string | atom" :validate-fn string-or-atom? :description "a CSS style e.g. \"150px\". If there are less items then this height, box will shrink. If there are more, items will scroll"}
{:name :disabled? :required false :default false :type "boolean | atom" :description "when true, the time input will be disabled. Can be atom or value"}
{:name :hide-border? :required false :default false :type "boolean | atom" :description "when true, the list will be displayed without a border"}
{:name :item-renderer :required false :type "-> nil | atom" :validate-fn fn? :description "a function which takes no params and returns nothing. Called for each element during setup, the returned component renders the element, responds to clicks etc."}])
;;TODO hide hover highlights for links when disabled
(defn- list-container
[{:keys [choices model on-change id-fn multi-select? disabled? hide-border? label-fn required? as-exclusions? item-renderer]
:as args}]
{:pre [(validate-args-macro selection-list-args-desc args "selection-list")]}
(let [selected (if multi-select? model (-> model first vector set))
items (map (if item-renderer
#(item-renderer % id-fn selected on-change disabled? label-fn required? as-exclusions?) ;; TODO do we need to pass id-fn?
(if multi-select?
#(as-checked % id-fn selected on-change disabled? label-fn required? as-exclusions?)
#(as-radio % id-fn selected on-change disabled? label-fn required? as-exclusions?)))
choices)
bounds (select-keys args [:width :height :max-height])
spacing (if hide-border? spacing-unbordered spacing-bordered)]
;; In single select mode force selections to one. This causes a second render
;; TODO: GR commented this out to fix the bug where #{nil} was being returned for an empty list. Remove when we're sure there are no ill effects.
#_(when-not (= selected model) (on-change selected))
[border
:radius "4px"
:border (when hide-border? "none")
:child (into [:div {:class "list-group noselect" :style (merge list-style bounds spacing)}] items)]))
(defn- configure
"Augment passed attributes with defaults and deref any atoms"
[attributes]
(merge {:multi-select? true
:as-exclusions? false
:required? false
:disabled? false
:hide-border? false
:id-fn :id
:label-fn :label}
(fmap deref-or-value attributes)))
(defn selection-list
"Produce a list box with items arranged vertically"
[& {:as args}]
{:pre [(validate-args-macro selection-list-args-desc args "selection-list")]}
;;NOTE: Consumer has complete control over what is selected or not. A current design tradeoff
;; causes all selection changes to trigger a complete list re-render as a result of on-change callback.
;; this approach may be not ideal for very large list choices.
(fn [& {:as args}]
[list-container (configure args)]))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,214 @@
(ns re-com.splits
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [get-element-by-id sum-scroll-offsets]]
[re-com.box :refer [flex-child-style flex-flow-style]]
[re-com.validate :refer [string-or-hiccup? number-or-string? html-attr? css-style?] :refer-macros [validate-args-macro]]
[reagent.core :as reagent]))
(defn drag-handle
"Return a drag handle to go into a vertical or horizontal splitter bar:
orientation: Can be :horizonal or :vertical
over?: When true, the mouse is assumed to be over the splitter so show a bolder color"
[orientation over?]
(let [vertical? (= orientation :vertical)
length "20px"
width "8px"
pos1 "3px"
pos2 "3px"
color (if over? "#999" "#ccc")
border (str "solid 1px " color)
flex-flow (str (if vertical? "row" "column") " nowrap")]
[:div {:class "display-flex"
:style (merge (flex-flow-style flex-flow)
{:width (if vertical? width length)
:height (if vertical? length width)
:margin "auto"})}
[:div {:style (if vertical?
{:width pos1 :height length :border-right border}
{:width length :height pos1 :border-bottom border})}]
[:div {:style (if vertical?
{:width pos2 :height length :border-right border}
{:width length :height pos2 :border-bottom border})}]]))
;; ------------------------------------------------------------------------------------
;; Component: h-split
;; ------------------------------------------------------------------------------------
(def hv-split-args-desc
[{:name :panel-1 :required true :type "hiccup" :validate-fn string-or-hiccup? :description "markup to go in the left (or top) panel"}
{:name :panel-2 :required true :type "hiccup" :validate-fn string-or-hiccup? :description "markup to go in the right (or bottom) panel"}
{:name :size :required false :default "auto" :type "string" :validate-fn string? :description [:span "applied to the outer container of the two panels. Equivalent to CSS style " [:span.bold "flex"] "." [:br] "Examples: " [:code "initial"] ", " [:code "auto"] ", " [:code "none"]", " [:code "100px"] ", " [:code "2"] " or a generic triple of " [:code "grow shrink basis"]]}
{:name :width :required false :type "string" :validate-fn string? :description "width of the outer container of the two panels. A CSS width style"}
{:name :height :required false :type "string" :validate-fn string? :description "height of the outer container of the two panels. A CSS height style"}
{:name :on-split-change :required false :type "double -> nil" :validate-fn fn? :description [:span "called when the user moves the splitter bar (on mouse up, not on each mouse move). Given the new " [:code ":panel-1"] " percentage split"]}
{:name :initial-split :required false :default 50 :type "double | string" :validate-fn number-or-string? :description [:span "initial split percentage for " [:code ":panel-1"] ". Can be double value or string (with/without percentage sign)"]}
{:name :splitter-size :required false :default "8px" :type "string" :validate-fn string? :description "thickness of the splitter"}
{:name :margin :required false :default "8px" :type "string" :validate-fn string? :description "thickness of the margin around the panels"}
{:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated, applied to outer container"}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override, applied to outer container"}
{: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, applied to outer container"]}])
(defn h-split
"Returns markup for a horizontal layout component"
[& {:keys [size width height on-split-change initial-split splitter-size margin]
:or {size "auto" initial-split 50 splitter-size "8px" margin "8px"}
:as args}]
{:pre [(validate-args-macro hv-split-args-desc args "h-split")]}
(let [container-id (gensym "h-split-")
split-perc (reagent/atom (js/parseInt initial-split)) ;; splitter position as a percentage of width
dragging? (reagent/atom false) ;; is the user dragging the splitter (mouse is down)?
over? (reagent/atom false) ;; is the mouse over the splitter, if so, highlight it
stop-drag (fn []
(when on-split-change (on-split-change @split-perc))
(reset! dragging? false))
calc-perc (fn [mouse-x] ;; turn a mouse y coordinate into a percentage position
(let [container (get-element-by-id container-id) ;; the outside container
offsets (sum-scroll-offsets container) ;; take any scrolling into account
c-width (.-clientWidth container) ;; the container's width
c-left-x (.-offsetLeft container) ;; the container's left X
relative-x (+ (- mouse-x c-left-x) (:left offsets))] ;; the X of the mouse, relative to container
(* 100.0 (/ relative-x c-width)))) ;; do the percentage calculation
<html>? #(= % (.-documentElement js/document)) ;; test for the <html> element
mouseout (fn [event]
(if (<html>? (.-relatedTarget event)) ;; stop drag if we leave the <html> element
(stop-drag)))
mousemove (fn [event]
(reset! split-perc (calc-perc (.-clientX event))))
mousedown (fn [event]
(.preventDefault event) ;; stop selection of text during drag
(reset! dragging? true))
mouseover-split #(reset! over? true) ;; true CANCELs mouse-over (false cancels all others)
mouseout-split #(reset! over? false)
make-container-attrs (fn [class style attr in-drag?]
(merge {:class (str "rc-h-split display-flex " class)
:id container-id
:style (merge (flex-child-style size)
(flex-flow-style "row nowrap")
{:margin margin
:width width
:height height}
style)}
(when in-drag? ;; only listen when we are dragging
{:on-mouse-up (handler-fn (stop-drag))
:on-mouse-move (handler-fn (mousemove event))
:on-mouse-out (handler-fn (mouseout event))})
attr))
make-panel-attrs (fn [class in-drag? percentage]
{:class (str "display-flex " class)
:style (merge (flex-child-style (str percentage " 1 0px"))
{:overflow "hidden"} ;; TODO: Shouldn't have this...test removing it
(when in-drag? {:pointer-events "none"}))})
make-splitter-attrs (fn [class]
{:class (str "display-flex " class)
:on-mouse-down (handler-fn (mousedown event))
:on-mouse-over (handler-fn (mouseover-split))
:on-mouse-out (handler-fn (mouseout-split))
:style (merge (flex-child-style (str "0 0 " splitter-size))
{:cursor "col-resize"}
(when @over? {:background-color "#f8f8f8"}))})]
(fn
[& {:keys [panel-1 panel-2 class style attr]}]
[:div (make-container-attrs class style attr @dragging?)
[:div (make-panel-attrs "rc-h-split-top" @dragging? @split-perc)
panel-1]
[:div (make-splitter-attrs "rc-h-split-splitter")
[drag-handle :vertical @over?]]
[:div (make-panel-attrs "rc-h-split-bottom" @dragging? (- 100 @split-perc))
panel-2]])))
;; ------------------------------------------------------------------------------------
;; Component: v-split
;; ------------------------------------------------------------------------------------
(defn v-split
"Returns markup for a vertical layout component"
[& {:keys [size width height on-split-change initial-split splitter-size margin]
:or {size "auto" initial-split 50 splitter-size "8px" margin "8px"}
:as args}]
{:pre [(validate-args-macro hv-split-args-desc args "v-split")]}
(let [container-id (gensym "v-split-")
split-perc (reagent/atom (js/parseInt initial-split)) ;; splitter position as a percentage of height
dragging? (reagent/atom false) ;; is the user dragging the splitter (mouse is down)?
over? (reagent/atom false) ;; is the mouse over the splitter, if so, highlight it
stop-drag (fn []
(when on-split-change (on-split-change @split-perc))
(reset! dragging? false))
calc-perc (fn [mouse-y] ;; turn a mouse y coordinate into a percentage position
(let [container (get-element-by-id container-id) ;; the outside container
offsets (sum-scroll-offsets container) ;; take any scrolling into account
c-height (.-clientHeight container) ;; the container's height
c-top-y (.-offsetTop container) ;; the container's top Y
relative-y (+ (- mouse-y c-top-y) (:top offsets))] ;; the Y of the mouse, relative to container
(* 100.0 (/ relative-y c-height)))) ;; do the percentage calculation
<html>? #(= % (.-documentElement js/document)) ;; test for the <html> element
mouseout (fn [event]
(if (<html>? (.-relatedTarget event)) ;; stop drag if we leave the <html> element
(stop-drag)))
mousemove (fn [event]
(reset! split-perc (calc-perc (.-clientY event))))
mousedown (fn [event]
(.preventDefault event) ;; stop selection of text during drag
(reset! dragging? true))
mouseover-split #(reset! over? true)
mouseout-split #(reset! over? false)
make-container-attrs (fn [class style attr in-drag?]
(merge {:class (str "rc-v-split display-flex " class)
:id container-id
:style (merge (flex-child-style size)
(flex-flow-style "column nowrap")
{:margin margin
:width width
:height height}
style)}
(when in-drag? ;; only listen when we are dragging
{:on-mouse-up (handler-fn (stop-drag))
:on-mouse-move (handler-fn (mousemove event))
:on-mouse-out (handler-fn (mouseout event))})
attr))
make-panel-attrs (fn [class in-drag? percentage]
{:class (str "display-flex " class)
:style (merge (flex-child-style (str percentage " 1 0px"))
{:overflow "hidden"} ;; TODO: Shouldn't have this...test removing it
(when in-drag? {:pointer-events "none"}))})
make-splitter-attrs (fn [class]
{:class (str "display-flex " class)
:on-mouse-down (handler-fn (mousedown event))
:on-mouse-over (handler-fn (mouseover-split))
:on-mouse-out (handler-fn (mouseout-split))
:style (merge (flex-child-style (str "0 0 " splitter-size))
{:cursor "row-resize"}
(when @over? {:background-color "#f8f8f8"}))})]
(fn
[& {:keys [panel-1 panel-2 class style attr]}]
[:div (make-container-attrs class style attr @dragging?)
[:div (make-panel-attrs "re-v-split-top" @dragging? @split-perc)
panel-1]
[:div (make-splitter-attrs "re-v-split-splitter")
[drag-handle :horizontal @over?]]
[:div (make-panel-attrs "re-v-split-bottom" @dragging? (- 100 @split-perc))
panel-2]])))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,153 @@
(ns re-com.tabs
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.util :refer [deref-or-value]]
[re-com.box :refer [flex-child-style]]
[re-com.validate :refer [css-style? vector-of-maps?] :refer-macros [validate-args-macro]]))
;;--------------------------------------------------------------------------------------------------
;; Component: horizontal-tabs
;;--------------------------------------------------------------------------------------------------
(def tabs-args-desc
[{:name :tabs :required true :type "vector of tabs | atom" :validate-fn vector-of-maps? :description "one element in the vector for each tab. Typically, each element is a map with :id and :label keys"}
{:name :model :required true :type "unique-id | atom" :description "the unique identifier of the currently selected tab"}
{:name :on-change :required true :type "unique-id -> nil" :validate-fn fn? :description "called when user alters the selection. Passed the unique identifier of the selection"}
{:name :id-fn :required false :default :id :type "tab -> anything" :validate-fn ifn? :description [:span "given an element of " [:code ":tabs"] ", returns its unique identifier (aka id)"]}
{:name :label-fn :required false :default :label :type "tab -> string | hiccup" :validate-fn ifn? :description [:span "given an element of " [:code ":tabs"] ", returns its displayable label"]}
{:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override (for each individual tab rather than the container)"}])
(defn horizontal-tabs
[& {:keys [model tabs on-change id-fn label-fn style]
:or {id-fn :id label-fn :label}
:as args}]
{:pre [(validate-args-macro tabs-args-desc args "tabs")]}
(let [current (deref-or-value model)
tabs (deref-or-value tabs)
_ (assert (not-empty (filter #(= current (id-fn %)) tabs)) "model not found in tabs vector")]
[:ul
{:class "rc-tabs nav nav-tabs noselect"
:style (flex-child-style "none")}
(for [t tabs]
(let [id (id-fn t)
label (label-fn t)
selected? (= id current)] ;; must use current instead of @model to avoid reagent warnings
[:li
{:class (if selected? "active")
:key (str id)}
[:a
{:style (merge {:cursor "pointer"}
style)
:on-click (when on-change (handler-fn (on-change id)))}
label]]))]))
;;--------------------------------------------------------------------------------------------------
;; Component: horizontal-bar-tabs
;;--------------------------------------------------------------------------------------------------
(defn- bar-tabs
[& {:keys [model tabs on-change id-fn label-fn style vertical?]}]
(let [current (deref-or-value model)
tabs (deref-or-value tabs)
_ (assert (not-empty (filter #(= current (id-fn %)) tabs)) "model not found in tabs vector")]
[:div
{:class (str "rc-tabs noselect btn-group" (if vertical? "-vertical"))
:style (flex-child-style "none")}
(for [t tabs]
(let [id (id-fn t)
label (label-fn t)
selected? (= id current)] ;; must use current instead of @model to avoid reagent warnings
[:button
{:type "button"
:key (str id)
:class (str "btn btn-default " (if selected? "active"))
:style style
:on-click (when on-change (handler-fn (on-change id)))}
label]))]))
(defn horizontal-bar-tabs
[& {:keys [model tabs on-change id-fn label-fn style]
:or {id-fn :id label-fn :label}
:as args}]
{:pre [(validate-args-macro tabs-args-desc args "tabs")]}
(bar-tabs
:model model
:tabs tabs
:on-change on-change
:style style
:id-fn id-fn
:label-fn label-fn
:vertical? false))
(defn vertical-bar-tabs
[& {:keys [model tabs on-change id-fn label-fn style]
:or {id-fn :id label-fn :label}
:as args}]
{:pre [(validate-args-macro tabs-args-desc args "tabs")]}
(bar-tabs
:model model
:tabs tabs
:on-change on-change
:style style
:id-fn id-fn
:label-fn label-fn
:vertical? true))
;;--------------------------------------------------------------------------------------------------
;; Component: pill-tabs
;;--------------------------------------------------------------------------------------------------
(defn- pill-tabs ;; tabs-like in action
[& {:keys [model tabs on-change id-fn label-fn style vertical?]}]
(let [current (deref-or-value model)
tabs (deref-or-value tabs)
_ (assert (not-empty (filter #(= current (id-fn %)) tabs)) "model not found in tabs vector")]
[:ul
{:class (str "rc-tabs noselect nav nav-pills" (when vertical? " nav-stacked"))
:style (flex-child-style "none")
:role "tabslist"}
(for [t tabs]
(let [id (id-fn t)
label (label-fn t)
selected? (= id current)] ;; must use 'current' instead of @model to avoid reagent warnings
[:li
{:class (if selected? "active" "")
:key (str id)}
[:a
{:style (merge {:cursor "pointer"}
style)
:on-click (when on-change (handler-fn (on-change id)))}
label]]))]))
(defn horizontal-pill-tabs
[& {:keys [model tabs on-change id-fn style label-fn]
:or {id-fn :id label-fn :label}
:as args}]
{:pre [(validate-args-macro tabs-args-desc args "tabs")]}
(pill-tabs
:model model
:tabs tabs
:on-change on-change
:style style
:id-fn id-fn
:label-fn label-fn
:vertical? false))
(defn vertical-pill-tabs
[& {:keys [model tabs on-change id-fn style label-fn]
:or {id-fn :id label-fn :label}
:as args}]
{:pre [(validate-args-macro tabs-args-desc args "tabs")]}
(pill-tabs
:model model
:tabs tabs
:on-change on-change
:style style
:id-fn id-fn
:label-fn label-fn
:vertical? true))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,526 @@
// Compiled by ClojureScript 1.9.229 {}
goog.provide('re_com.tabs');
goog.require('cljs.core');
goog.require('re_com.util');
goog.require('re_com.box');
goog.require('re_com.validate');
re_com.tabs.tabs_args_desc = new cljs.core.PersistentVector(null, 6, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.PersistentArrayMap(null, 5, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"tabs","tabs",-779855354),new cljs.core.Keyword(null,"required","required",1807647006),true,new cljs.core.Keyword(null,"type","type",1174270348),"vector of tabs | atom",new cljs.core.Keyword(null,"validate-fn","validate-fn",1430169944),re_com.validate.vector_of_maps_QMARK_,new cljs.core.Keyword(null,"description","description",-1428560544),"one element in the vector for each tab. Typically, each element is a map with :id and :label keys"], null),new cljs.core.PersistentArrayMap(null, 4, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"model","model",331153215),new cljs.core.Keyword(null,"required","required",1807647006),true,new cljs.core.Keyword(null,"type","type",1174270348),"unique-id | atom",new cljs.core.Keyword(null,"description","description",-1428560544),"the unique identifier of the currently selected tab"], null),new cljs.core.PersistentArrayMap(null, 5, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"on-change","on-change",-732046149),new cljs.core.Keyword(null,"required","required",1807647006),true,new cljs.core.Keyword(null,"type","type",1174270348),"unique-id -> nil",new cljs.core.Keyword(null,"validate-fn","validate-fn",1430169944),cljs.core.fn_QMARK_,new cljs.core.Keyword(null,"description","description",-1428560544),"called when user alters the selection. Passed the unique identifier of the selection"], null),new cljs.core.PersistentArrayMap(null, 6, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"required","required",1807647006),false,new cljs.core.Keyword(null,"default","default",-1987822328),new cljs.core.Keyword(null,"id","id",-1388402092),new cljs.core.Keyword(null,"type","type",1174270348),"tab -> anything",new cljs.core.Keyword(null,"validate-fn","validate-fn",1430169944),cljs.core.ifn_QMARK_,new cljs.core.Keyword(null,"description","description",-1428560544),new cljs.core.PersistentVector(null, 4, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"span","span",1394872991),"given an element of ",new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"code","code",1586293142),":tabs"], null),", returns its unique identifier (aka id)"], null)], null),new cljs.core.PersistentArrayMap(null, 6, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"required","required",1807647006),false,new cljs.core.Keyword(null,"default","default",-1987822328),new cljs.core.Keyword(null,"label","label",1718410804),new cljs.core.Keyword(null,"type","type",1174270348),"tab -> string | hiccup",new cljs.core.Keyword(null,"validate-fn","validate-fn",1430169944),cljs.core.ifn_QMARK_,new cljs.core.Keyword(null,"description","description",-1428560544),new cljs.core.PersistentVector(null, 4, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"span","span",1394872991),"given an element of ",new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"code","code",1586293142),":tabs"], null),", returns its displayable label"], null)], null),new cljs.core.PersistentArrayMap(null, 5, [new cljs.core.Keyword(null,"name","name",1843675177),new cljs.core.Keyword(null,"style","style",-496642736),new cljs.core.Keyword(null,"required","required",1807647006),false,new cljs.core.Keyword(null,"type","type",1174270348),"CSS style map",new cljs.core.Keyword(null,"validate-fn","validate-fn",1430169944),re_com.validate.css_style_QMARK_,new cljs.core.Keyword(null,"description","description",-1428560544),"CSS styles to add or override (for each individual tab rather than the container)"], null)], null);
re_com.tabs.horizontal_tabs = (function re_com$tabs$horizontal_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31634 = arguments.length;
var i__26206__auto___31635 = (0);
while(true){
if((i__26206__auto___31635 < len__26205__auto___31634)){
args__26212__auto__.push((arguments[i__26206__auto___31635]));
var G__31636 = (i__26206__auto___31635 + (1));
i__26206__auto___31635 = G__31636;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.horizontal_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.horizontal_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31627){
var map__31628 = p__31627;
var map__31628__$1 = ((((!((map__31628 == null)))?((((map__31628.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31628.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31628):map__31628);
var args = map__31628__$1;
var model = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var label_fn = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"label","label",1718410804));
var style = cljs.core.get.call(null,map__31628__$1,new cljs.core.Keyword(null,"style","style",-496642736));
if(cljs.core.truth_(((!(goog.DEBUG))?true:re_com.validate.validate_args.call(null,re_com.validate.extract_arg_data.call(null,re_com.tabs.tabs_args_desc),args,"tabs")))){
} else {
throw (new Error("Assert failed: (validate-args-macro tabs-args-desc args \"tabs\")"));
}
var current = re_com.util.deref_or_value.call(null,model);
var tabs__$1 = re_com.util.deref_or_value.call(null,tabs);
var _ = (cljs.core.truth_(cljs.core.not_empty.call(null,cljs.core.filter.call(null,((function (current,tabs__$1,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style){
return (function (p1__31625_SHARP_){
return cljs.core._EQ_.call(null,current,id_fn.call(null,p1__31625_SHARP_));
});})(current,tabs__$1,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style))
,tabs__$1)))?null:(function(){throw (new Error([cljs.core.str("Assert failed: "),cljs.core.str("model not found in tabs vector"),cljs.core.str("\n"),cljs.core.str("(not-empty (filter (fn* [p1__31625#] (= current (id-fn p1__31625#))) tabs))")].join('')))})());
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"ul","ul",-1349521403),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),"rc-tabs nav nav-tabs noselect",new cljs.core.Keyword(null,"style","style",-496642736),re_com.box.flex_child_style.call(null,"none")], null),(function (){var iter__25910__auto__ = ((function (current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style){
return (function re_com$tabs$iter__31630(s__31631){
return (new cljs.core.LazySeq(null,((function (current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style){
return (function (){
var s__31631__$1 = s__31631;
while(true){
var temp__4657__auto__ = cljs.core.seq.call(null,s__31631__$1);
if(temp__4657__auto__){
var s__31631__$2 = temp__4657__auto__;
if(cljs.core.chunked_seq_QMARK_.call(null,s__31631__$2)){
var c__25908__auto__ = cljs.core.chunk_first.call(null,s__31631__$2);
var size__25909__auto__ = cljs.core.count.call(null,c__25908__auto__);
var b__31633 = cljs.core.chunk_buffer.call(null,size__25909__auto__);
if((function (){var i__31632 = (0);
while(true){
if((i__31632 < size__25909__auto__)){
var t = cljs.core._nth.call(null,c__25908__auto__,i__31632);
cljs.core.chunk_append.call(null,b__31633,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"li","li",723558921),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),((selected_QMARK_)?"active":null),new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join('')], null),new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"a","a",-2123407586),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"style","style",-496642736),cljs.core.merge.call(null,new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"cursor","cursor",1011937484),"pointer"], null),style),new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (i__31632,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31633,s__31631__$2,temp__4657__auto__,current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style){
return (function (event){
on_change.call(null,id);
return null;
});})(i__31632,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31633,s__31631__$2,temp__4657__auto__,current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style))
:null)], null),label], null)], null);
})());
var G__31637 = (i__31632 + (1));
i__31632 = G__31637;
continue;
} else {
return true;
}
break;
}
})()){
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31633),re_com$tabs$iter__31630.call(null,cljs.core.chunk_rest.call(null,s__31631__$2)));
} else {
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31633),null);
}
} else {
var t = cljs.core.first.call(null,s__31631__$2);
return cljs.core.cons.call(null,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"li","li",723558921),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),((selected_QMARK_)?"active":null),new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join('')], null),new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"a","a",-2123407586),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"style","style",-496642736),cljs.core.merge.call(null,new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"cursor","cursor",1011937484),"pointer"], null),style),new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (id,label,selected_QMARK_,t,s__31631__$2,temp__4657__auto__,current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style){
return (function (event){
on_change.call(null,id);
return null;
});})(id,label,selected_QMARK_,t,s__31631__$2,temp__4657__auto__,current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style))
:null)], null),label], null)], null);
})(),re_com$tabs$iter__31630.call(null,cljs.core.rest.call(null,s__31631__$2)));
}
} else {
return null;
}
break;
}
});})(current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style))
,null,null));
});})(current,tabs__$1,_,map__31628,map__31628__$1,args,model,tabs,on_change,id_fn,label_fn,style))
;
return iter__25910__auto__.call(null,tabs__$1);
})()], null);
});
re_com.tabs.horizontal_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.horizontal_tabs.cljs$lang$applyTo = (function (seq31626){
return re_com.tabs.horizontal_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31626));
});
re_com.tabs.bar_tabs = (function re_com$tabs$bar_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31647 = arguments.length;
var i__26206__auto___31648 = (0);
while(true){
if((i__26206__auto___31648 < len__26205__auto___31647)){
args__26212__auto__.push((arguments[i__26206__auto___31648]));
var G__31649 = (i__26206__auto___31648 + (1));
i__26206__auto___31648 = G__31649;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.bar_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.bar_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31640){
var map__31641 = p__31640;
var map__31641__$1 = ((((!((map__31641 == null)))?((((map__31641.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31641.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31641):map__31641);
var model = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798));
var label_fn = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263));
var style = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"style","style",-496642736));
var vertical_QMARK_ = cljs.core.get.call(null,map__31641__$1,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444));
var current = re_com.util.deref_or_value.call(null,model);
var tabs__$1 = re_com.util.deref_or_value.call(null,tabs);
var _ = (cljs.core.truth_(cljs.core.not_empty.call(null,cljs.core.filter.call(null,((function (current,tabs__$1,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (p1__31638_SHARP_){
return cljs.core._EQ_.call(null,current,id_fn.call(null,p1__31638_SHARP_));
});})(current,tabs__$1,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
,tabs__$1)))?null:(function(){throw (new Error([cljs.core.str("Assert failed: "),cljs.core.str("model not found in tabs vector"),cljs.core.str("\n"),cljs.core.str("(not-empty (filter (fn* [p1__31638#] (= current (id-fn p1__31638#))) tabs))")].join('')))})());
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"div","div",1057191632),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),[cljs.core.str("rc-tabs noselect btn-group"),cljs.core.str((cljs.core.truth_(vertical_QMARK_)?"-vertical":null))].join(''),new cljs.core.Keyword(null,"style","style",-496642736),re_com.box.flex_child_style.call(null,"none")], null),(function (){var iter__25910__auto__ = ((function (current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function re_com$tabs$iter__31643(s__31644){
return (new cljs.core.LazySeq(null,((function (current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (){
var s__31644__$1 = s__31644;
while(true){
var temp__4657__auto__ = cljs.core.seq.call(null,s__31644__$1);
if(temp__4657__auto__){
var s__31644__$2 = temp__4657__auto__;
if(cljs.core.chunked_seq_QMARK_.call(null,s__31644__$2)){
var c__25908__auto__ = cljs.core.chunk_first.call(null,s__31644__$2);
var size__25909__auto__ = cljs.core.count.call(null,c__25908__auto__);
var b__31646 = cljs.core.chunk_buffer.call(null,size__25909__auto__);
if((function (){var i__31645 = (0);
while(true){
if((i__31645 < size__25909__auto__)){
var t = cljs.core._nth.call(null,c__25908__auto__,i__31645);
cljs.core.chunk_append.call(null,b__31646,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"button","button",1456579943),new cljs.core.PersistentArrayMap(null, 5, [new cljs.core.Keyword(null,"type","type",1174270348),"button",new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join(''),new cljs.core.Keyword(null,"class","class",-2030961996),[cljs.core.str("btn btn-default "),cljs.core.str(((selected_QMARK_)?"active":null))].join(''),new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (i__31645,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31646,s__31644__$2,temp__4657__auto__,current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (event){
on_change.call(null,id);
return null;
});})(i__31645,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31646,s__31644__$2,temp__4657__auto__,current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
:null)], null),label], null);
})());
var G__31650 = (i__31645 + (1));
i__31645 = G__31650;
continue;
} else {
return true;
}
break;
}
})()){
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31646),re_com$tabs$iter__31643.call(null,cljs.core.chunk_rest.call(null,s__31644__$2)));
} else {
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31646),null);
}
} else {
var t = cljs.core.first.call(null,s__31644__$2);
return cljs.core.cons.call(null,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"button","button",1456579943),new cljs.core.PersistentArrayMap(null, 5, [new cljs.core.Keyword(null,"type","type",1174270348),"button",new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join(''),new cljs.core.Keyword(null,"class","class",-2030961996),[cljs.core.str("btn btn-default "),cljs.core.str(((selected_QMARK_)?"active":null))].join(''),new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (id,label,selected_QMARK_,t,s__31644__$2,temp__4657__auto__,current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (event){
on_change.call(null,id);
return null;
});})(id,label,selected_QMARK_,t,s__31644__$2,temp__4657__auto__,current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
:null)], null),label], null);
})(),re_com$tabs$iter__31643.call(null,cljs.core.rest.call(null,s__31644__$2)));
}
} else {
return null;
}
break;
}
});})(current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
,null,null));
});})(current,tabs__$1,_,map__31641,map__31641__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
;
return iter__25910__auto__.call(null,tabs__$1);
})()], null);
});
re_com.tabs.bar_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.bar_tabs.cljs$lang$applyTo = (function (seq31639){
return re_com.tabs.bar_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31639));
});
re_com.tabs.horizontal_bar_tabs = (function re_com$tabs$horizontal_bar_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31655 = arguments.length;
var i__26206__auto___31656 = (0);
while(true){
if((i__26206__auto___31656 < len__26205__auto___31655)){
args__26212__auto__.push((arguments[i__26206__auto___31656]));
var G__31657 = (i__26206__auto___31656 + (1));
i__26206__auto___31656 = G__31657;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.horizontal_bar_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.horizontal_bar_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31652){
var map__31653 = p__31652;
var map__31653__$1 = ((((!((map__31653 == null)))?((((map__31653.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31653.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31653):map__31653);
var args = map__31653__$1;
var model = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var label_fn = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"label","label",1718410804));
var style = cljs.core.get.call(null,map__31653__$1,new cljs.core.Keyword(null,"style","style",-496642736));
if(cljs.core.truth_(((!(goog.DEBUG))?true:re_com.validate.validate_args.call(null,re_com.validate.extract_arg_data.call(null,re_com.tabs.tabs_args_desc),args,"tabs")))){
} else {
throw (new Error("Assert failed: (validate-args-macro tabs-args-desc args \"tabs\")"));
}
return re_com.tabs.bar_tabs.call(null,new cljs.core.Keyword(null,"model","model",331153215),model,new cljs.core.Keyword(null,"tabs","tabs",-779855354),tabs,new cljs.core.Keyword(null,"on-change","on-change",-732046149),on_change,new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),id_fn,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),label_fn,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444),false);
});
re_com.tabs.horizontal_bar_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.horizontal_bar_tabs.cljs$lang$applyTo = (function (seq31651){
return re_com.tabs.horizontal_bar_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31651));
});
re_com.tabs.vertical_bar_tabs = (function re_com$tabs$vertical_bar_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31662 = arguments.length;
var i__26206__auto___31663 = (0);
while(true){
if((i__26206__auto___31663 < len__26205__auto___31662)){
args__26212__auto__.push((arguments[i__26206__auto___31663]));
var G__31664 = (i__26206__auto___31663 + (1));
i__26206__auto___31663 = G__31664;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.vertical_bar_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.vertical_bar_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31659){
var map__31660 = p__31659;
var map__31660__$1 = ((((!((map__31660 == null)))?((((map__31660.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31660.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31660):map__31660);
var args = map__31660__$1;
var model = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var label_fn = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"label","label",1718410804));
var style = cljs.core.get.call(null,map__31660__$1,new cljs.core.Keyword(null,"style","style",-496642736));
if(cljs.core.truth_(((!(goog.DEBUG))?true:re_com.validate.validate_args.call(null,re_com.validate.extract_arg_data.call(null,re_com.tabs.tabs_args_desc),args,"tabs")))){
} else {
throw (new Error("Assert failed: (validate-args-macro tabs-args-desc args \"tabs\")"));
}
return re_com.tabs.bar_tabs.call(null,new cljs.core.Keyword(null,"model","model",331153215),model,new cljs.core.Keyword(null,"tabs","tabs",-779855354),tabs,new cljs.core.Keyword(null,"on-change","on-change",-732046149),on_change,new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),id_fn,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),label_fn,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444),true);
});
re_com.tabs.vertical_bar_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.vertical_bar_tabs.cljs$lang$applyTo = (function (seq31658){
return re_com.tabs.vertical_bar_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31658));
});
re_com.tabs.pill_tabs = (function re_com$tabs$pill_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31674 = arguments.length;
var i__26206__auto___31675 = (0);
while(true){
if((i__26206__auto___31675 < len__26205__auto___31674)){
args__26212__auto__.push((arguments[i__26206__auto___31675]));
var G__31676 = (i__26206__auto___31675 + (1));
i__26206__auto___31675 = G__31676;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.pill_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.pill_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31667){
var map__31668 = p__31667;
var map__31668__$1 = ((((!((map__31668 == null)))?((((map__31668.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31668.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31668):map__31668);
var model = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798));
var label_fn = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263));
var style = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"style","style",-496642736));
var vertical_QMARK_ = cljs.core.get.call(null,map__31668__$1,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444));
var current = re_com.util.deref_or_value.call(null,model);
var tabs__$1 = re_com.util.deref_or_value.call(null,tabs);
var _ = (cljs.core.truth_(cljs.core.not_empty.call(null,cljs.core.filter.call(null,((function (current,tabs__$1,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (p1__31665_SHARP_){
return cljs.core._EQ_.call(null,current,id_fn.call(null,p1__31665_SHARP_));
});})(current,tabs__$1,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
,tabs__$1)))?null:(function(){throw (new Error([cljs.core.str("Assert failed: "),cljs.core.str("model not found in tabs vector"),cljs.core.str("\n"),cljs.core.str("(not-empty (filter (fn* [p1__31665#] (= current (id-fn p1__31665#))) tabs))")].join('')))})());
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"ul","ul",-1349521403),new cljs.core.PersistentArrayMap(null, 3, [new cljs.core.Keyword(null,"class","class",-2030961996),[cljs.core.str("rc-tabs noselect nav nav-pills"),cljs.core.str((cljs.core.truth_(vertical_QMARK_)?" nav-stacked":null))].join(''),new cljs.core.Keyword(null,"style","style",-496642736),re_com.box.flex_child_style.call(null,"none"),new cljs.core.Keyword(null,"role","role",-736691072),"tabslist"], null),(function (){var iter__25910__auto__ = ((function (current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function re_com$tabs$iter__31670(s__31671){
return (new cljs.core.LazySeq(null,((function (current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (){
var s__31671__$1 = s__31671;
while(true){
var temp__4657__auto__ = cljs.core.seq.call(null,s__31671__$1);
if(temp__4657__auto__){
var s__31671__$2 = temp__4657__auto__;
if(cljs.core.chunked_seq_QMARK_.call(null,s__31671__$2)){
var c__25908__auto__ = cljs.core.chunk_first.call(null,s__31671__$2);
var size__25909__auto__ = cljs.core.count.call(null,c__25908__auto__);
var b__31673 = cljs.core.chunk_buffer.call(null,size__25909__auto__);
if((function (){var i__31672 = (0);
while(true){
if((i__31672 < size__25909__auto__)){
var t = cljs.core._nth.call(null,c__25908__auto__,i__31672);
cljs.core.chunk_append.call(null,b__31673,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"li","li",723558921),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),((selected_QMARK_)?"active":""),new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join('')], null),new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"a","a",-2123407586),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"style","style",-496642736),cljs.core.merge.call(null,new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"cursor","cursor",1011937484),"pointer"], null),style),new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (i__31672,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31673,s__31671__$2,temp__4657__auto__,current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (event){
on_change.call(null,id);
return null;
});})(i__31672,id,label,selected_QMARK_,t,c__25908__auto__,size__25909__auto__,b__31673,s__31671__$2,temp__4657__auto__,current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
:null)], null),label], null)], null);
})());
var G__31677 = (i__31672 + (1));
i__31672 = G__31677;
continue;
} else {
return true;
}
break;
}
})()){
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31673),re_com$tabs$iter__31670.call(null,cljs.core.chunk_rest.call(null,s__31671__$2)));
} else {
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__31673),null);
}
} else {
var t = cljs.core.first.call(null,s__31671__$2);
return cljs.core.cons.call(null,(function (){var id = id_fn.call(null,t);
var label = label_fn.call(null,t);
var selected_QMARK_ = cljs.core._EQ_.call(null,id,current);
return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"li","li",723558921),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"class","class",-2030961996),((selected_QMARK_)?"active":""),new cljs.core.Keyword(null,"key","key",-1516042587),[cljs.core.str(id)].join('')], null),new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"a","a",-2123407586),new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"style","style",-496642736),cljs.core.merge.call(null,new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"cursor","cursor",1011937484),"pointer"], null),style),new cljs.core.Keyword(null,"on-click","on-click",1632826543),(cljs.core.truth_(on_change)?((function (id,label,selected_QMARK_,t,s__31671__$2,temp__4657__auto__,current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_){
return (function (event){
on_change.call(null,id);
return null;
});})(id,label,selected_QMARK_,t,s__31671__$2,temp__4657__auto__,current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
:null)], null),label], null)], null);
})(),re_com$tabs$iter__31670.call(null,cljs.core.rest.call(null,s__31671__$2)));
}
} else {
return null;
}
break;
}
});})(current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
,null,null));
});})(current,tabs__$1,_,map__31668,map__31668__$1,model,tabs,on_change,id_fn,label_fn,style,vertical_QMARK_))
;
return iter__25910__auto__.call(null,tabs__$1);
})()], null);
});
re_com.tabs.pill_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.pill_tabs.cljs$lang$applyTo = (function (seq31666){
return re_com.tabs.pill_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31666));
});
re_com.tabs.horizontal_pill_tabs = (function re_com$tabs$horizontal_pill_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31682 = arguments.length;
var i__26206__auto___31683 = (0);
while(true){
if((i__26206__auto___31683 < len__26205__auto___31682)){
args__26212__auto__.push((arguments[i__26206__auto___31683]));
var G__31684 = (i__26206__auto___31683 + (1));
i__26206__auto___31683 = G__31684;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.horizontal_pill_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.horizontal_pill_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31679){
var map__31680 = p__31679;
var map__31680__$1 = ((((!((map__31680 == null)))?((((map__31680.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31680.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31680):map__31680);
var args = map__31680__$1;
var model = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var style = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"style","style",-496642736));
var label_fn = cljs.core.get.call(null,map__31680__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"label","label",1718410804));
if(cljs.core.truth_(((!(goog.DEBUG))?true:re_com.validate.validate_args.call(null,re_com.validate.extract_arg_data.call(null,re_com.tabs.tabs_args_desc),args,"tabs")))){
} else {
throw (new Error("Assert failed: (validate-args-macro tabs-args-desc args \"tabs\")"));
}
return re_com.tabs.pill_tabs.call(null,new cljs.core.Keyword(null,"model","model",331153215),model,new cljs.core.Keyword(null,"tabs","tabs",-779855354),tabs,new cljs.core.Keyword(null,"on-change","on-change",-732046149),on_change,new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),id_fn,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),label_fn,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444),false);
});
re_com.tabs.horizontal_pill_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.horizontal_pill_tabs.cljs$lang$applyTo = (function (seq31678){
return re_com.tabs.horizontal_pill_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31678));
});
re_com.tabs.vertical_pill_tabs = (function re_com$tabs$vertical_pill_tabs(var_args){
var args__26212__auto__ = [];
var len__26205__auto___31689 = arguments.length;
var i__26206__auto___31690 = (0);
while(true){
if((i__26206__auto___31690 < len__26205__auto___31689)){
args__26212__auto__.push((arguments[i__26206__auto___31690]));
var G__31691 = (i__26206__auto___31690 + (1));
i__26206__auto___31690 = G__31691;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.tabs.vertical_pill_tabs.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.tabs.vertical_pill_tabs.cljs$core$IFn$_invoke$arity$variadic = (function (p__31686){
var map__31687 = p__31686;
var map__31687__$1 = ((((!((map__31687 == null)))?((((map__31687.cljs$lang$protocol_mask$partition0$ & (64))) || (map__31687.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__31687):map__31687);
var args = map__31687__$1;
var model = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"model","model",331153215));
var tabs = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"tabs","tabs",-779855354));
var on_change = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"on-change","on-change",-732046149));
var id_fn = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var style = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"style","style",-496642736));
var label_fn = cljs.core.get.call(null,map__31687__$1,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),new cljs.core.Keyword(null,"label","label",1718410804));
if(cljs.core.truth_(((!(goog.DEBUG))?true:re_com.validate.validate_args.call(null,re_com.validate.extract_arg_data.call(null,re_com.tabs.tabs_args_desc),args,"tabs")))){
} else {
throw (new Error("Assert failed: (validate-args-macro tabs-args-desc args \"tabs\")"));
}
return re_com.tabs.pill_tabs.call(null,new cljs.core.Keyword(null,"model","model",331153215),model,new cljs.core.Keyword(null,"tabs","tabs",-779855354),tabs,new cljs.core.Keyword(null,"on-change","on-change",-732046149),on_change,new cljs.core.Keyword(null,"style","style",-496642736),style,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),id_fn,new cljs.core.Keyword(null,"label-fn","label-fn",-860923263),label_fn,new cljs.core.Keyword(null,"vertical?","vertical?",-1522630444),true);
});
re_com.tabs.vertical_pill_tabs.cljs$lang$maxFixedArity = (0);
re_com.tabs.vertical_pill_tabs.cljs$lang$applyTo = (function (seq31685){
return re_com.tabs.vertical_pill_tabs.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq31685));
});
//# sourceMappingURL=tabs.js.map?rel=1603199194299

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,103 @@
(ns re-com.text
(:require-macros [re-com.core :refer [handler-fn]])
(:require [re-com.box :refer [v-box box line flex-child-style]]
[re-com.util :refer [deep-merge]]
[re-com.validate :refer [title-levels-list title-level-type? css-style?
html-attr? string-or-hiccup?] :refer-macros [validate-args-macro]]))
;; ------------------------------------------------------------------------------------
;; Component: label
;; ------------------------------------------------------------------------------------
(def label-args-desc
[{:name :label :required true :type "anything" :description "text or hiccup or whatever to display"}
{:name :on-click :required false :type "-> nil" :validate-fn fn? :description "a function which takes no params and returns nothing. Called when the label is clicked"}
{:name :width :required false :type "string" :validate-fn string? :description "a CSS width"}
{: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 "additional CSS styles"}
{: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 label
"Returns markup for a basic label"
[& {:keys [label on-click width class style attr]
:as args}]
{:pre [(validate-args-macro label-args-desc args "label")]}
[box
:width width
:align :start
:class "display-inline-flex"
:child [:span
(merge
{:class (str "rc-label " class)
:style (merge (flex-child-style "none")
style)}
(when on-click
{:on-click (handler-fn (on-click))})
attr)
label]])
;; ------------------------------------------------------------------------------------
;; Component: title
;; ------------------------------------------------------------------------------------
(def title-args-desc
[{:name :label :required true :type "anything" :description "title or hiccup or anything to display"}
{:name :level :required false :type "keyword" :validate-fn title-level-type? :description [:span "one of " title-levels-list ". If not provided then style the title using " [:code ":class"] " or " [:code ":style"]] }
{:name :underline? :required false :default false :type "boolean" :description "if true, the title is underlined"}
{:name :margin-top :required false :default "0.4em" :type "string" :validate-fn string? :description "CSS size for space above the title"}
{:name :margin-bottom :required false :default "0.1em" :type "string" :validate-fn string? :description "CSS size for space below the title"}
{: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 title
"A title with four preset levels"
[& {:keys [label level underline? margin-top margin-bottom class style attr]
:or {margin-top "0.6em" margin-bottom "0.3em"}
:as args}]
{:pre [(validate-args-macro title-args-desc args "title")]}
(let [preset-class (if (nil? level) "" (name level))]
[v-box
:class preset-class
:children [[:span (merge {:class (str "rc-title display-flex " preset-class " " class)
:style (merge (flex-child-style "none")
{:margin-top margin-top}
{:line-height 1} ;; so that the margins are correct
(when-not underline? {:margin-bottom margin-bottom})
style)}
attr)
label]
(when underline? [line
:size "1px"
:style {:margin-bottom margin-bottom}])]]))
(defn p
"acts like [:p ]
Creates a paragraph of body text, expected to have a font-szie of 14px or 15px,
which should have limited width.
Why limited text width? See http://baymard.com/blog/line-length-readability
The actualy font-size is inherited.
At 14px, 450px will yield between 69 and 73 chars.
At 15px, 450px will yield about 66 to 70 chars.
So we're at the upper end of the prefered 50 to 75 char range.
If the first child is a map, it is interpreted as a map of styles / attributes."
[& children]
(let [child1 (first children) ;; it might be a map of attributes, including styles
[m children] (if (map? child1)
[child1 (rest children)]
[{} children])
m (deep-merge {:style {:flex "none"
:width "450px"
:min-width "450px"}}
m)]
[:span m (into [:p] children)])) ;; the wrapping span allows children to contain [:ul] etc

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,95 @@
(ns re-com.tour
(:require-macros [re-com.core :refer [handler-fn]])
(:require [reagent.core :as reagent]
[re-com.box :refer [flex-child-style]]
[re-com.buttons :refer [button]]))
;;--------------------------------------------------------------------------------------------------
;; Component: tour
;;
;; Strings together
;;--------------------------------------------------------------------------------------------------
(defn make-tour
"Returns a map containing
- A reagent atom for each tour step controlling popover show/hide (boolean)
- A standard atom holding the current step (integer)
- A copy of the steps parameter passed in, to determine the order for prev/next functions
It sets the first step atom to true so that it will be initially shown
Sample return value:
{:steps [:step1 :step2 :step3]
:current-step (atom 0)
:step1 (reagent/atom true)
:step2 (reagent/atom false)
:step3 (reagent/atom false)}"
[tour-spec]
(let [tour-map {:current-step (atom 0) :steps tour-spec}] ;; Only need normal atom
(reduce #(assoc %1 %2 (reagent/atom false)) tour-map tour-spec))) ;; Old way: (merge {} (map #(hash-map % (reagent/atom false)) tour-spec))
(defn- initialise-tour
"Resets all poover atoms to false"
[tour]
(doall (for [step (:steps tour)] (reset! (step tour) false))))
(defn start-tour
"Sets the first popover atom in the tour to true"
[tour]
(initialise-tour tour)
(reset! (:current-step tour) 0)
(reset! ((first (:steps tour)) tour) true))
(defn finish-tour
"Closes all tour popovers"
[tour]
(initialise-tour tour))
(defn- next-tour-step
[tour]
(let [steps (:steps tour)
old-step @(:current-step tour)
new-step (inc old-step)]
(when (< new-step (count (:steps tour)))
(reset! (:current-step tour) new-step)
(reset! ((nth steps old-step) tour) false)
(reset! ((nth steps new-step) tour) true))))
(defn- prev-tour-step
[tour]
(let [steps (:steps tour)
old-step @(:current-step tour)
new-step (dec old-step)]
(when (>= new-step 0)
(reset! (:current-step tour) new-step)
(reset! ((nth steps old-step) tour) false)
(reset! ((nth steps new-step) tour) true))))
(defn make-tour-nav
"Generate the hr and previous/next buttons markup.
If first button in tour, don't generate a Previous button.
If last button in tour, change Next button to a Finish button"
[tour]
(let [on-first-button (= @(:current-step tour) 0)
on-last-button (= @(:current-step tour) (dec (count (:steps tour))))]
[:div
[:hr {:style (merge (flex-child-style "none")
{:margin "10px 0px 10px"})}]
(when-not on-first-button
[button
:label "Previous"
:on-click (handler-fn (prev-tour-step tour))
:style {:margin-right "15px"}
:class "btn-default"])
[button
:label (if on-last-button "Finish" "Next")
:on-click (handler-fn (if on-last-button
(finish-tour tour)
(next-tour-step tour)))
:class "btn-default"]]))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,147 @@
// Compiled by ClojureScript 1.9.229 {}
goog.provide('re_com.tour');
goog.require('cljs.core');
goog.require('reagent.core');
goog.require('re_com.box');
goog.require('re_com.buttons');
/**
* Returns a map containing
* - A reagent atom for each tour step controlling popover show/hide (boolean)
* - A standard atom holding the current step (integer)
* - A copy of the steps parameter passed in, to determine the order for prev/next functions
* It sets the first step atom to true so that it will be initially shown
* Sample return value:
* {:steps [:step1 :step2 :step3]
* :current-step (atom 0)
* :step1 (reagent/atom true)
* :step2 (reagent/atom false)
* :step3 (reagent/atom false)}
*/
re_com.tour.make_tour = (function re_com$tour$make_tour(tour_spec){
var tour_map = new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"current-step","current-step",-2023410137),cljs.core.atom.call(null,(0)),new cljs.core.Keyword(null,"steps","steps",-128433302),tour_spec], null);
return cljs.core.reduce.call(null,((function (tour_map){
return (function (p1__32433_SHARP_,p2__32434_SHARP_){
return cljs.core.assoc.call(null,p1__32433_SHARP_,p2__32434_SHARP_,reagent.core.atom.call(null,false));
});})(tour_map))
,tour_map,tour_spec);
});
/**
* Resets all poover atoms to false
*/
re_com.tour.initialise_tour = (function re_com$tour$initialise_tour(tour){
return cljs.core.doall.call(null,(function (){var iter__25910__auto__ = (function re_com$tour$initialise_tour_$_iter__32439(s__32440){
return (new cljs.core.LazySeq(null,(function (){
var s__32440__$1 = s__32440;
while(true){
var temp__4657__auto__ = cljs.core.seq.call(null,s__32440__$1);
if(temp__4657__auto__){
var s__32440__$2 = temp__4657__auto__;
if(cljs.core.chunked_seq_QMARK_.call(null,s__32440__$2)){
var c__25908__auto__ = cljs.core.chunk_first.call(null,s__32440__$2);
var size__25909__auto__ = cljs.core.count.call(null,c__25908__auto__);
var b__32442 = cljs.core.chunk_buffer.call(null,size__25909__auto__);
if((function (){var i__32441 = (0);
while(true){
if((i__32441 < size__25909__auto__)){
var step = cljs.core._nth.call(null,c__25908__auto__,i__32441);
cljs.core.chunk_append.call(null,b__32442,cljs.core.reset_BANG_.call(null,step.call(null,tour),false));
var G__32443 = (i__32441 + (1));
i__32441 = G__32443;
continue;
} else {
return true;
}
break;
}
})()){
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__32442),re_com$tour$initialise_tour_$_iter__32439.call(null,cljs.core.chunk_rest.call(null,s__32440__$2)));
} else {
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__32442),null);
}
} else {
var step = cljs.core.first.call(null,s__32440__$2);
return cljs.core.cons.call(null,cljs.core.reset_BANG_.call(null,step.call(null,tour),false),re_com$tour$initialise_tour_$_iter__32439.call(null,cljs.core.rest.call(null,s__32440__$2)));
}
} else {
return null;
}
break;
}
}),null,null));
});
return iter__25910__auto__.call(null,new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour));
})());
});
/**
* Sets the first popover atom in the tour to true
*/
re_com.tour.start_tour = (function re_com$tour$start_tour(tour){
re_com.tour.initialise_tour.call(null,tour);
cljs.core.reset_BANG_.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour),(0));
return cljs.core.reset_BANG_.call(null,cljs.core.first.call(null,new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour)).call(null,tour),true);
});
/**
* Closes all tour popovers
*/
re_com.tour.finish_tour = (function re_com$tour$finish_tour(tour){
return re_com.tour.initialise_tour.call(null,tour);
});
re_com.tour.next_tour_step = (function re_com$tour$next_tour_step(tour){
var steps = new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour);
var old_step = cljs.core.deref.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour));
var new_step = (old_step + (1));
if((new_step < cljs.core.count.call(null,new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour)))){
cljs.core.reset_BANG_.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour),new_step);
cljs.core.reset_BANG_.call(null,cljs.core.nth.call(null,steps,old_step).call(null,tour),false);
return cljs.core.reset_BANG_.call(null,cljs.core.nth.call(null,steps,new_step).call(null,tour),true);
} else {
return null;
}
});
re_com.tour.prev_tour_step = (function re_com$tour$prev_tour_step(tour){
var steps = new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour);
var old_step = cljs.core.deref.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour));
var new_step = (old_step - (1));
if((new_step >= (0))){
cljs.core.reset_BANG_.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour),new_step);
cljs.core.reset_BANG_.call(null,cljs.core.nth.call(null,steps,old_step).call(null,tour),false);
return cljs.core.reset_BANG_.call(null,cljs.core.nth.call(null,steps,new_step).call(null,tour),true);
} else {
return null;
}
});
/**
* Generate the hr and previous/next buttons markup.
* If first button in tour, don't generate a Previous button.
* If last button in tour, change Next button to a Finish button
*/
re_com.tour.make_tour_nav = (function re_com$tour$make_tour_nav(tour){
var on_first_button = cljs.core._EQ_.call(null,cljs.core.deref.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour)),(0));
var on_last_button = cljs.core._EQ_.call(null,cljs.core.deref.call(null,new cljs.core.Keyword(null,"current-step","current-step",-2023410137).cljs$core$IFn$_invoke$arity$1(tour)),(cljs.core.count.call(null,new cljs.core.Keyword(null,"steps","steps",-128433302).cljs$core$IFn$_invoke$arity$1(tour)) - (1)));
return new cljs.core.PersistentVector(null, 4, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"div","div",1057191632),new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"hr","hr",1377740067),new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"style","style",-496642736),cljs.core.merge.call(null,re_com.box.flex_child_style.call(null,"none"),new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"margin","margin",-995903681),"10px 0px 10px"], null))], null)], null),((on_first_button)?null:new cljs.core.PersistentVector(null, 9, 5, cljs.core.PersistentVector.EMPTY_NODE, [re_com.buttons.button,new cljs.core.Keyword(null,"label","label",1718410804),"Previous",new cljs.core.Keyword(null,"on-click","on-click",1632826543),((function (on_first_button,on_last_button){
return (function (event){
re_com.tour.prev_tour_step.call(null,tour);
return null;
});})(on_first_button,on_last_button))
,new cljs.core.Keyword(null,"style","style",-496642736),new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"margin-right","margin-right",809689658),"15px"], null),new cljs.core.Keyword(null,"class","class",-2030961996),"btn-default"], null)),new cljs.core.PersistentVector(null, 7, 5, cljs.core.PersistentVector.EMPTY_NODE, [re_com.buttons.button,new cljs.core.Keyword(null,"label","label",1718410804),((on_last_button)?"Finish":"Next"),new cljs.core.Keyword(null,"on-click","on-click",1632826543),((function (on_first_button,on_last_button){
return (function (event){
if(on_last_button){
re_com.tour.finish_tour.call(null,tour);
} else {
re_com.tour.next_tour_step.call(null,tour);
}
return null;
});})(on_first_button,on_last_button))
,new cljs.core.Keyword(null,"class","class",-2030961996),"btn-default"], null)], null);
});
//# sourceMappingURL=tour.js.map?rel=1603199196399

View file

@ -0,0 +1 @@
{"version":3,"file":"\/Users\/simon\/workspace\/swinging-needle-meter\/docs\/js\/compiled\/out\/re_com\/tour.js","sources":["tour.cljs?rel=1603199196400"],"lineCount":147,"mappings":";AAAA;;;;;AAaA;;;;;;;;;;;;;wBAAA,xBAAMA,wDAYHC;AAZH,AAaE,eAAA,2CAAA,oGAAA,1JAAMC,4HAAwB,yBAAA,zBAACC,qFAAeF;AAA9C,AAEE,kCAAA,3BAACK;kBAADF,iBAAAC;AAAA,AAAS,iCAAAD,iBAAAC,3CAACE,4DAAY,4BAAA,5BAACC;;CAAqBN,SAASD;;AAGzD;;;8BAAA,9BAAOQ,oEAEJC;AAFH,AAGE,OAACC,0BAAM,iBAAAC,sBAAA,oDAAAC;AAAA,AAAA,YAAAC,kBAAA,KAAA;AAAA,AAAA,IAAAD,eAAAA;;AAAA,AAAA,IAAAE,qBAAA,AAAAC,wBAAAH;AAAA,AAAA,GAAAE;AAAA,AAAA,IAAAF,eAAAE;AAAA,AAAA,GAAA,AAAAE,uCAAAJ;AAAA,IAAAK,mBAuuE0C,AAAAkC,gCAAAvC;IAvuE1CM,sBAAA,AAAAC,0BAAAF;IAAAG,WAAA,AAAAC,iCAAAH;AAAA,AAAA,GAAA,AAAA,iBAAAI,WAAA;;AAAA,AAAA,GAAA,CAAAA,WAAAJ;AAAA,WAAA,AAAAK,yBAAAN,iBAAAK,jDAAMU;AAAN,AAAA,AAAA,AAAAR,iCAAAJ,SAA0B,qDAAA,rDAACa,gCAAO,AAACD,eAAKvB;;AAAxC,eAAA,CAAAa,WAAA;;;;AAAA;;;;;AAAA,OAAAG,+BAAA,AAAAC,0BAAAN,UAAA,AAAAO,oDAAA,AAAAC,+BAAAhB;;AAAA,OAAAa,+BAAA,AAAAC,0BAAAN,UAAA;;;AAAA,WAAA,AAAAS,0BAAAjB,jCAAMoB;AAAN,AAAA,OAAAF,qFAAA,AAAAH,oDAAA,AAAAI,yBAAAnB,zIAA0B,qDAAA,rDAACqB,gCAAO,AAACD,eAAKvB;;;AAAxC;;;;GAAA,KAAA;;AAAA,AAAA,OAAAE,8BAAW,AAAA,qFAAQF;;;AAG5B;;;yBAAA,zBAAMyB,0DAEHzB;AAFH,AAGE,AAACD,sCAAgBC;;AACjB,0IAAA,1IAACwB,gCAAO,AAAA,oGAAexB;;AACvB,6KAAA,tKAACwB,gCAAO,AAAC,AAACJ,0BAAM,AAAA,qFAAQpB,iBAAOA;;AAGjC;;;0BAAA,1BAAM0B,4DAEH1B;AAFH,AAGE,OAACD,sCAAgBC;;AAGnB,6BAAA,7BAAO2B,kEACJ3B;AADH,AAEE,IAAM6B,QAAU,AAAA,qFAAQ7B;eAAxB,AAAA4B,XACME,qCAAW,AAAA,oGAAe9B;IAC1B+B,WAAU,YAAA,XAAKD;AAFrB,AAGE,GAAM,CAAGC,WAAS,AAACrB,0BAAM,AAAA,qFAAQV;AAAjC,AACE,AAACwB,gCAAO,AAAA,oGAAexB,MAAM+B;;AAC7B,wFAAA,xFAACP,gCAAO,AAAC,AAACQ,wBAAIH,MAAMC,oBAAU9B;;AAC9B,+FAAA,xFAACwB,gCAAO,AAAC,AAACQ,wBAAIH,MAAME,oBAAU\/B;;AAHhC;;;AAMJ,6BAAA,7BAAOiC,kEACJjC;AADH,AAEE,IAAM6B,QAAS,AAAA,qFAAQ7B;eAAvB,AAAA4B,XACME,qCAAU,AAAA,oGAAe9B;IACzB+B,WAAS,YAAA,XAAKD;AAFpB,AAGE,GAAM,aAAA,ZAAIC;AAAV,AACE,AAACP,gCAAO,AAAA,oGAAexB,MAAM+B;;AAC7B,wFAAA,xFAACP,gCAAO,AAAC,AAACQ,wBAAIH,MAAMC,oBAAU9B;;AAC9B,+FAAA,xFAACwB,gCAAO,AAAC,AAACQ,wBAAIH,MAAME,oBAAU\/B;;AAHhC;;;AAMJ;;;;;4BAAA,5BAAMkC,gEAIHlC;AAJH,AAKE,IAAMmC,kBAAgB,yBAAA,AAAAP,qIAAA,9JAACQ,mDAAG,AAAA,oGAAepC;IACnCqC,iBAAgB,yBAAA,AAAAT,zBAACQ,mDAAG,AAAA,oGAAepC,OAAM,yHAAA,xHAAK,AAACU,0BAAM,AAAA,qFAAQV;AADnE,AAAA,0FAAA,mDAAA,mFAAA,iDAAA,2CAAA,uDAGgB,wEAAA,2CAAA,yDAAA,5KAACsC,0BAAM,sCAAA,tCAACC,2LAEpB,mBAAA,KAAA,AAAA,yGAAA,uDAAA,WAAA,jMAAUJ,yGACPK,qJAEU;kBAAAC;AAAA,AAAY,AAACR,qCAAejC;;AAA5B;;qQARjB,yGAAA,0FAAA,vcAKI,uDAAA,2CAAA,oEAAA,eAAA,wDAAA,0GAMCwC,6EACU,kBAAA,SAAA,zBAAIH,8FACJ;kBAAAI;AAAA,AAAY,GAAIJ;AACF,AAACX,kCAAY1B;;AACb,AAAC2B,qCAAe3B;;;AAF9B;;CAbf,wDAAA","names":["re-com.tour\/make-tour","tour-spec","tour-map","cljs.core\/atom","p1__32433#","p2__32434#","cljs.core\/reduce","cljs.core\/assoc","reagent.core\/atom","re-com.tour\/initialise-tour","tour","cljs.core\/doall","iter__25910__auto__","s__32440","cljs.core\/LazySeq","temp__4657__auto__","cljs.core\/seq","cljs.core\/chunked-seq?","c__25908__auto__","size__25909__auto__","cljs.core\/count","b__32442","cljs.core\/chunk-buffer","i__32441","cljs.core\/-nth","cljs.core\/chunk-append","cljs.core\/chunk-cons","cljs.core\/chunk","iter__32439","cljs.core\/chunk-rest","cljs.core\/first","cljs.core\/cons","cljs.core\/rest","step","cljs.core\/reset!","re-com.tour\/start-tour","re-com.tour\/finish-tour","re-com.tour\/next-tour-step","cljs.core\/deref","steps","old-step","new-step","cljs.core\/nth","re-com.tour\/prev-tour-step","re-com.tour\/make-tour-nav","on-first-button","cljs.core\/=","on-last-button","cljs.core\/merge","re-com.box\/flex-child-style","re-com.buttons\/button","event","cljs.core\/chunk-first"]}

View file

@ -0,0 +1,313 @@
(ns re-com.typeahead
(:require-macros [re-com.core :refer [handler-fn]]
[cljs.core.async.macros :refer [alt! go-loop]])
(:require [cljs.core.async :refer [chan timeout <! put!]]
[re-com.misc :refer [throbber input-text]]
[re-com.util :refer [deref-or-value px]]
[re-com.popover :refer [popover-tooltip]] ;; need?
[re-com.box :refer [h-box v-box box gap line flex-child-style align-style]] ;; need?
[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]
[goog.events.KeyCodes]))
;; ------------------------------------------------------------------------------------
;; Component: typeahead
;; ------------------------------------------------------------------------------------
(def typeahead-args-desc
[{:name :data-source :required true :type "fn" :validate-fn fn? :description [:span [:code ":data-source"] " supplies suggestion objects. This can either accept a single string argument (the search term), or a string and a callback. For the first case, the fn should return a collection of suggestion objects (which can be anything). For the second case, the fn should return "[:code "nil" ]", and eventually result in a call to the callback with a collection of suggestion objects."]}
{:name :on-change :required false :default nil :type "string -> nil" :validate-fn fn? :description [:span [:code ":change-on-blur?"] " controls when it is called. It is passed a suggestion object."] }
{:name :change-on-blur? :required false :default true :type "boolean | atom" :description [:span "when true, invoke " [:code ":on-change"] " when the use chooses a suggestion, otherwise invoke it on every change (navigating through suggestions with the mouse or keyboard, or if "[:code "rigid?"]" is also "[:code "false" ]", invoke it on every character typed.)"] }
{:name :model :required false :default nil :type "object | atom" :description "The initial value of the typeahead (should match the suggestion objects returned by " [:code ":data-source"] ")."}
{:name :debounce-delay :required false :default 250 :type "integer" :validate-fn integer? :description [:span "After receiving input, the typeahead will wait this many milliseconds without receiving new input before calling " [:code ":data-source"] "."]}
{:name :render-suggestion :required false :type "render fn" :validate-fn fn? :description "override the rendering of the suggestion items by passing a fn that returns hiccup forms. The fn will receive two arguments: the search term, and the suggestion object."}
{:name :suggestion-to-string :required false :type "suggestion -> string" :validate-fn fn? :description "When a suggestion is chosen, the input-text value will be set to the result of calling this fn with the suggestion object."}
{:name :rigid? :required false :default true :type "boolean | atom" :description [:span "If "[:code "false"]" the user will be allowed to choose arbitrary text input rather than a suggestion from " [:code ":data-source"]". In this case, a string will be supplied in lieu of a suggestion object." ]}
;; the rest of the arguments are forwarded to the wrapped `input-text`
{: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 :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"]}])
;; TODO
;; ability to focus & blur the input-text would be nice... this is also missing from input-text
;; the typeahead should blur the input-text after a selection is chosen
(declare debounce display-suggestion)
(defn- make-typeahead-state
"Return an initial value for the typeahead state, given `args`."
[{:as args :keys [on-change rigid? change-on-blur? data-source suggestion-to-string debounce-delay model]}]
(let [external-model-value (deref-or-value model)]
(cond-> (let [c-input (chan)]
{:input-text ""
:external-model (deref-or-value model)
:model (deref-or-value model)
:waiting? false
:suggestions []
:displaying-suggestion? false
:suggestion-to-string (or suggestion-to-string str)
:data-source data-source
:change-on-blur? change-on-blur?
:on-change on-change
:rigid? rigid?
:c-input c-input
:c-search (debounce c-input debounce-delay)})
external-model-value
(display-suggestion external-model-value))))
;; ------------------------------------------------------------------------------------
;; State predicates: state -> value? -> boolean
;; ------------------------------------------------------------------------------------
(defn- event-updates-model?
"Should `event` update the `typeahead` `model`?"
[{:as state :keys [change-on-blur? rigid?]} event]
(let [change-on-blur? (deref-or-value change-on-blur?)
rigid? (deref-or-value rigid?)]
(case event
:input-text-blurred (and change-on-blur? (not rigid?))
:suggestion-activated (not change-on-blur?)
:input-text-changed (not (or change-on-blur? rigid?)))))
(defn- event-displays-suggestion?
"Should `event` cause the `input-text` value to be used to show the active suggestion?"
[{:as state :keys [change-on-blur?]} event]
(let [change-on-blur? (deref-or-value change-on-blur?)]
(case event
:suggestion-activated (not change-on-blur?)
false)))
;; ------------------------------------------------------------------------------------
;; State update helpers: state -> value? -> next-state
;; all pure, _except_ that they may call `on-change`
;; ------------------------------------------------------------------------------------
(defn- update-model
"Change the `typeahead` `model` value to `new-value`"
[{:as state :keys [on-change]} new-value]
(when on-change (on-change new-value))
(assoc state :model new-value))
(defn- display-suggestion
"Change the `input-text` `model` to the string representation of `suggestion`"
[{:as state :keys [suggestion-to-string]} suggestion]
(let [suggestion-string (suggestion-to-string suggestion)]
(cond-> state
suggestion-string (assoc :input-text suggestion-string
:displaying-suggestion? true))))
(defn- clear-suggestions
[state]
(-> state
(dissoc :suggestions :suggestion-active-index)))
(defn- activate-suggestion-by-index
"Make the suggestion at `index` the active suggestion"
[{:as state :keys [suggestions]} index]
(let [suggestion (nth suggestions index)]
(cond-> state
:always (assoc :suggestion-active-index index)
(event-updates-model? state :suggestion-activated) (update-model suggestion)
(event-displays-suggestion? state :suggestion-activated) (display-suggestion suggestion))))
(defn- choose-suggestion-by-index
"Choose the suggestion at `index`"
[{:as state :keys [suggestions]} index]
(let [suggestion (nth suggestions index)]
(-> state
(activate-suggestion-by-index index)
(update-model suggestion)
(display-suggestion suggestion)
clear-suggestions)))
(defn- choose-suggestion-active
[{:as state :keys [suggestion-active-index]}]
(cond-> state
suggestion-active-index (choose-suggestion-by-index suggestion-active-index)))
(defn- wrap [index count] (mod (+ count index) count))
(defn- activate-suggestion-next
[{:as state :keys [suggestions suggestion-active-index]}]
(cond-> state
suggestions
(activate-suggestion-by-index (-> suggestion-active-index (or -1) inc (wrap (count suggestions))))))
(defn- activate-suggestion-prev
[{:as state :keys [suggestions suggestion-active-index]}]
(cond-> state
suggestions
(activate-suggestion-by-index (-> suggestion-active-index (or 0) dec (wrap (count suggestions))))))
(defn- reset-typeahead
[state]
(cond-> state
:always clear-suggestions
:always (assoc :waiting? false :input-text "" :displaying-suggestion? false)
(event-updates-model? state :input-text-changed) (update-model nil)))
(defn- got-suggestions
"Update state when new suggestions are available"
[state suggestions]
(-> state
(assoc :suggestions suggestions
:waiting? false
:suggestion-active-index nil)))
(defn- input-text-will-blur
"Update state when the `input-text` is about to lose focus."
[{:keys [input-text displaying-suggestion?] :as state}]
(cond-> state
(and (not displaying-suggestion?)
(event-updates-model? state :input-text-blurred))
(update-model input-text)))
(defn- change-data-source
"Update `state` given a new `data-source`. Resets the typeahead since any existing suggestions
came from the old `data-source`."
[state data-source]
(-> state
reset-typeahead
(assoc :data-source data-source)))
(defn- external-model-changed
"Update state when the external model value has changed."
[state new-value]
(-> state
(update-model new-value)
(display-suggestion new-value)
clear-suggestions))
;; ------------------------------------------------------------------------------------
;; Functions with side-effects
;; ------------------------------------------------------------------------------------
(defn- search-data-source!
"Call the `data-source` fn with `text`, and then call `got-suggestions` with the result
(asynchronously, if `data-source` does not return a truthy value)."
[data-source state-atom text]
(if-let [return-value (data-source text #(swap! state-atom got-suggestions %1))]
(swap! state-atom got-suggestions return-value)
(swap! state-atom assoc :waiting? true)))
(defn- search-data-source-loop!
"For every value arriving on the `c-search` channel, call `search-data-source!`."
[state-atom c-search]
(go-loop []
(let [new-text (<! c-search)
data-source (:data-source @state-atom)]
(if (= "" new-text)
(swap! state-atom reset-typeahead)
(search-data-source! data-source state-atom new-text))
(recur))))
(defn- input-text-on-change!
"Update state in response to `input-text` `on-change`, and put text on the `c-input` channel"
[state-atom new-text]
(let [{:as state :keys [input-text c-input]} @state-atom]
(if (= new-text input-text) state ;; keypresses that do not change the value still call on-change, ignore these
(do
(when-not (clojure.string/blank? new-text) (put! c-input new-text))
(swap! state-atom
#(cond-> %
:always (assoc :input-text new-text :displaying-suggestion? false)
(event-updates-model? state :input-text-changed) (update-model new-text)))))))
(defn- input-text-on-key-down!
[state-atom event]
(condp = (.-which event)
goog.events.KeyCodes.UP (swap! state-atom activate-suggestion-prev)
goog.events.KeyCodes.DOWN (swap! state-atom activate-suggestion-next)
goog.events.KeyCodes.ENTER (swap! state-atom choose-suggestion-active)
goog.events.KeyCodes.ESC (swap! state-atom reset-typeahead)
;; tab requires special treatment
;; trap it IFF there are suggestions, otherwise let the input defocus
goog.events.KeyCodes.TAB
(if (not-empty (:suggestions @state-atom))
(do (swap! state-atom activate-suggestion-next)
(.preventDefault event))
(swap! state-atom input-text-will-blur))
true))
;; ------------------------------------------------------------------------------------
;; The typeahead component
;; ------------------------------------------------------------------------------------
(defn- typeahead
"typeahead reagent component"
[& {:keys [data-source on-change rigid? change-on-blur?] :as args}]
{:pre [(validate-args-macro typeahead-args-desc args "typeahead")]}
(let [{:as state :keys [c-search c-input]} (make-typeahead-state args)
state-atom (reagent/atom state)
input-text-model (reagent/cursor state-atom [:input-text])]
(search-data-source-loop! state-atom c-search)
(fn
[& {:as args
:keys [data-source rigid? render-suggestion suggestion-to-string model
;; forwarded to wrapped `input-text`:
placeholder width height status-icon? status status-tooltip disabled? class style]}]
{:pre [(validate-args-macro typeahead-args-desc args "typeahead")]}
(let [{:as state :keys [suggestions waiting? suggestion-active-index external-model]} @state-atom
last-data-source (:data-source state)
latest-external-model (deref-or-value model)
width (or width "250px")]
(when (not= last-data-source data-source)
(swap! state-atom change-data-source data-source))
(when (not= latest-external-model external-model)
(swap! state-atom external-model-changed latest-external-model))
[v-box
:width width
:children
[[input-text
:model input-text-model
:class class
:style style
:disabled? disabled?
:status-icon? status-icon?
:status status
:status-tooltip status-tooltip
:width width
:height height
:placeholder placeholder
:on-change (partial input-text-on-change! state-atom)
:change-on-blur? false
:attr {:on-key-down (partial input-text-on-key-down! state-atom)}]
(if (or (not-empty suggestions) waiting?)
[v-box
:class "rc-typeahead-suggestions-container"
:children [(when waiting?
[box :align :center :child [throbber :size :small :class "rc-typeahead-throbber"]])
(for [[ i s ] (map vector (range) suggestions)
:let [selected? (= suggestion-active-index i)]]
^{:key i}
[box
:child (if render-suggestion
(render-suggestion s)
s)
:class (str "rc-typeahead-suggestion"
(when selected? " active"))
:attr {:on-mouse-over #(swap! state-atom activate-suggestion-by-index i)
:on-mouse-down #(do (.preventDefault %) (swap! state-atom choose-suggestion-by-index i))}])]])]]))))
(defn- debounce
"Return a channel which will receive a value from the `in` channel only
if no further value is received on the `in` channel in the next `ms` milliseconds."
[in ms]
(let [out (chan)]
(go-loop [last-val nil]
(let [val (if (nil? last-val) (<! in) last-val)
timer (timeout ms)]
(let [v (alt!
in ([val _] val)
timer (do (>! out val) nil))]
(recur v))))
out))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,165 @@
(ns re-com.util
(:require
[clojure.set :refer [superset?]]
[reagent.ratom :refer [RAtom Reaction RCursor Track Wrapper]]
[goog.date.DateTime]
[goog.date.UtcDateTime]))
(defn fmap
"Takes a function 'f' amd a map 'm'. Applies 'f' to each value in 'm' and returns.
(fmap inc {:a 4 :b 2}) => {:a 5 :b 3}"
[f m]
(into {} (for [[k val] m] [k (f val)])))
(defn deep-merge
"Recursively merges maps. If vals are not maps, the last value wins."
[& vals]
(if (every? map? vals)
(apply merge-with deep-merge vals)
(last vals)))
(defn deref-or-value
"Takes a value or an atom
If it's a value, returns it
If it's a Reagent object that supports IDeref, returns the value inside it by derefing
"
[val-or-atom]
(if (satisfies? IDeref val-or-atom)
@val-or-atom
val-or-atom))
(defn deref-or-value-peek
"Takes a value or an atom
If it's a value, returns it
If it's a Reagent object that supports IDeref, returns the value inside it, but WITHOUT derefing
The arg validation code uses this, since calling deref-or-value adds this arg to the watched ratom list for the component
in question, which in turn can cause different rendering behaviour between dev (where we validate) and prod (where we don't).
This was experienced in popover-content-wrapper with the position-injected atom which was not derefed there, however
the dev-only validation caused it to be derefed, modifying its render behaviour and causing mayhem and madness for the developer.
See below that different Reagent types have different ways of retrieving the value without causing capture, although in the case of
Track, we just deref it as there is no peek or state, so hopefully this won't cause issues (surely this is used very rarely).
"
[val-or-atom]
(if (satisfies? IDeref val-or-atom)
(cond
(instance? RAtom val-or-atom) val-or-atom.state
(instance? Reaction val-or-atom) (._peek-at val-or-atom)
(instance? RCursor val-or-atom) (._peek val-or-atom)
(instance? Track val-or-atom) @val-or-atom
(instance? Wrapper val-or-atom) val-or-atom.state
:else (throw (js/Error. "Unknown reactive data type")))
val-or-atom))
(defn get-element-by-id
[id]
(.getElementById js/document id))
(defn pad-zero
"Left pad a string 's' with '0', until 's' has length 'len'. Return 's' unchanged, if it is already len or greater"
[s len]
(if (< (count s) len)
(apply str (take-last len (concat (repeat len \0) s)))
s))
(defn pad-zero-number
"return 'num' as a string of 'len' characters, left padding with '0' as necessary"
[num len]
(pad-zero (str num) len))
(defn px
"takes a number (and optional :negative keyword to indicate a negative value) and returns that number as a string with 'px' at the end"
[val & negative]
(str (if negative (- val) val) "px"))
;; ----------------------------------------------------------------------------
;; Handy vector functions
;; ----------------------------------------------------------------------------
(defn remove-nth
"Removes the item at position n from a vector v, returning a shrunk vector"
[v n]
(vec
(concat
(subvec v 0 n) (subvec v (inc n) (count v)))))
(defn insert-nth
[vect index item]
(apply merge (subvec vect 0 index) item (subvec vect index)))
;; ----------------------------------------------------------------------------
;; Utilities for vectors of maps containing :id
;; ----------------------------------------------------------------------------
(defn position-for-id
"Takes a vector of maps 'v'. Returns the position of the first item in 'v' whose id-fn (default :id) matches 'id'.
Returns nil if id not found"
[id v & {:keys [id-fn] :or {id-fn :id}}]
(let [index-fn (fn [index item] (when (= (id-fn item) id) index))]
(first (keep-indexed index-fn v))))
(defn item-for-id
"Takes a vector of maps 'v'. Returns the first item in 'v' whose id-fn (default :id) matches 'id'.
Returns nil if id not found"
[id v & {:keys [id-fn] :or {id-fn :id}}]
(first (filter #(= (id-fn %) id) v)))
(defn remove-id-item
"Takes a vector of maps 'v', each of which has an id-fn (default :id) key.
Return v where item matching 'id' is excluded"
[id v & {:keys [id-fn] :or {id-fn :id}}]
(filterv #(not= (id-fn %) id) v))
;; ----------------------------------------------------------------------------
;; Other functions
;; ----------------------------------------------------------------------------
(defn enumerate
"(for [[index item first? last?] (enumerate coll)] ...) "
[coll]
(let [c (dec (count coll))
f (fn [index item] [index item (= 0 index) (= c index)])]
(map-indexed f coll)))
(defn sum-scroll-offsets
"Given a DOM node, I traverse through all ascendant nodes (until I reach body), summing any scrollLeft and scrollTop values
and return these sums in a map"
[node]
(loop [current-node (.-parentNode node) ;; Begin at parent
sum-scroll-left 0
sum-scroll-top 0]
(if (not= (.-tagName current-node) "BODY")
(recur (.-parentNode current-node)
(+ sum-scroll-left (.-scrollLeft current-node))
(+ sum-scroll-top (.-scrollTop current-node)))
{:left sum-scroll-left
:top sum-scroll-top})))
;; ----------------------------------------------------------------------------
;; date functions
;; ----------------------------------------------------------------------------
(defn now->utc
"Answer a goog.date.UtcDateTime based on local date/time."
[]
(let [local-date (js/goog.date.DateTime.)]
(js/goog.date.UtcDateTime.
(.getYear local-date)
(.getMonth local-date)
(.getDate local-date)
0 0 0 0)))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,400 @@
// Compiled by ClojureScript 1.9.229 {}
goog.provide('re_com.util');
goog.require('cljs.core');
goog.require('clojure.set');
goog.require('reagent.ratom');
goog.require('goog.date.DateTime');
goog.require('goog.date.UtcDateTime');
/**
* Takes a function 'f' amd a map 'm'. Applies 'f' to each value in 'm' and returns.
* (fmap inc {:a 4 :b 2}) => {:a 5 :b 3}
*/
re_com.util.fmap = (function re_com$util$fmap(f,m){
return cljs.core.into.call(null,cljs.core.PersistentArrayMap.EMPTY,(function (){var iter__25910__auto__ = (function re_com$util$fmap_$_iter__26710(s__26711){
return (new cljs.core.LazySeq(null,(function (){
var s__26711__$1 = s__26711;
while(true){
var temp__4657__auto__ = cljs.core.seq.call(null,s__26711__$1);
if(temp__4657__auto__){
var s__26711__$2 = temp__4657__auto__;
if(cljs.core.chunked_seq_QMARK_.call(null,s__26711__$2)){
var c__25908__auto__ = cljs.core.chunk_first.call(null,s__26711__$2);
var size__25909__auto__ = cljs.core.count.call(null,c__25908__auto__);
var b__26713 = cljs.core.chunk_buffer.call(null,size__25909__auto__);
if((function (){var i__26712 = (0);
while(true){
if((i__26712 < size__25909__auto__)){
var vec__26720 = cljs.core._nth.call(null,c__25908__auto__,i__26712);
var k = cljs.core.nth.call(null,vec__26720,(0),null);
var val = cljs.core.nth.call(null,vec__26720,(1),null);
cljs.core.chunk_append.call(null,b__26713,new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [k,f.call(null,val)], null));
var G__26726 = (i__26712 + (1));
i__26712 = G__26726;
continue;
} else {
return true;
}
break;
}
})()){
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__26713),re_com$util$fmap_$_iter__26710.call(null,cljs.core.chunk_rest.call(null,s__26711__$2)));
} else {
return cljs.core.chunk_cons.call(null,cljs.core.chunk.call(null,b__26713),null);
}
} else {
var vec__26723 = cljs.core.first.call(null,s__26711__$2);
var k = cljs.core.nth.call(null,vec__26723,(0),null);
var val = cljs.core.nth.call(null,vec__26723,(1),null);
return cljs.core.cons.call(null,new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [k,f.call(null,val)], null),re_com$util$fmap_$_iter__26710.call(null,cljs.core.rest.call(null,s__26711__$2)));
}
} else {
return null;
}
break;
}
}),null,null));
});
return iter__25910__auto__.call(null,m);
})());
});
/**
* Recursively merges maps. If vals are not maps, the last value wins.
*/
re_com.util.deep_merge = (function re_com$util$deep_merge(var_args){
var args__26212__auto__ = [];
var len__26205__auto___26728 = arguments.length;
var i__26206__auto___26729 = (0);
while(true){
if((i__26206__auto___26729 < len__26205__auto___26728)){
args__26212__auto__.push((arguments[i__26206__auto___26729]));
var G__26730 = (i__26206__auto___26729 + (1));
i__26206__auto___26729 = G__26730;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((0) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((0)),(0),null)):null);
return re_com.util.deep_merge.cljs$core$IFn$_invoke$arity$variadic(argseq__26213__auto__);
});
re_com.util.deep_merge.cljs$core$IFn$_invoke$arity$variadic = (function (vals){
if(cljs.core.every_QMARK_.call(null,cljs.core.map_QMARK_,vals)){
return cljs.core.apply.call(null,cljs.core.merge_with,re_com.util.deep_merge,vals);
} else {
return cljs.core.last.call(null,vals);
}
});
re_com.util.deep_merge.cljs$lang$maxFixedArity = (0);
re_com.util.deep_merge.cljs$lang$applyTo = (function (seq26727){
return re_com.util.deep_merge.cljs$core$IFn$_invoke$arity$variadic(cljs.core.seq.call(null,seq26727));
});
/**
* Takes a value or an atom
* If it's a value, returns it
* If it's a Reagent object that supports IDeref, returns the value inside it by derefing
*
*/
re_com.util.deref_or_value = (function re_com$util$deref_or_value(val_or_atom){
if(((!((val_or_atom == null)))?((((val_or_atom.cljs$lang$protocol_mask$partition0$ & (32768))) || (val_or_atom.cljs$core$IDeref$))?true:(((!val_or_atom.cljs$lang$protocol_mask$partition0$))?cljs.core.native_satisfies_QMARK_.call(null,cljs.core.IDeref,val_or_atom):false)):cljs.core.native_satisfies_QMARK_.call(null,cljs.core.IDeref,val_or_atom))){
return cljs.core.deref.call(null,val_or_atom);
} else {
return val_or_atom;
}
});
/**
* Takes a value or an atom
* If it's a value, returns it
* If it's a Reagent object that supports IDeref, returns the value inside it, but WITHOUT derefing
*
* The arg validation code uses this, since calling deref-or-value adds this arg to the watched ratom list for the component
* in question, which in turn can cause different rendering behaviour between dev (where we validate) and prod (where we don't).
*
* This was experienced in popover-content-wrapper with the position-injected atom which was not derefed there, however
* the dev-only validation caused it to be derefed, modifying its render behaviour and causing mayhem and madness for the developer.
*
* See below that different Reagent types have different ways of retrieving the value without causing capture, although in the case of
* Track, we just deref it as there is no peek or state, so hopefully this won't cause issues (surely this is used very rarely).
*
*/
re_com.util.deref_or_value_peek = (function re_com$util$deref_or_value_peek(val_or_atom){
if(((!((val_or_atom == null)))?((((val_or_atom.cljs$lang$protocol_mask$partition0$ & (32768))) || (val_or_atom.cljs$core$IDeref$))?true:(((!val_or_atom.cljs$lang$protocol_mask$partition0$))?cljs.core.native_satisfies_QMARK_.call(null,cljs.core.IDeref,val_or_atom):false)):cljs.core.native_satisfies_QMARK_.call(null,cljs.core.IDeref,val_or_atom))){
if((val_or_atom instanceof reagent.ratom.RAtom)){
return val_or_atom.state;
} else {
if((val_or_atom instanceof reagent.ratom.Reaction)){
return val_or_atom._peek_at();
} else {
if((val_or_atom instanceof reagent.ratom.RCursor)){
return val_or_atom._peek();
} else {
if((val_or_atom instanceof reagent.ratom.Track)){
return cljs.core.deref.call(null,val_or_atom);
} else {
if((val_or_atom instanceof reagent.ratom.Wrapper)){
return val_or_atom.state;
} else {
throw (new Error("Unknown reactive data type"));
}
}
}
}
}
} else {
return val_or_atom;
}
});
re_com.util.get_element_by_id = (function re_com$util$get_element_by_id(id){
return document.getElementById(id);
});
/**
* Left pad a string 's' with '0', until 's' has length 'len'. Return 's' unchanged, if it is already len or greater
*/
re_com.util.pad_zero = (function re_com$util$pad_zero(s,len){
if((cljs.core.count.call(null,s) < len)){
return cljs.core.apply.call(null,cljs.core.str,cljs.core.take_last.call(null,len,cljs.core.concat.call(null,cljs.core.repeat.call(null,len,"0"),s)));
} else {
return s;
}
});
/**
* return 'num' as a string of 'len' characters, left padding with '0' as necessary
*/
re_com.util.pad_zero_number = (function re_com$util$pad_zero_number(num,len){
return re_com.util.pad_zero.call(null,[cljs.core.str(num)].join(''),len);
});
/**
* takes a number (and optional :negative keyword to indicate a negative value) and returns that number as a string with 'px' at the end
*/
re_com.util.px = (function re_com$util$px(var_args){
var args__26212__auto__ = [];
var len__26205__auto___26737 = arguments.length;
var i__26206__auto___26738 = (0);
while(true){
if((i__26206__auto___26738 < len__26205__auto___26737)){
args__26212__auto__.push((arguments[i__26206__auto___26738]));
var G__26739 = (i__26206__auto___26738 + (1));
i__26206__auto___26738 = G__26739;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((1) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((1)),(0),null)):null);
return re_com.util.px.cljs$core$IFn$_invoke$arity$variadic((arguments[(0)]),argseq__26213__auto__);
});
re_com.util.px.cljs$core$IFn$_invoke$arity$variadic = (function (val,negative){
return [cljs.core.str((cljs.core.truth_(negative)?(- val):val)),cljs.core.str("px")].join('');
});
re_com.util.px.cljs$lang$maxFixedArity = (1);
re_com.util.px.cljs$lang$applyTo = (function (seq26735){
var G__26736 = cljs.core.first.call(null,seq26735);
var seq26735__$1 = cljs.core.next.call(null,seq26735);
return re_com.util.px.cljs$core$IFn$_invoke$arity$variadic(G__26736,seq26735__$1);
});
/**
* Removes the item at position n from a vector v, returning a shrunk vector
*/
re_com.util.remove_nth = (function re_com$util$remove_nth(v,n){
return cljs.core.vec.call(null,cljs.core.concat.call(null,cljs.core.subvec.call(null,v,(0),n),cljs.core.subvec.call(null,v,(n + (1)),cljs.core.count.call(null,v))));
});
re_com.util.insert_nth = (function re_com$util$insert_nth(vect,index,item){
return cljs.core.apply.call(null,cljs.core.merge,cljs.core.subvec.call(null,vect,(0),index),item,cljs.core.subvec.call(null,vect,index));
});
/**
* Takes a vector of maps 'v'. Returns the position of the first item in 'v' whose id-fn (default :id) matches 'id'.
* Returns nil if id not found
*/
re_com.util.position_for_id = (function re_com$util$position_for_id(var_args){
var args__26212__auto__ = [];
var len__26205__auto___26746 = arguments.length;
var i__26206__auto___26747 = (0);
while(true){
if((i__26206__auto___26747 < len__26205__auto___26746)){
args__26212__auto__.push((arguments[i__26206__auto___26747]));
var G__26748 = (i__26206__auto___26747 + (1));
i__26206__auto___26747 = G__26748;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((2) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((2)),(0),null)):null);
return re_com.util.position_for_id.cljs$core$IFn$_invoke$arity$variadic((arguments[(0)]),(arguments[(1)]),argseq__26213__auto__);
});
re_com.util.position_for_id.cljs$core$IFn$_invoke$arity$variadic = (function (id,v,p__26743){
var map__26744 = p__26743;
var map__26744__$1 = ((((!((map__26744 == null)))?((((map__26744.cljs$lang$protocol_mask$partition0$ & (64))) || (map__26744.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__26744):map__26744);
var id_fn = cljs.core.get.call(null,map__26744__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
var index_fn = ((function (map__26744,map__26744__$1,id_fn){
return (function (index,item){
if(cljs.core._EQ_.call(null,id_fn.call(null,item),id)){
return index;
} else {
return null;
}
});})(map__26744,map__26744__$1,id_fn))
;
return cljs.core.first.call(null,cljs.core.keep_indexed.call(null,index_fn,v));
});
re_com.util.position_for_id.cljs$lang$maxFixedArity = (2);
re_com.util.position_for_id.cljs$lang$applyTo = (function (seq26740){
var G__26741 = cljs.core.first.call(null,seq26740);
var seq26740__$1 = cljs.core.next.call(null,seq26740);
var G__26742 = cljs.core.first.call(null,seq26740__$1);
var seq26740__$2 = cljs.core.next.call(null,seq26740__$1);
return re_com.util.position_for_id.cljs$core$IFn$_invoke$arity$variadic(G__26741,G__26742,seq26740__$2);
});
/**
* Takes a vector of maps 'v'. Returns the first item in 'v' whose id-fn (default :id) matches 'id'.
* Returns nil if id not found
*/
re_com.util.item_for_id = (function re_com$util$item_for_id(var_args){
var args__26212__auto__ = [];
var len__26205__auto___26756 = arguments.length;
var i__26206__auto___26757 = (0);
while(true){
if((i__26206__auto___26757 < len__26205__auto___26756)){
args__26212__auto__.push((arguments[i__26206__auto___26757]));
var G__26758 = (i__26206__auto___26757 + (1));
i__26206__auto___26757 = G__26758;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((2) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((2)),(0),null)):null);
return re_com.util.item_for_id.cljs$core$IFn$_invoke$arity$variadic((arguments[(0)]),(arguments[(1)]),argseq__26213__auto__);
});
re_com.util.item_for_id.cljs$core$IFn$_invoke$arity$variadic = (function (id,v,p__26753){
var map__26754 = p__26753;
var map__26754__$1 = ((((!((map__26754 == null)))?((((map__26754.cljs$lang$protocol_mask$partition0$ & (64))) || (map__26754.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__26754):map__26754);
var id_fn = cljs.core.get.call(null,map__26754__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
return cljs.core.first.call(null,cljs.core.filter.call(null,((function (map__26754,map__26754__$1,id_fn){
return (function (p1__26749_SHARP_){
return cljs.core._EQ_.call(null,id_fn.call(null,p1__26749_SHARP_),id);
});})(map__26754,map__26754__$1,id_fn))
,v));
});
re_com.util.item_for_id.cljs$lang$maxFixedArity = (2);
re_com.util.item_for_id.cljs$lang$applyTo = (function (seq26750){
var G__26751 = cljs.core.first.call(null,seq26750);
var seq26750__$1 = cljs.core.next.call(null,seq26750);
var G__26752 = cljs.core.first.call(null,seq26750__$1);
var seq26750__$2 = cljs.core.next.call(null,seq26750__$1);
return re_com.util.item_for_id.cljs$core$IFn$_invoke$arity$variadic(G__26751,G__26752,seq26750__$2);
});
/**
* Takes a vector of maps 'v', each of which has an id-fn (default :id) key.
* Return v where item matching 'id' is excluded
*/
re_com.util.remove_id_item = (function re_com$util$remove_id_item(var_args){
var args__26212__auto__ = [];
var len__26205__auto___26766 = arguments.length;
var i__26206__auto___26767 = (0);
while(true){
if((i__26206__auto___26767 < len__26205__auto___26766)){
args__26212__auto__.push((arguments[i__26206__auto___26767]));
var G__26768 = (i__26206__auto___26767 + (1));
i__26206__auto___26767 = G__26768;
continue;
} else {
}
break;
}
var argseq__26213__auto__ = ((((2) < args__26212__auto__.length))?(new cljs.core.IndexedSeq(args__26212__auto__.slice((2)),(0),null)):null);
return re_com.util.remove_id_item.cljs$core$IFn$_invoke$arity$variadic((arguments[(0)]),(arguments[(1)]),argseq__26213__auto__);
});
re_com.util.remove_id_item.cljs$core$IFn$_invoke$arity$variadic = (function (id,v,p__26763){
var map__26764 = p__26763;
var map__26764__$1 = ((((!((map__26764 == null)))?((((map__26764.cljs$lang$protocol_mask$partition0$ & (64))) || (map__26764.cljs$core$ISeq$))?true:false):false))?cljs.core.apply.call(null,cljs.core.hash_map,map__26764):map__26764);
var id_fn = cljs.core.get.call(null,map__26764__$1,new cljs.core.Keyword(null,"id-fn","id-fn",316222798),new cljs.core.Keyword(null,"id","id",-1388402092));
return cljs.core.filterv.call(null,((function (map__26764,map__26764__$1,id_fn){
return (function (p1__26759_SHARP_){
return cljs.core.not_EQ_.call(null,id_fn.call(null,p1__26759_SHARP_),id);
});})(map__26764,map__26764__$1,id_fn))
,v);
});
re_com.util.remove_id_item.cljs$lang$maxFixedArity = (2);
re_com.util.remove_id_item.cljs$lang$applyTo = (function (seq26760){
var G__26761 = cljs.core.first.call(null,seq26760);
var seq26760__$1 = cljs.core.next.call(null,seq26760);
var G__26762 = cljs.core.first.call(null,seq26760__$1);
var seq26760__$2 = cljs.core.next.call(null,seq26760__$1);
return re_com.util.remove_id_item.cljs$core$IFn$_invoke$arity$variadic(G__26761,G__26762,seq26760__$2);
});
/**
* (for [[index item first? last?] (enumerate coll)] ...)
*/
re_com.util.enumerate = (function re_com$util$enumerate(coll){
var c = (cljs.core.count.call(null,coll) - (1));
var f = ((function (c){
return (function (index,item){
return new cljs.core.PersistentVector(null, 4, 5, cljs.core.PersistentVector.EMPTY_NODE, [index,item,cljs.core._EQ_.call(null,(0),index),cljs.core._EQ_.call(null,c,index)], null);
});})(c))
;
return cljs.core.map_indexed.call(null,f,coll);
});
/**
* Given a DOM node, I traverse through all ascendant nodes (until I reach body), summing any scrollLeft and scrollTop values
* and return these sums in a map
*/
re_com.util.sum_scroll_offsets = (function re_com$util$sum_scroll_offsets(node){
var current_node = node.parentNode;
var sum_scroll_left = (0);
var sum_scroll_top = (0);
while(true){
if(cljs.core.not_EQ_.call(null,current_node.tagName,"BODY")){
var G__26769 = current_node.parentNode;
var G__26770 = (sum_scroll_left + current_node.scrollLeft);
var G__26771 = (sum_scroll_top + current_node.scrollTop);
current_node = G__26769;
sum_scroll_left = G__26770;
sum_scroll_top = G__26771;
continue;
} else {
return new cljs.core.PersistentArrayMap(null, 2, [new cljs.core.Keyword(null,"left","left",-399115937),sum_scroll_left,new cljs.core.Keyword(null,"top","top",-1856271961),sum_scroll_top], null);
}
break;
}
});
/**
* Answer a goog.date.UtcDateTime based on local date/time.
*/
re_com.util.now__GT_utc = (function re_com$util$now__GT_utc(){
var local_date = (new goog.date.DateTime());
return (new goog.date.UtcDateTime(local_date.getYear(),local_date.getMonth(),local_date.getDate(),(0),(0),(0),(0)));
});
//# sourceMappingURL=util.js.map?rel=1603199188274

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,372 @@
(ns re-com.validate
(:require
[clojure.set :refer [superset?]]
[re-com.util :refer [deref-or-value-peek]]
[reagent.core :as reagent]
[reagent.impl.template :refer [valid-tag?]]
[goog.string :as gstring]
[goog.date.UtcDateTime]))
;; -- Helpers -----------------------------------------------------------------
(defn left-string
"Converts obj to a string and truncates it to max-len chars if necessary.
When truncation is necessary, adds an elipsis to the end"
[obj max-len]
(gstring/truncate (str obj) max-len))
(defn log-error
"Sends a message to the DeV Tools console as an error. Returns false to indicate 'error' condition"
[& args]
(.error js/console (apply str args))
false)
(defn log-warning
"Sends a message to the DeV Tools console as an warning. Returns true to indicate 'not and error' condition"
[& args]
(.warn js/console (apply str args))
true)
(defn hash-map-with-name-keys
[v]
(zipmap (map :name v) v))
(defn extract-arg-data
"Package up all the relevant data for validation purposes from the xxx-args-desc map into a new map"
[args-desc]
{:arg-names (set (map :name args-desc))
:required-args (->> args-desc
(filter :required)
(map :name)
set)
:validated-args (->> (filter :validate-fn args-desc)
vec
(hash-map-with-name-keys))})
;; ----------------------------------------------------------------------------
;; Primary validation functions
;; ----------------------------------------------------------------------------
(defn arg-names-valid?
"returns true if every passed-args is value. Otherwise log the problem and return false"
[defined-args passed-args]
(or (superset? defined-args passed-args)
(let [missing-args (remove defined-args passed-args)]
(log-error "Invalid argument(s): " missing-args)))) ;; Regent will show the component-path
(defn required-args-passed?
"returns true if all the required args are supplied. Otherwise log the error and return false"
[required-args passed-args]
(or (superset? passed-args required-args)
(let [missing-args (remove passed-args required-args)]
(log-error "Missing required argument(s): " missing-args)))) ;; Regent will show the component-path
(defn validate-fns-pass?
"Gathers together a list of args that have a validator and...
returns true if all argument values are valid OR are just warnings (log warning to the console).
Otherwise log an error to the console and return false.
Validation functions can return:
- true: validation success
- false: validation failed - use standard error message
- map: validation failed - includes two keys:
:status - :error: log to console as error
:warning: log to console as warning
:message - use this string in the message of the warning/error"
[args-with-validators passed-args component-name]
(let [validate-arg (fn [[_ v-arg-def]]
(let [arg-name (:name v-arg-def)
arg-val (deref-or-value-peek (arg-name passed-args)) ;; Automatically extract value if it's in an atom
required? (:required v-arg-def)
validate-result ((:validate-fn v-arg-def) arg-val)
log-msg-base (str "Validation failed for argument '" arg-name "' in component '" component-name "': ")
comp-path (str " at " (reagent/component-path (reagent/current-component)))
warning? (= (:status validate-result) :warning)]
;(println (str "[" component-name "] " arg-name " = '" (if (nil? arg-val) "nil" (left-string arg-val 200)) "' => " validate-result))
(cond
(or (true? validate-result)
(and (nil? arg-val) ;; Allow nil values through if the arg is NOT required
(not required?))) true
(false? validate-result) (log-error log-msg-base "Expected '" (:type v-arg-def) "'. Got '" (if (nil? arg-val) "nil" (left-string arg-val 60)) "'" comp-path)
(map? validate-result) ((if warning? log-warning log-error)
log-msg-base
(:message validate-result)
(when warning? comp-path))
:else (log-error "Invalid return from validate-fn: " validate-result comp-path))))]
(->> (select-keys args-with-validators (vec (keys passed-args)))
(map validate-arg)
(every? true?))))
(defn validate-args
"Calls three validation tests:
- Are arg names valid?
- Have all required args been passed?
- Specific valiadation function calls to check arg values if specified
If they all pass, returns true.
Normally used for a call to the {:pre...} at the beginning of a function"
[arg-defs passed-args & component-name]
(if-not ^boolean js/goog.DEBUG
true
(let [passed-arg-keys (set (keys passed-args))]
(and (arg-names-valid? (:arg-names arg-defs) passed-arg-keys)
(required-args-passed? (:required-args arg-defs) passed-arg-keys)
(validate-fns-pass? (:validated-args arg-defs) passed-args (first component-name))))))
;; ----------------------------------------------------------------------------
;; Custom :validate-fn functions based on (validate-arg-against-set)
;; ----------------------------------------------------------------------------
(def justify-options [:start :end :center :between :around])
(def align-options [:start :end :center :baseline :stretch])
(def scroll-options [:auto :off :on :spill])
(def alert-types [:none :info :warning :danger])
(def button-sizes [:regular :smaller :larger])
(def throbber-sizes [:regular :smaller :small :large])
(def input-status-types [:success :warning :error :validating])
(def popover-status-types [:success :warning :error :validating :info])
(def title-levels [:level1 :level2 :level3 :level4])
(def position-options [:above-left :above-center :above-right
:below-left :below-center :below-right
:left-above :left-center :left-below
:right-above :right-center :right-below])
(defn validate-arg-against-set
"Validates the passed argument against the expected set"
[arg arg-name valid-set]
(let [arg (deref-or-value-peek arg)]
(or (not= (some (hash-set arg) valid-set) nil)
(str "Invalid " arg-name ". Expected one of " valid-set ". Got '" (left-string arg 40) "'"))))
(defn justify-style? [arg] (validate-arg-against-set arg ":justify-style" justify-options))
(defn align-style? [arg] (validate-arg-against-set arg ":align-style" align-options))
(defn scroll-style? [arg] (validate-arg-against-set arg ":scroll-style" scroll-options))
(defn alert-type? [arg] (validate-arg-against-set arg ":alert-type" alert-types))
(defn button-size? [arg] (validate-arg-against-set arg ":size" button-sizes))
(defn throbber-size? [arg] (validate-arg-against-set arg ":size" throbber-sizes))
(defn input-status-type? [arg] (validate-arg-against-set arg ":status" input-status-types))
(defn popover-status-type? [arg] (validate-arg-against-set arg ":status" popover-status-types))
(defn title-level-type? [arg] (validate-arg-against-set arg ":level" title-levels))
(defn position? [arg] (validate-arg-against-set arg ":position" position-options))
;; ----------------------------------------------------------------------------
;; Predefined hiccup lists for streamlined consumption in arg documentation
;; ----------------------------------------------------------------------------
(defn make-code-list
"Given a vector or list of codes, create a [:span] hiccup vector containing a comma separated list of the codes"
[codes]
(into [:span] (interpose ", " (map #(vector :code (str %)) codes))))
(def justify-options-list (make-code-list justify-options))
(def align-options-list (make-code-list align-options))
(def scroll-options-list (make-code-list scroll-options))
(def alert-types-list (make-code-list alert-types))
(def button-sizes-list (make-code-list button-sizes))
(def throbber-sizes-list (make-code-list throbber-sizes))
(def input-status-types-list (make-code-list input-status-types))
(def popover-status-types-list (make-code-list popover-status-types))
(def title-levels-list (make-code-list title-levels))
(def position-options-list (make-code-list position-options))
;; ----------------------------------------------------------------------------
;; Custom :validate-fn functions
;; ----------------------------------------------------------------------------
(def html-attrs #{; ----- HTML attributes (:class and :style commented out as they are not valid in re-com)
; ----- Reference: https://facebook.github.io/react/docs/dom-elements.html#all-supported-html-attributes
:accept :accept-charset :access-key :action :allow-full-screen :allow-transparency :alt :async :auto-complete :auto-focus :auto-play :capture
:cell-padding :cell-spacing :challenge :char-set :checked :cite #_:class :class-name :cols :col-span :content :content-editable :context-menu :controls
:coords :cross-origin :data :date-time :default :defer :dir :disabled :download :draggable :enc-type :form :form-action :form-enc-type :form-method
:form-no-validate :form-target :frame-border :headers :height :hidden :high :href :href-lang :html-for :http-equiv :icon :id :input-mode :integrity
:is :key-params :key-type :kind :label :lang :list :loop :low :manifest
:margin-height :margin-width :max :max-length :media :media-group :method :min :min-length :multiple :muted :name :no-validate :nonce :open :optimum :pattern :placeholder
:poster :preload :profile :radio-group :read-only :rel :required :reversed :role :rows :row-span :sandbox :scope :scoped :scrolling :seamless :selected :shape :size :sizes
:span :spell-check :src :src-doc :src-lang :src-set :start :step #_:style :summary :tab-index :target :title :type :use-map :value :width :wmode :wrap
; ----- SVG attributes
; ----- Reference: https://facebook.github.io/react/docs/dom-elements.html#all-supported-svg-attributes
:accentheight :accumulate :additive :alignment-baseline :allow-reorder :alphabetic :amplitude :arabic-form :ascent :attribute-name :attribute-type
:auto-reverse :azimuth :base-frequency :base-profile :baseline-shift :bbox :begin :bias :by :calc-mode :cap-height :clip :clip-path :clip-path-units
:clip-rule :color-interpolation :color-interpolation-filters :color-profile :color-rendering :content-script-type :content-style-type :cursor :cx :cy :d
:decelerate :descent :diffuse-constant :direction :display :divisor :dominant-baseline :dur :dx :dy :edge-mode :elevation :enable-background :end :exponent
:external-resources-required :fill :fill-opacity :fill-rule :filter :filter-res :filter-units :flood-color :flood-opacity :focusable :font-family :font-size
:font-size-adjust :font-stretch :font-style :font-variant :font-weight :format :from :fx :fy :g1 :g2 :glyph-name :glyph-orientation-horizontal :glyph-orientation-vertical
:glyph-ref :gradient-transform :gradient-units :hanging :horiz-adv-x :horiz-origin-x :ideographic :image-rendering :in :in2 :intercept :k :k1 :k2 :k3 :k4
:kernel-matrix :kernel-unit-length :kerning :key-points :key-splines :key-times :length-adjust :letter-spacing :lighting-color :limiting-cone-angle :local
:marker-end :marker-height :marker-mid :marker-start :marker-units :marker-width :mask :mask-content-units :mask-units :mathematical :mode :num-octaves
:offset :opacity :operator :order :orient :orientation :origin :overflow :overline-position :overline-thickness :paint-order :panose1 :path-length
:pattern-content-units :pattern-transform :pattern-units :pointer-events :points :points-at-x :points-at-y :points-at-z :preserve-alpha :preserve-aspect-ratio
:primitive-units :r :radius :ref-x :ref-y :rendering-intent :repeat-count :repeat-dur :required-extensions :required-features :restart :result :rotate :rx :ry
:scale :seed :shape-rendering :slope :spacing :specular-constant :specular-exponent :speed :spread-method :start-offset :std-deviation :stemh :stemv :stitch-tiles
:stop-color :stop-opacity :strikethrough-position :strikethrough-thickness :string :stroke :stroke-dasharray :stroke-dashoffset :stroke-linecap :stroke-linejoin
:stroke-miterlimit :stroke-opacity :stroke-width :surface-scale :system-language :table-values :target-x :target-y :text-anchor :text-decoration :text-length
:text-rendering :to :transform :u1 :u2 :underline-position :underline-thickness :unicode :unicode-bidi :unicode-range :units-per-em :v-alphabetic :v-hanging
:v-ideographic :v-mathematical :values :vector-effect :version :vert-adv-y :vert-origin-x :vert-origin-y :view-box :view-target :visibility :widths :word-spacing
:writing-mode :x :x1 :x2 :x-channel-selector :x-height :xlink-actuate :xlink-arcrole :xlink-href :xlink-role :xlink-show :xlink-title :xlink-type :xml-base
:xml-lang :xml-space :y :y1 :y2 :y-channel-selector :z :zoom-and-pan
; ----- Event attributes
; ----- Reference: https://facebook.github.io/react/docs/events.html#supported-events
:on-copy :on-cut :on-paste :on-composition-end :on-composition-start :on-composition-update :on-key-down
:on-key-press :on-key-up :on-focus :on-blur :on-change :on-input :on-submit :on-click
:on-context-menu :on-double-click :on-drag :on-drag-end :on-drag-enter :on-drag-exit :on-drag-leave
:on-drag-over :on-drag-start :on-drop :on-mouse-down :on-mouse-enter :on-mouse-leave :on-mouse-move
:on-mouse-out :on-mouse-over :on-mouse-up :on-select :on-touch-cancel :on-touch-end :on-touch-move
:on-touch-start :on-scroll :on-wheel :on-abort :on-can-play :on-can-play-through :on-duration-change
:on-emptied :on-encrypted :on-ended :on-error :on-loaded-data :on-loaded-metadata :on-load-start
:on-pause :on-play :on-playing :on-progress :on-rate-change :on-seeked :on-seeking :on-stalled
:on-suspend :on-time-update :on-volume-change :on-waiting :on-load #_:on-error :on-animation-start
:on-animation-end :on-animation-iteration :on-transition-end
; ----- '--capture' versions of the above events
:on-copy-capture :on-cut-capture :on-paste-capture :on-composition-end-capture :on-composition-start-capture :on-composition-update-capture :on-key-down-capture
:on-key-press-capture :on-key-up-capture :on-focus-capture :on-blur-capture :on-change-capture :on-input-capture :on-submit-capture :on-click-capture
:on-context-menu-capture :on-double-click-capture :on-drag-capture :on-drag-end-capture :on-drag-enter-capture :on-drag-exit-capture :on-drag-leave-capture
:on-drag-over-capture :on-drag-start-capture :on-drop-capture :on-mouse-down-capture :on-mouse-enter-capture :on-mouse-leave-capture :on-mouse-move-capture
:on-mouse-out-capture :on-mouse-over-capture :on-mouse-up-capture :on-select-capture :on-touch-cancel-capture :on-touch-end-capture :on-touch-move-capture
:on-touch-start-capture :on-scroll-capture :on-wheel-capture :on-abort-capture :on-can-play-capture :on-can-play-through-capture :on-duration-change-capture
:on-emptied-capture :on-encrypted-capture :on-ended-capture :on-error-capture :on-loaded-data-capture :on-loaded-metadata-capture :on-load-start-capture
:on-pause-capture :on-play-capture :on-playing-capture :on-progress-capture :on-rate-change-capture :on-seeked-capture :on-seeking-capture :on-stalled-capture
:on-suspend-capture :on-time-update-capture :on-volume-change-capture :on-waiting-capture :on-load-capture #_:on-error-capture :on-animation-start-capture
:on-animation-end-capture :on-animation-iteration-capture :on-transition-end-capture})
; ----- Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*
; ----- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
(def extension-attrs #{:data :aria})
(def css-styles #{; ----- Standard CSS styles
; ----- Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference
:align-content :align-items :align-self :all :animation :animation-delay :animation-direction :animation-duration :animation-fill-mode
:animation-iteration-count :animation-name :animation-play-state :animation-timing-function :backface-visibility :background
:background-attachment :background-blend-mode :background-clip :background-color :background-image :background-origin :background-position
:background-repeat :background-size :block-size :border :border-block-end :border-block-end-color :border-block-end-style :border-block-end-width
:border-block-start :border-block-start-color :border-block-start-style :border-block-start-width :border-bottom :border-bottom-color
:border-bottom-left-radius :border-bottom-right-radius :border-bottom-style :border-bottom-width :border-collapse :border-color :border-image
:border-image-outset :border-image-repeat :border-image-slice :border-image-source :border-image-width :border-inline-end :border-inline-end-color
:border-inline-end-style :border-inline-end-width :border-inline-start :border-inline-start-color :border-inline-start-style :border-inline-start-width
:border-left :border-left-color :border-left-style :border-left-width :border-radius :border-right :border-right-color :border-right-style
:border-right-width :border-spacing :border-style :border-top :border-top-color :border-top-left-radius :border-top-right-radius :border-top-style
:border-top-width :border-width :bottom :box-decoration-break :box-shadow :box-sizing :break-after :break-before :break-inside :caption-side :ch :clear
:clip :clip-path :cm :color :column-count :column-fill :column-gap :column-rule :column-rule-color :column-rule-style :column-rule-width :columns
:column-span :column-width :content :counter-increment :counter-reset :cursor :deg :direction :display :dpcm :dpi :dppx :em :empty-cells :ex :filter
:flex :flex-basis :flex-direction :flex-flow :flex-grow :flex-shrink :flex-wrap :float :font :font-family :font-feature-settings :font-kerning
:font-language-override :font-size :font-size-adjust :font-stretch :font-style :font-synthesis :font-variant :font-variant-alternates :font-variant-caps
:font-variant-east-asian :font-variant-ligatures :font-variant-numeric :font-variant-position :font-weight :fr :grad :grid :grid-area :grid-auto-columns
:grid-auto-flow :grid-auto-position :grid-auto-rows :grid-column :grid-column-end :grid-column-gap :grid-column-start :grid-gap :grid-row :grid-row-end
:grid-row-gap :grid-row-start :grid-template :grid-template-areas :grid-template-columns :grid-template-rows :height :hyphens :hz :image-orientation
:image-rendering :image-resolution :ime-mode :in :inherit :initial :inline-size :isolation :justify-content :khz :left :letter-spacing :line-break
:line-height :list-style :list-style-image :list-style-position :list-style-type :margin :margin-block-end :margin-block-start :margin-bottom
:margin-inline-end :margin-inline-start :margin-left :margin-right :margin-top :marks :mask :mask-clip :mask-composite :mask-image :mask-mode
:mask-origin :mask-position :mask-repeat :mask-size :mask-type :max-block-size :max-height :max-inline-size :max-width :min-block-size :min-height
:min-inline-size :min-width :mix-blend-mode :mm :ms :object-fit :object-position :offset-block-end :offset-block-start :offset-inline-end
:offset-inline-start :opacity :order :orphans :outline :outline-color :outline-offset :outline-style :outline-width :overflow :overflow-wrap
:overflow-x :overflow-y :padding :padding-block-end :padding-block-start :padding-bottom :padding-inline-end :padding-inline-start :padding-left
:padding-right :padding-top :page-break-after :page-break-before :page-break-inside :pc :perspective :perspective-origin :pointer-events :position
:pt :px :quotes :rad :rem :resize :revert :right :ruby-align :ruby-merge :ruby-position :s :scroll-behavior :scroll-snap-coordinate :scroll-snap-destination
:scroll-snap-type :shape-image-threshold :shape-margin :shape-outside :table-layout :tab-size :text-align :text-align-last :text-combine-upright
:text-decoration :text-decoration-color :text-decoration-line :text-decoration-style :text-emphasis :text-emphasis-color :text-emphasis-position
:text-emphasis-style :text-indent :text-orientation :text-overflow :text-rendering :text-shadow :text-transform :text-underline-position :top
:touch-action :transform :transform-box :transform-origin :transform-style :transition :transition-delay :transition-duration :transition-property
:transition-timing-function :turn :unicode-bidi :unicode-range :unset :vertical-align :vh :visibility :vmax :vmin :vw :white-space :widows :width
:will-change :word-break :word-spacing :word-wrap :writing-mode :z-index
; ----- Browser specific styles
:-webkit-user-select :-moz-user-select :-ms-user-select :user-select
:-webkit-flex-flow :-webkit-flex-direction :-webkit-flex-wrap :-webkit-justify-content :-webkit-align-items :-webkit-align-content
:-webkit-flex :-webkit-flex-grow :-webkit-flex-shrink :-webkit-flex-basis :-webkit-order :-webkit-align-self})
(defn string-or-hiccup?
"Returns true if the passed argument is either valid hiccup or a string, otherwise false/error"
[arg]
(valid-tag? (deref-or-value-peek arg)))
(defn vector-of-maps?
"Returns true if the passed argument is a vector of maps (either directly or contained in an atom), otherwise false/error
Notes:
- actually it also accepts a list of maps (should we rename this? Potential long/ugly names: sequential-of-maps?, vector-or-list-of-maps?)
- vector/list can be empty
- only checks the first element in the vector/list"
[arg]
(let [arg (deref-or-value-peek arg)]
(and (sequential? arg) ;; Allows lists as well
(or (empty? arg)
(map? (first arg))))))
(defn css-style?
"Returns true if the passed argument is a valid CSS style.
Otherwise returns a warning map"
[arg]
(if-not ^boolean js/goog.DEBUG
true
(let [arg (deref-or-value-peek arg)]
(and (map? arg)
(let [arg-keys (keys arg)]
(or (superset? css-styles arg-keys)
{:status :warning
:message (str "Unknown CSS style(s): " (remove css-styles arg-keys))}))))))
(defn extension-attribute?
"Returns truthy if the attribute name is an extension attribute, that is data-* or aria-*, otherwise falsey."
([attr]
(let [attr (name attr)
ext? #(and (= (.indexOf attr %) 0)
(> (count attr) (count %)))]
(some (comp ext? #(str % "-") name) extension-attrs))))
(defn invalid-html-attrs
"Returns the subset of HTML attributes contained in the passed argument that are not valid HTML attributes."
[attrs]
(remove #(or (html-attrs %)
(extension-attribute? %))
attrs))
(defn html-attr?
"Returns true if the passed argument is a valid HTML, SVG or event attribute.
Otherwise returns a warning map.
Notes:
- Prevents :class and :style attributes"
[arg]
(if-not ^boolean js/goog.DEBUG
true
(let [arg (deref-or-value-peek arg)]
(and (map? arg)
(let [arg-keys (set (keys arg))
contains-class? (contains? arg-keys :class)
contains-style? (contains? arg-keys :style)
result (cond
contains-class? ":class not allowed in :attr argument"
contains-style? ":style not allowed in :attr argument"
:else (when-let [invalid (not-empty (invalid-html-attrs arg-keys))]
(str "Unknown HTML attribute(s): " invalid)))]
(or (nil? result)
{:status (if (or contains-class? contains-style?) :error :warning)
:message result}))))))
(defn goog-date?
"Returns true if the passed argument is a valid goog.date.UtcDateTime, otherwise false/error"
[arg]
(let [arg (deref-or-value-peek arg)]
(instance? js/goog.date.UtcDateTime arg)))
(defn regex?
"Returns true if the passed argument is a valid regular expression, otherwise false/error"
[arg]
(let [arg (deref-or-value-peek arg)]
(instance? js/RegExp arg)))
(defn number-or-string?
"Returns true if the passed argument is a number or a string, otherwise false/error"
[arg]
(let [arg (deref-or-value-peek arg)]
(or (number? arg) (string? arg))))
(defn string-or-atom?
"Returns true if the passed argument is a string (or a string within an atom), otherwise false/error"
[arg]
(string? (deref-or-value-peek arg)))
(defn set-or-atom?
"Returns true if the passed argument is a set (or a set within an atom), otherwise false/error"
[arg]
(set? (deref-or-value-peek arg)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long