diff --git a/run/resources/public/assets/css/re-com.css b/run/resources/public/assets/css/re-com.css index 9db5a12a..edc8ae61 100644 --- a/run/resources/public/assets/css/re-com.css +++ b/run/resources/public/assets/css/re-com.css @@ -745,6 +745,120 @@ fieldset[disabled] .btn { background-color: #F7F7F7 } +/*daterange*/ + +.rc-daterange-td-basic { + padding: 5px; + width: 30px; + text-align: center; + color: #2c2d2ecb; + white-space: nowrap; + font-weight: normal; +} + +.daterange-default-td:hover { + cursor: pointer; + background: #357ebd; + color: #FFF; + /*background-color: #889afd3b*/ +} + +.daterange-week { + font-size: 80%; + width: 14px; + text-align: left; + color: #ccc +} + +.daterange-interval-td { + background-color: #b0d2ffb4; +} + +.daterange-interval-td:hover { + cursor: pointer; + background: #357ebd; + color: #FFF; + /*background-color: #50b4db*/ +} + +.daterange-start-td { + cursor: pointer; + background-color: #3ab0ffbd; + border-radius: 5px 0px 0px 5px; +} + +.daterange-end-td { + cursor: pointer; + background-color: #3ab0ffbd; + border-radius: 0px 5px 5px 0px; +} + +.daterange-start-td:hover, .daterange-end-td:hover { + cursor: pointer; + color: white; + background-color: #357ebd; +} + +.daterange-temp-td { + background-color: #889afd3b; +} + +.daterange-temp-td:hover { + background-color: #357ebd; + border-radius: 0px 5px 5px 0px; + cursor: pointer; + color: white; +} + +.rc-daterange-nav-icon { + height: 24px; + width: 24px; +} + +.rc-daterange-nav-button:hover { + background-color: #357ebd; + cursor: pointer; + color: white; +} + +.rc-daterange-nav-button path { + fill: #357ebd; +} + +.rc-daterange-nav-button:hover path { + fill: #FFF +} + +.daterange-day-title { + text-align: center; + font-weight: 500; + padding: 4px; +} + +.daterange-disabled-td { + opacity: 0.4; +} + +.daterange-today { + background-color: #ffd413; + border-radius: 100px; +} + +.daterange-today:hover { + cursor: pointer; + color: white; + background-color: #9e8100; +} + +.rc-daterange-month-title { + font-size: 17px; + font-weight: 700; +} + +.rc-daterange-year-title { + font-weight: 630; +} + /*---------------------------------------------------------------------------------------- END OF DATE PICKER SECTION... diff --git a/src/re_com/datepicker.cljs b/src/re_com/datepicker.cljs index a0304a1e..17375e9d 100644 --- a/src/re_com/datepicker.cljs +++ b/src/re_com/datepicker.cljs @@ -512,6 +512,7 @@ [{:name :model :required false :type "satisfies DateTimeProtocol | r/atom" :validate-fn date-like? :description [:span "the selected date. If provided, should pass pred " [:code ":selectable-fn"] ". If not provided, (now->utc) will be used and the returned date will be a " [:code "goog.date.UtcDateTime"]]} {:name :on-change :required true :type "satisfies DateTimeProtocol -> nil" :validate-fn fn? :description [:span "called when a new selection is made. Returned type is the same as model (unless model is nil, in which case it will be " [:code "goog.date.UtcDateTime"] ")"]} {:name :disabled? :required false :default false :type "boolean | atom" :description "when true, the user can't select dates but can navigate"} + {:name :initial-display :required false :type "satisfies DateTimeProtocol | r/atom" :validate-fn date-like? :description "set the months shown when no model is selected, defaults to the current month"} {:name :selectable-fn :required false :default "(fn [date] true)" :type "function" :validate-fn fn? :description "This predicate function is called with one argument, the 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"} @@ -528,12 +529,12 @@ {:name :debug-as :required false :type "map" :validate-fn map? :description [:span "Used in dev builds to assist with debugging, when one component is used implement another component, and we want the implementation component to masquerade as the original component in debug output, such as component stacks. A map optionally containing keys" [:code ":component"] "and" [:code ":args"] "."]}])) (defn datepicker - [& {:keys [model] :as args}] + [& {:keys [model initial-display] :as args}] (or (validate-args-macro datepicker-args-desc args) (let [external-model (reagent/atom (deref-or-value model)) ;; Set model type in stone on creation of this datepicker instance internal-model (reagent/atom @external-model) ;; Holds the last known external value of model, to detect external model changes - display-month (reagent/atom (cljs-time/first-day-of-the-month (or @internal-model (now->utc))))] + display-month (reagent/atom (cljs-time/first-day-of-the-month (or @internal-model initial-display (now->utc))))] (fn datepicker-render [& {:keys [model on-change disabled? start-of-week hide-border? class style attr parts src debug-as] :or {start-of-week 6} ;; Default to Sunday @@ -547,7 +548,7 @@ (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 (cljs-time/first-day-of-the-month (or @internal-model (now->utc))))) + (reset! display-month (cljs-time/first-day-of-the-month (or @internal-model initial-display (now->utc))))) [main-div-with [:table (merge @@ -602,40 +603,40 @@ (defn datepicker-dropdown [& {:keys [src] :as args}] (or - (validate-args-macro datepicker-dropdown-args-desc args) - (let [shown? (reagent/atom false) - cancel-popover #(reset! shown? false) - position :below-left] - (fn datepicker-dropdown-render - [& {:keys [model show-weeks? on-change format goog? no-clip? placeholder width disabled? position-offset src debug-as] - :or {no-clip? true, position-offset 0} - :as passthrough-args}] - (or - (validate-args-macro datepicker-dropdown-args-desc 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 (-> passthrough-args - (dissoc :format :goog? :no-clip? :placeholder :width :position-offset) ;; these keys only valid at this API level - (assoc :on-change collapse-on-select) - (assoc :src (at)) - (merge {:hide-border? true}) ;; apply defaults - vec - flatten)] - [popover-anchor-wrapper - :src src - :debug-as (or debug-as (reflect-current-component)) - :class "rc-datepicker-dropdown-wrapper" - :showing? shown? - :position position - :anchor [anchor-button shown? model format goog? placeholder width disabled?] - :popover [popover-content-wrapper - :src (at) - :position-offset (+ (if show-weeks? 43 44) position-offset) - :no-clip? no-clip? - :arrow-length 0 - :arrow-width 0 - :arrow-gap 3 - :padding "0px" - :on-cancel cancel-popover - :body (into [datepicker] passthrough-args)]])))))) + (validate-args-macro datepicker-dropdown-args-desc args) + (let [shown? (reagent/atom false) + cancel-popover #(reset! shown? false) + position :below-left] + (fn datepicker-dropdown-render + [& {:keys [model show-weeks? on-change format goog? no-clip? placeholder width disabled? position-offset src debug-as] + :or {no-clip? true, position-offset 0} + :as passthrough-args}] + (or + (validate-args-macro datepicker-dropdown-args-desc 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 (-> passthrough-args + (dissoc :format :goog? :no-clip? :placeholder :width :position-offset) ;; these keys only valid at this API level + (assoc :on-change collapse-on-select) + (assoc :src (at)) + (merge {:hide-border? true}) ;; apply defaults + vec + flatten)] + [popover-anchor-wrapper + :src src + :debug-as (or debug-as (reflect-current-component)) + :class "rc-datepicker-dropdown-wrapper" + :showing? shown? + :position position + :anchor [anchor-button shown? model format goog? placeholder width disabled?] + :popover [popover-content-wrapper + :src (at) + :position-offset (+ (if show-weeks? 43 44) position-offset) + :no-clip? no-clip? + :arrow-length 0 + :arrow-width 0 + :arrow-gap 3 + :padding "0px" + :on-cancel cancel-popover + :body (into [datepicker] passthrough-args)]])))))) diff --git a/src/re_com/daterange.cljs b/src/re_com/daterange.cljs new file mode 100644 index 00000000..10a594bb --- /dev/null +++ b/src/re_com/daterange.cljs @@ -0,0 +1,539 @@ +(ns re-com.daterange + (:require-macros + [re-com.core :refer [handler-fn at reflect-current-component]]) + (:require + [reagent.core :as r] + [re-com.config :refer [include-args-desc?]] + [re-com.box :refer [line border flex-child-style]] + [re-com.core :as re-com :refer [at v-box h-box box popover-anchor-wrapper popover-content-wrapper]] + [re-com.validate :refer [date-like? css-style? html-attr? parts?] :refer-macros [validate-args-macro]] + [re-com.util :refer [deref-or-value now->utc]] + [cljs-time.format :refer [parse unparse formatter]] + [cljs-time.core :as cljs-time] + [goog.string :as gstring :refer [format]]) + (:import + [goog.i18n DateTimeFormat])) + +(defn- dec-month [date-time] (cljs-time/minus date-time (cljs-time/months 1))) +(defn- plus-month [date-time] (cljs-time/plus date-time (cljs-time/months 1))) +(defn- plus-day [date-time] (cljs-time/plus date-time (cljs-time/days 1))) +(defn- dec-year [date-time] (cljs-time/minus date-time (cljs-time/years 1))) +(defn- plus-year [date-time] (cljs-time/plus date-time (cljs-time/years 1))) + +;for internationalisation +(defn- month-label + "Returns the appropriate month from the list on ordered months (likely not in english)" + [date {:keys [months]}] + (if months + (->> date + cljs-time/month + dec + (nth months) + str) + (unparse (formatter "MMMM") date))) + + +;;button icon svg's +(defn- prev-month-icon + [parts] + [:svg + (merge {:class (str "rc-daterange-nav-icon" (get-in parts [:prev-month-icon :class])) + :style (get-in parts [:prev-month-icon :style]) + :viewBox "0 0 24 24"} + (get-in parts [:prev-month-icon :attr])) + [:path {:d "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z"}]]) + +(defn- prev-year-icon + [parts] + [:svg + (merge {:class (str "rc-daterange-nav-icon" (get-in parts [:prev-month-icon :class])) + :style (get-in parts [:prev-month-icon :style]) + :viewBox "0 0 24 24"} + (get-in parts [:prev-year-icon :attr])) + [:g + {:transform "translate(1.5)"} + [:path {:d "m 16.793529,7.4382353 -1.41,-1.41 -5.9999996,5.9999997 5.9999996,6 1.41,-1.41 -4.58,-4.59 z"}] + [:path {:d "m 10.862647,7.4429412 -1.4100003,-1.41 -6,5.9999998 6,6 1.4100003,-1.41 -4.5800003,-4.59 z"}]]]) + +(defn- next-month-icon + [parts] + [:svg + (merge {:class (str "rc-daterange-nav-icon" (get-in parts [:prev-month-icon :class])) + :style (get-in parts [:prev-month-icon :style]) + :viewBox "0 0 24 24"} + (get-in parts [:next-month-icon :attr])) + [:path {:d "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"}]]) + +(defn- next-year-icon + [parts] + [:svg + (merge {:class (str "rc-daterange-nav-icon" (get-in parts [:prev-month-icon :class])) + :style (get-in parts [:prev-month-icon :style]) + :viewBox "0 0 24 24"} + (get-in parts [:next-year-icon :attr])) + [:g + {:transform "translate(-1.5)"} + [:path {:d "m 8.5882353,6 -1.41,1.41 4.5799997,4.59 -4.5799997,4.59 1.41,1.41 5.9999997,-6 z"}] + [:path {:d "m 14.547353,5.9623529 -1.41,1.41 4.58,4.5900001 -4.58,4.59 1.41,1.41 6,-6 z"}]]]) + + +;;boxes containing icons, attr's should be added at this level +(defn- prev-year-nav [current-month-atom parts] + (let [prev-year (dec-year (deref-or-value current-month-atom))] + [box :src (at) + :class (str "rc-daterange-nav-button " (get-in parts [:prev-year :class])) + :style (get-in parts [:prev-year :style]) + :attr (merge + {:on-click #(reset! current-month-atom prev-year)} + (get-in parts [:prev-year :attr])) + :child [prev-year-icon parts]])) + +(defn- prev-month-nav [current-month-atom parts] + (let [prev-month (dec-month (deref-or-value current-month-atom))] + [box :src (at) + :class (str "rc-daterange-nav-button " (get-in parts [:prev-month :class])) + :style (get-in parts [:prev-month :style]) + :attr (merge + {:on-click #(reset! current-month-atom prev-month)} + (get-in parts [:prev-month :attr])) + :child [prev-month-icon parts]])) + +(defn- next-year-nav [current-month-atom parts] + (let [next-year (plus-year (deref-or-value current-month-atom))] + [box :src (at) + :class (str "rc-daterange-nav-button " (get-in parts [:next-year :class])) + :style (get-in parts [:next-year :style]) + :attr (merge + {:on-click #(reset! current-month-atom next-year)} + (get-in parts [:next-year :attr])) + :child [next-year-icon parts]])) + +(defn- next-month-nav [current-month-atom parts] + (let [next-month (plus-month (deref-or-value current-month-atom))] + [box :src (at) + :class (str "rc-daterange-nav-button " (get-in parts [:next-month :class])) + :style (get-in parts [:next-month :stlye]) + :attr (merge + {:on-click #(reset! current-month-atom next-month)} + (get-in parts [:next-month :attr])) + :child [next-month-icon parts]])) + +(defn- prev-nav [current-month-atom parts i18n] + [h-box :src (at) + :align-self :stretch + :class (str "rc-daterange-prev-nav" (get-in parts [:prev-nav :class])) + :style (get-in parts [:prev-nav :style]) + :attr (get-in parts [:prev-nav :attr]) + :children [[prev-year-nav current-month-atom parts] + [line] + [prev-month-nav current-month-atom parts] + [h-box + :size "auto" + :justify :center + :children [[box + :src (at) + :class (str "rc-daterange-month-title" (get-in parts [:month-title :class])) + :style (get-in parts [:month-title :style]) + :attr (get-in parts [:month-title :attr]) + :child (month-label (deref-or-value current-month-atom) i18n)]]] + [box + :align-self :end + :justify :end + :width "49px" + :class (str "rc-daterange-year-title " (get-in parts [:year-title :class])) + :style (get-in parts [:year-title :style]) + :attr (get-in parts [:year-title :attr]) + :child (str (unparse (formatter "YYYY") (deref-or-value current-month-atom)))]]]) + +(defn- next-nav [current-month-atom parts i18n] + [h-box :src (at) + :align-self :stretch + :class (str "rc-daterange-next-nav" (get-in parts [:next-nav :class])) + :style (get-in parts [:next-nav :style]) + :attr (get-in parts [:next-nav :attr]) + :children [[box + :align-self :end + :justify :start + :width "49px" + :class (str "rc-daterange-year-title " (get-in parts [:year-title :class])) + :style (get-in parts [:year-title :style]) + :attr (get-in parts [:year-title :attr]) + :child (str (unparse (formatter "YYYY") (plus-month (deref-or-value current-month-atom))))] + [h-box + :size "auto" + :justify :center + :children [[box + :src (at) + :class (str "rc-daterange-month-title " (get-in parts [:month-title :class])) + :style (get-in parts [:month-title :style]) + :attr (get-in parts [:month-title :attr]) + :child (month-label (plus-month (deref-or-value current-month-atom)) i18n)]]] + [next-month-nav current-month-atom parts] + [line] + [next-year-nav current-month-atom parts]]]) + +(defn- main-div-with + "Main container to pass: class, style and attributes" + [body hide-border? class style attr parts src debug-as] + [h-box + :src src + :debug-as debug-as + :size "none" + :height "250px" + :class "rc-daterange-wrapper" + :children [[border + :src (at) + :class (str "rc-daterange-border noselect" (get-in parts [:border :class])) + :style (get-in parts [:border :style]) + :attr (get-in parts [:border :attr]) + :radius "5px" + :size "none" + :border (when hide-border? "none") + :child [:div + (merge {:class class + :style (merge {:font-size "13px" + :position "static"} + style)} + attr) + body]]]]) + +(defn- date-disabled? + "Checks various things to see if a date had been disabled." + [date [minimum maximum disabled? selectable-fn]] + (let [too-early? (when minimum (cljs-time/before? date (deref-or-value minimum))) + too-late? (when maximum (cljs-time/after? date (deref-or-value maximum))) + de-selected? (when selectable-fn (not (selectable-fn date)))] + (or too-early? too-late? de-selected? disabled?))) + +(defn- create-interval + "inclusively creates a vector of date-formats from start to end." + [start end] + (let [first (deref-or-value start) + last (deref-or-value end)] + (loop [cur first result []] + (if (cljs-time/after? cur last) + result + (recur (plus-day cur) (conj result cur)))))) + +(defn- interval-valid? + "Returns true if all days are NOT disabled in some way." + [start end disabled-data] + (let [interval (create-interval start end)] + (->> interval + (map #(date-disabled? % disabled-data)) + (some identity) + not))) + +(defn- td-click-handler + "Depending on the stage of the selection and if the new selected date is before the old start date, do different things" + [day [fsm start-date end-date] on-change check-interval? disabled-data] + (if + (and (= @fsm "pick-end") ;if we're picking and end date + (cljs-time/before? @start-date day) + (if check-interval? (interval-valid? start-date day disabled-data) true)) + (do + (reset! fsm "pick-start") + (reset! end-date day) ;update the internal end-date value + (on-change {:start @start-date :end day})) ;run the on-change function + + (do ;if we're picking a start date + (reset! start-date day) + (reset! end-date day) ;set the end-date to the same date for view reasons + (reset! fsm "pick-end")))) ;we are next picking an end date + +(defn- class-for-td + "Given a date, and the values in the internal model, determine which css class the :td should have" + [day start-date end-date temp-end disabled? selectable-fn minimum maximum show-today?] + (cond + (and @start-date (cljs-time/equal? day @start-date)) "daterange-start-td" + (and @start-date (cljs-time/equal? day @end-date)) "daterange-end-td" + (and @start-date (not= day "") (cljs-time/before? day @end-date) (cljs-time/after? day @start-date)) "daterange-interval-td" + (when minimum (cljs-time/before? day (deref-or-value minimum))) "daterange-disabled-td" + (when maximum (cljs-time/after? day (deref-or-value maximum))) "daterange-disabled-td" + disabled? "daterange-disabled-td" + (when selectable-fn (not (selectable-fn day))) "daterange-disabled-td" + (and @start-date (not= day "") (cljs-time/equal? @end-date @start-date) (cljs-time/before? day (plus-day @temp-end)) (cljs-time/after? day @start-date)) "daterange-temp-td" ;changed to fix flashing + (and show-today? (cljs-time/equal? day (now->utc))) "daterange-today" + :default "daterange-default-td")) + +(defn- create-day-td + "Create table data elements with reactive classes and on click/hover handlers" + [day [fsm start-date end-date temp-end] {:keys [on-change disabled? selectable-fn minimum maximum show-today? check-interval? parts] :as args}] + (let [disabled-data (vector minimum maximum disabled? selectable-fn)] + (if (= day "") [:td ""] + (let [correct-class (class-for-td day start-date end-date temp-end disabled? selectable-fn minimum maximum show-today?) + clickable? (not (date-disabled? day disabled-data))] + (into [:td] + (vector (merge {:class (str "rc-daterange-td-basic " correct-class (get-in parts [:date :class])) + :style (get-in parts [:date :style]) + :on-click #(when clickable? (td-click-handler day [fsm start-date end-date] on-change check-interval? disabled-data)) + :on-mouse-enter #(reset! temp-end day)} + (get-in parts [:date :attr])) + (str (cljs-time/day day)))))))) + +(defn week-td [week-number] + [:td {:class (str "daterange-td-basic " "daterange-week")} week-number]) + +(defn week-of-year-calc [days-list] + (cljs-time/week-number-of-year (last days-list))) + +(defn- create-week-tr + "Given a list of days, create a table row with each :td referring to a different day" + [days-list atoms {:keys [show-weeks?] :as args}] + (let [week-of-year (week-of-year-calc days-list)] + (into (if show-weeks? [:tr (week-td week-of-year)] [:tr]) + (for [day days-list] + [create-day-td day atoms args])))) + +(defn- parse-date-from-ints + "Given 3 ints, parse them as a useable date-format e.g. 11 2 2021" + [d m y] + (parse (formatter "ddMMYYYY") (str (format "%02d" d) (format "%02d" m) (str y)))) + +(defn- empty-days-count + "Returns the number of empty date tiles at the start of the month based on the first day of the month and the chosen week start day, monday = 1 sunday = 7" + [chosen start] + (let [chosen (if chosen chosen 1)] ;default week start of monday + (if (> chosen start) + (- 7 (- chosen start)) + (- start chosen)))) + +(defn- days-for-month + "Produces a partitioned list of date-formats with all the days in the given month, with leading empty strings to align with the days of the week" + [date-from-month start-of-week] + (let [month (cljs-time/month date-from-month) + year (cljs-time/year date-from-month) + last-day-of-month (cljs-time/day (cljs-time/last-day-of-the-month date-from-month)) + first-day-val (cljs-time/day-of-week (cljs-time/first-day-of-the-month date-from-month)) ;; 1 = mon 7 = sun + day-ints (range 1 (inc last-day-of-month)) ;; e.g. (1 2 3 ... 31) + days (map #(parse-date-from-ints % month year) day-ints) ;; turn into real date-times + with-lead-emptys (flatten (cons (repeat (empty-days-count start-of-week first-day-val) "") days))] ;; for padding the table + (partition-all 7 with-lead-emptys))) ;; split into lists of 7 to be passed to create-week-tr + +(def days-vec [[:td "M"] [:td "Tu"] [:td "W"] [:td "Th"] [:td "F"] [:td "Sa"] [:td "Su"]]) ;for cycling and display depending on start-of-week + +(defn- create-table + "Given the result from days-for-month for a given month, create the :tbody using the relevant :tr and :td functions above" + [date atoms {:keys [start-of-week i18n parts show-weeks?] :as args}] + (let [into-tr (if show-weeks? [:tr [:td]] [:tr]) + days-of-week (if (:days i18n) + (map (fn [new-day [td _]] [td new-day]) (:days i18n) days-vec) ;update days vec with the changed days + days-vec) + add-parts (fn [[td day-string]] + (vector td (merge {:class (str "daterange-day-title" (get-in parts [:day-title :class])) + :style (get-in parts [:day-title :style])} + (get-in parts [:day-title :attr])) + day-string)) + with-parts (map #(add-parts %) days-of-week) + table-row-weekdays (into into-tr (take 7 (drop (dec start-of-week) (cycle with-parts)))) + + partitioned-days (days-for-month date start-of-week) + date-rows (for [x partitioned-days] + [create-week-tr x atoms args]) + + with-weekdays-row (into [:tbody table-row-weekdays]) + with-dates (into with-weekdays-row date-rows)] + [:table + (merge {:class (str "rc-daterange-table" (get-in parts [:table :class])) + :style (get-in parts [:table :style])} + (get-in parts [:table :attr])) + with-dates])) + +(defn- model-changed? + "takes two date ranges and checks if they are different" + [old latest] + (not (and + (nil? latest) + (cljs-time/equal? (:start old) (:start latest)) + (cljs-time/equal? (:end old) (:end latest))))) + +(defn model? + "useless" + [{:keys [start end]}] + (and (date-like? start) (date-like? end)) + true) + +;for validation and demo +(def daterange-parts-desc + (when include-args-desc? + [{:name :wrapper :level 0 :class "rc-daterange-wrapper" :impl "[date-range]" :notes "Outer wrapper of the date-range picker."} ;seems this isn't a used accessor, even in datepicker? + {:name :border :level 1 :class "rc-daterange-border" :impl "[border]" :notes "The border."} + {:type :legacy :level 2 :class "rc-daterange" :impl "[:div]" :notes "The daterange container." :name-label "-"} + {:type :legacy :level 3 :impl "[h-box]" :notes "To display hozitonally." :name-label "-"} + + {:type :legacy :level 4 :impl "[v-box]" :notes "To contain the left side of the display." :name-label "-"} + {:name :prev-nav :level 5 :class "rc-daterange-prev-nav" :impl "[h-box]" :notes "Contains navigation buttons and month/year."} + {:name :prev-year :level 6 :class "rc-daterange-nav-button" :impl "[box]" :notes "Previous year button."} + {:name :prev-year-icon :level 7 :class "rc-daterange-nav-icon" :impl "[:svg]" :notes "Previous year icon."} + {:name :prev-month :level 6 :class "rc-daterange-nav-button" :impl "[box]" :notes "Previous month button."} + {:name :prev-month-icon :level 7 :class "rc-daterange-nav-icon" :impl "[:svg]" :notes "Previous month icon."} + {:name :month-title :level 6 :class "rc-daterange-month-title" :impl "[box]" :notes "Month title for both sides."} + {:name :year-title :level 6 :class "rc-daterange-year-title" :impl "[box]" :notes "Year title for both sides."} + {:name :table :level 5 :class "rc-daterange-table" :impl "[:table]" :notes "Table."} + {:type :legacy :level 6 :impl "[:tr]" :notes "Row containing day titles." :name-label "-"} + {:name :day-title :level 7 :class "rc-daterange-day-title" :impl "[:td]" :notes "Titles for columns, days of the week"} + {:name :date :level 7 :class "rc-daterange-td-basic" :impl "[:td]" :notes "The date tiles populating the table."} + + {:type :legacy :level 4 :impl "[v-box]" :notes "To contain the right side of the display." :name-label "-"} + {:name :next-nav :level 5 :class "rc-daterange-next-nav" :impl "[h-box]" :notes "Contains navigation buttons and month/year."} + {:name :next-month :level 6 :class "rc-daterange-nav-button" :impl "[box]" :notes "Next month button."} + {:name :next-month-icon :level 7 :class "rc-daterange-nav-icon" :impl "[:svg]" :notes "Next month icon."} + {:name :next-year :level 6 :class "rc-daterange-nav-button" :impl "[:box]" :notes "Next year button."} + {:name :next-year-icon :level 7 :class "rc-daterange-nav-icon" :impl "[:svg]" :notes "Next year icon."}])) + +(def daterange-parts + (when include-args-desc? + (set (map :name daterange-parts-desc)))) + +(def daterange-args-desc + "used to validate the arguments supplied by the user" + (when include-args-desc? + [{:name :model :required false :type "map with keys :start, :end | r/atom" :validate-fn model? :description "the selected date range. Only updates after a selection has been completed. A closed (inclusive) interval. A map containing :start and :end whose values must both satisfy DateTimeProtocol. Nil is also acceptable if you want to start with nothing selected"} + {:name :on-change :required true :type "satisfies DateTimeProtocol -> nil" :validate-fn fn? :description "called when a new complete selection has been made"} + {:name :disabled? :required false :default false :type "boolean | atom" :description "when true, the user can't select dates but can navigate"} + {:name :initial-display :required false :type "satisfies DateTimeProtocol | r/atom" :validate-fn date-like? :description "set the months shown when no model is selected, defaults to the current month"} + {:name :selectable-fn :required false :type "function" :validate-fn fn? :description "called on each date, if it returns false, that date is not selectable"} + {:name :show-today? :required false :default false :type "boolean" :description "when true, todays date is highlighted"} + {:name :minimum :required false :type "satisfies DateTimeProtocol | r/atom" :validate-fn date-like? :description "no selection before this date"} + {:name :maximum :required false :type "satisfies DateTimeProtocol | r/atom" :validate-fn date-like? :description "no selection after this date"} + {:name :check-interval? :required false :default false :type "boolean" :description "if true, the user cannot select ranges which contain disabled days. If false, ranges spanning deselected or disabled dates are valid"} + {:name :start-of-week :required false :default 1 :type "int" :validate-fn int? :description "choose left most column of the table, 1 = monday ... 7 = sunday"} + {:name :show-weeks? :required false :default false :type "boolean" :description "when true, week numbers are shown to the left"} + {:name :hide-border? :required false :type "boolean" :description "when true, the border is not displayed"} + {:name :i18n :required false :type "map" :validate-fn map? :description "internationalization map with optional keys :days and :months (both vectors of strings)"} + {:name :class :required false :type "string" :validate-fn string? :description "CSS class names, space separated (applies to the outer border div, not the wrapping div)"} + {:name :style :required false :type "CSS style map" :validate-fn css-style? :description "CSS styles to add or override (applies to the outer border div, not the wrapping div)"} + {:name :attr :required false :type "HTML attribute map" :validate-fn html-attr? :description "HTML attributes, like :on-mouse-move, No :class or :style allowed (applies to the outer border div, not the wrapping div)"} + {:name :parts :required false :type "map" :validate-fn (parts? daterange-parts) :description "See Parts section below."} + {:name :src :required false :type "map" :validate-fn map? :description [:span "Used in dev builds to assist with debugging. Source code coordinates map containing keys" [:code ":file"] "and" [:code ":line"] ". See 'Debugging'."]} + {:name :debug-as :required false :type "map" :validate-fn map? :description [:span "Used in dev builds to assist with debugging, when one component is used implement another component, and we want the implementation component to masquerade as the original component in debug output, such as component stacks. A map optionally containing keys" [:code ":component"] "and" [:code ":args"] "."]}])) + +(defn daterange + "Tracks the external model, but takes inputs into an internal model. The given on-change function is only called after a full selection has been made" + [& {:keys [model initial-display] :as args}] + (or + (validate-args-macro daterange-args-desc args) + (let [current-month (r/atom (or (deref-or-value initial-display) (now->utc))) + fsm (r/atom "pick-start") + start-date (r/atom (:start (deref-or-value model))) + end-date (r/atom (:end (deref-or-value model))) + temp-end (r/atom (now->utc))] ;for :on-hover css functionality + (fn render-fn + [& {:keys [model hide-border? i18n class style attr parts src debug-as] :as args}] + (or + (validate-args-macro daterange-args-desc args) ;re validate args each time they change + (let [latest-external-model (deref-or-value model) + internal-model-refernce {:start @start-date :end @end-date}] + (when (and (model-changed? latest-external-model internal-model-refernce) (= @fsm "pick-start")) + (reset! start-date (:start latest-external-model)) + (reset! end-date (:end latest-external-model))) + [main-div-with + [h-box :src (at) + :gap "60px" + :padding "15px" + :children [[v-box :src (at) + :gap "10px" + :children [[prev-nav current-month parts i18n] + [create-table @current-month [fsm start-date end-date temp-end] args]]] + [v-box :src (at) + :gap "10px" + :children [[next-nav current-month parts i18n] + [create-table (plus-month @current-month) [fsm start-date end-date temp-end] args]]]]] + hide-border? + class + style + attr + parts + src + (or debug-as (reflect-current-component))])))))) + +(defn- anchor-button + "Provide clickable field with current date label and dropdown button e.g. [ 2014 Sep 17 | # ]" + [shown? model format goog? placeholder width disabled?] + (let [format-str (if format format "dd MMM, yyyy")] + [:div {:class "rc-daterange-dropdown-anchor input-group display-flex noselect" + :style (flex-child-style "none") + :on-click (handler-fn + (when (not (deref-or-value disabled?)) + (swap! shown? not)))} + [h-box + :width (if width width "228px") + :children [[box + :size "auto" + :class (str "form-control dropdown-button" (when (deref-or-value disabled?) " dropdown-button-disabled")) + :style {:font-weight 600 :border-radius "5px 0px 0px 5px"} + :child (cond + (not (date-like? (:start (deref-or-value model)))) (do + (prn (:start (deref-or-value model))) + [:span {:style {:color "#bbb"}} placeholder]) + goog? (str + (.format (DateTimeFormat. (if (seq format) format format-str)) (:start (deref-or-value model))) + " - " + (.format (DateTimeFormat. (if (seq format) format format-str)) (:end (deref-or-value model)))) + + :else (str + (unparse (formatter format-str) (deref-or-value (:start (deref-or-value model)))) + " - " + (unparse (formatter format-str) (deref-or-value (:end (deref-or-value model))))))] + [h-box + :justify :around + :class (str "dropdown-button activator input-group-addon" (when (deref-or-value disabled?) " dropdown-button-disabled")) + :style {:padding "3px 0px 0px 0px" + :width "30px"} + :children [[:i.zmdi.zmdi-apps {:style {:font-size "24px"}}]]]]]])) + +(def daterange-dropdown-args-desc + (when include-args-desc? + (conj daterange-args-desc + {:name :format :required false :default "dd MMM, yyyy" :type "string" :description "[daterange-dropdown only] a representation of a date format. See cljs_time.format"} + {:name :goog? :required false :default false :type "boolean" :description [:span "[daterange only] use " [:code "goog.i18n.DateTimeFormat"] " instead of " [:code "cljs_time.format"] " for applying " [:code ":format"]]} + {:name :no-clip? :required false :default true :type "boolean" :description "[daterange-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"} + {:name :placeholder :required false :type "string" :description "[daterange-dropdown only] placeholder text for when a date is not selected."} + {:name :width :required false :validate-fn string? :type "string" :description "[daterange-dropdown only] a CSS width style"} + {:name :position-offset :required false :validate-fn number? :type "integer" :description "[daterange-dropdown only] px horizontal offset of the popup"}))) + +(defn daterange-dropdown + [& {:keys [src] :as args}] + (or + (validate-args-macro daterange-dropdown-args-desc args) + (let [shown? (r/atom false) + cancel-popover #(reset! shown? false) + position :below-left] + (fn render-fn + [& {:keys [model show-weeks? on-change format goog? no-clip? placeholder width disabled? position-offset src debug-as] + :or {no-clip? true, position-offset 0} + :as passthrough-args}] + (or + (validate-args-macro daterange-dropdown-args-desc 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 (-> passthrough-args + (dissoc :format :goog? :no-clip? :placeholder :width :position-offset) ;; these keys only valid at this API level + (assoc :on-change collapse-on-select) + (assoc :src (at)) + (merge {:hide-border? true}) ;; apply defaults + vec + flatten)] + [popover-anchor-wrapper + :src src + :debug-as (or debug-as (reflect-current-component)) + :class "rc-daterange-dropdown-wrapper" + :showing? shown? + :position position + :anchor [anchor-button shown? model format goog? placeholder width disabled?] + :popover [popover-content-wrapper + :src (at) + :position-offset (+ (if show-weeks? 87 88) position-offset) + :no-clip? no-clip? + :arrow-length 0 + :arrow-width 0 + :arrow-gap 3 + :padding "0px" + :on-cancel cancel-popover + :body (into [daterange] passthrough-args)]])))))) + + + + + diff --git a/src/re_demo/core.cljs b/src/re_demo/core.cljs index 87819246..5b384936 100644 --- a/src/re_demo/core.cljs +++ b/src/re_demo/core.cljs @@ -37,6 +37,7 @@ [re-demo.tabs :as tabs] [re-demo.popovers :as popovers] [re-demo.datepicker :as datepicker] + [re-demo.daterange :as daterange] [re-demo.selection-list :as selection-list] [re-demo.input-time :as input-time] [re-demo.layout :as layout] @@ -80,6 +81,7 @@ {:id :progress-bar :level :minor :label "Progress Bar" :panel progress-bar/panel} {:id :throbber :level :minor :label "Throbber" :panel throbber/panel} {:id :date :level :minor :label "Date Picker" :panel datepicker/panel} + {:id :daterange :level :minor :label "Date Range Picker" :panel daterange/panel} {:id :time :level :minor :label "Input Time" :panel input-time/panel} {:id :selection :level :major :label "Selection"} diff --git a/src/re_demo/datepicker.cljs b/src/re_demo/datepicker.cljs index b9ed3336..0c2c4e4f 100644 --- a/src/re_demo/datepicker.cljs +++ b/src/re_demo/datepicker.cljs @@ -1,24 +1,24 @@ (ns re-demo.datepicker (:require-macros - [reagent.ratom :refer [reaction]] - [re-com.core :refer []]) + [reagent.ratom :refer [reaction]] + [re-com.core :refer []]) (:require - [goog.date.Date] - [reagent.core :as reagent] - [cljs-time.core :refer [today days minus plus day-of-week before?]] - [cljs-time.coerce :refer [to-local-date]] - [cljs-time.format :refer [formatter unparse]] - [re-com.core :refer [at h-box v-box box gap single-dropdown datepicker datepicker-dropdown checkbox label title p button md-icon-button]] - [re-com.datepicker :refer [iso8601->date datepicker-parts-desc datepicker-dropdown-args-desc]] - [re-com.validate :refer [date-like?]] - [re-com.util :refer [now->utc px]] - [re-demo.utils :refer [panel-title title2 title3 parts-table args-table github-hyperlink status-text]]) + [goog.date.Date] + [reagent.core :as reagent] + [cljs-time.core :refer [today days minus plus day-of-week before?]] + [cljs-time.coerce :refer [to-local-date]] + [cljs-time.format :refer [formatter unparse]] + [re-com.core :refer [at h-box v-box box gap single-dropdown datepicker datepicker-dropdown checkbox label title p button md-icon-button]] + [re-com.datepicker :refer [iso8601->date datepicker-parts-desc datepicker-dropdown-args-desc]] + [re-com.validate :refer [date-like?]] + [re-com.util :refer [now->utc px]] + [re-demo.utils :refer [panel-title title2 title3 parts-table args-table github-hyperlink status-text]]) (:import - [goog.i18n DateTimeSymbols_pl])) + [goog.i18n DateTimeSymbols_pl])) (def ^:private days-map - {:Su "S" :Mo "M" :Tu "T" :We "W" :Th "T" :Fr "F" :Sa "S"}) + {:Su "S" :Mo "M" :Tu "T" :We "W" :Th "T" :Fr "F" :Sa "S"}) (defn- toggle-inclusion! @@ -315,4 +315,4 @@ ;; core holds a reference to panel, so need one level of indirection to get figwheel updates (defn panel [] - [datepicker-examples]) + [datepicker-examples]) \ No newline at end of file diff --git a/src/re_demo/daterange.cljs b/src/re_demo/daterange.cljs new file mode 100644 index 00000000..c3e89c11 --- /dev/null +++ b/src/re_demo/daterange.cljs @@ -0,0 +1,156 @@ +(ns re-demo.daterange + (:require-macros + [reagent.ratom :refer [reaction]] + [re-com.core :refer []]) + (:require + [goog.date.Date] + [reagent.core :as reagent] + [cljs-time.core :refer [today days minus plus day-of-week before?]] + [cljs-time.coerce :refer [to-local-date]] + [cljs-time.format :refer [formatter unparse]] + [re-com.core :refer [at h-box v-box box gap single-dropdown datepicker datepicker-dropdown checkbox label title p button md-icon-button checkbox]] + [re-com.datepicker :refer [iso8601->date datepicker-parts-desc datepicker-dropdown-args-desc]] + [re-com.daterange :as daterange :refer [daterange daterange-args-desc daterange-parts-desc daterange-dropdown-args-desc daterange-dropdown]] + [re-com.validate :refer [date-like?]] + [re-com.util :refer [now->utc px]] + [cljs-time.core :as cljs-time] + [re-demo.utils :refer [panel-title title2 title3 parts-table args-table github-hyperlink status-text]]) + (:import + [goog.i18n DateTimeSymbols_pl])) + +(def week-start-choices + [{:id 1 :label "Monday"} + {:id 2 :label "Tuesday"} + {:id 3 :label "Wednesday"} + {:id 4 :label "Thursday"} + {:id 5 :label "Friday"} + {:id 6 :label "Saturday"} + {:id 7 :label "Sunday"}]) + +(defn create-checkbox [atom day] + [v-box + :align :center + :children [[box :style {:font-size "smaller"} :child day] + [checkbox + :model ((keyword day) @atom) + :on-change #(swap! atom update-in [(keyword day)] not)]]]) + +(defn holder [] + (let [dropdown-model (reagent/atom nil) + model-atom (reagent/atom nil) + today-model (reagent/atom false) + disabled-model (reagent/atom false) + weeks-model (reagent/atom false) + interval-model (reagent/atom false) + week-start-model (reagent/atom 2) + selected-days (reagent/atom {:M true :Tu true :W true :Th true :Fr true :Sa true :Su true}) ;model for all checkboxes + valid? (fn [day] (nth (mapv val @selected-days) (dec (cljs-time/day-of-week day))))] ;convert to vector, check if day should be disabled + (fn [] + [v-box + :gap "10px" + :children [[panel-title "[daterange ...]" + "src/re_com/datepicker.cljs" + "src/re_demo/datepicker.cljs"] + [h-box + :gap "100px" + :children [[v-box + :gap "10px" + :width "450px" + :children [[title2 "Notes"] + [status-text "Alpha, written by an intern"] + [p "A date range picker component."] + [args-table daterange-dropdown-args-desc]]] + [v-box + :gap "15px" + :children [[title2 "Demo"] + [daterange + :show-today? @today-model + :disabled? @disabled-model + :show-weeks? @weeks-model + :check-interval? @interval-model + :model model-atom + :selectable-fn valid? + :start-of-week @week-start-model + :on-change #(reset! model-atom %)] + [h-box + :align :center + :children [[:code ":model"] + [box :child (str " is " + (if @model-atom (str + (unparse (formatter "dd MMM, yyyy") (:start @model-atom)) " ... " + (unparse (formatter "dd MMM, yyyy") (:end @model-atom))) "nil"))]]] + [v-box + :src (at) + :gap "10px" + :style {:min-width "550px" + :padding "15px" + :border-top "1px solid #DDD" + :background-color "#f7f7f7"} + :children [[title + :src (at) + :style {:margin-top "0"} + :level :level3 :label "Interactive Parameters"] + [checkbox :src (at) + :model disabled-model + :on-change #(swap! disabled-model not) + :label [box :child [:code ":disabled?"]]] + [checkbox :src (at) + :model today-model + :on-change #(swap! today-model not) + :label [box :child [:code ":show-today?"]]] + [checkbox :src (at) + :model weeks-model + :on-change #(swap! weeks-model not) + :label [box :child [:code ":show-weeks?"]]] + [h-box + :gap "5px" + :align :end + :children [[box :child [:code ":start-of-week"]] + [single-dropdown + :width "110px" + :choices week-start-choices + :model week-start-model + :on-change #(reset! week-start-model %)]]] + [h-box + :gap "5px" + :align :end + :children [[box :child [:code ":selectable-fn"]] + [create-checkbox selected-days "M"] + [create-checkbox selected-days "Tu"] + [create-checkbox selected-days "W"] + [create-checkbox selected-days "Th"] + [create-checkbox selected-days "Fr"] + [create-checkbox selected-days "Sa"] + [create-checkbox selected-days "Su"]]] + [gap :size "5px"] + [checkbox + :model interval-model + :on-change #(swap! interval-model not) + :label [box :child [:code "check-interval?"]]]]] + [v-box + :align :start + :gap "10px" + :children [[title + :src (at) + :level :level3 :label "Dropdown"] + [box :src (at) + :child "Attached to the same model and interactive paramters."] + [daterange-dropdown + :show-today? @today-model + :disabled? @disabled-model + :show-weeks? @weeks-model + :check-interval? @interval-model + :model model-atom + :selectable-fn valid? + :start-of-week @week-start-model + :on-change #(reset! model-atom %) + :placeholder "Select a range of dates"]]]]]]] + [parts-table "daterange" daterange-parts-desc]]]))) + + + + + +(defn panel + [] + [holder]) \ No newline at end of file