swinging-needle-meter/resources/public/js/compiled/out/re_com/input_time.cljs
2020-10-20 14:44:11 +01:00

190 lines
9.7 KiB
Clojure

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