date_picker.cljs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. (ns frontend.ui.date-picker
  2. (:require
  3. [rum.core :as rum]
  4. [cljs-time.core :refer [now today minus plus months days weeks year month day day-of-week first-day-of-the-month before? after?]]
  5. [cljs-time.predicates :refer [sunday?]]
  6. [cljs-time.format :refer [parse unparse formatters formatter]]
  7. [frontend.util :refer [deref-or-value now->utc]]
  8. [frontend.modules.shortcut.core :as shortcut]
  9. [frontend.util :as util]
  10. [frontend.state :as state]
  11. [goog.object :as gobj]))
  12. ;; Adapted from re-com date-picker
  13. ;; TODO: add left, right, up, down, enter bindings
  14. ;; Loosely based on ideas: https://github.com/dangrossman/bootstrap-daterangepicker
  15. ;; --- cljs-time facades ------------------------------------------------------
  16. (def ^:const month-format (formatter "MMMM yyyy"))
  17. (def ^:const week-format (formatter "ww"))
  18. (def ^:const date-format (formatter "yyyy MMM dd"))
  19. (defn iso8601->date [iso8601]
  20. (when (seq iso8601)
  21. (parse (formatters :basic-date) iso8601)))
  22. (defn- month-label [date] (unparse month-format date))
  23. (defn- dec-month [date] (minus date (months 1)))
  24. (defn- inc-month [date] (plus date (months 1)))
  25. (defn- inc-date [date n] (plus date (days n)))
  26. (defn- inc-week [date n] (plus date (weeks n)))
  27. (defn previous
  28. "If date fails pred, subtract period until true, otherwise answer date"
  29. ;; date - a date object that satisfies cljs-time.core/DateTimeProtocol.
  30. ;; If omitted, use now->utc, which returns a goog.date.UtcDateTime version of now with time removed.
  31. ;; pred - can be one of cljs-time.predicate e.g. sunday? but any valid pred is supported.
  32. ;; period - a period which will be subtracted see cljs-time.core periods
  33. ;; Note: If period and pred do not represent same granularity, some steps may be skipped
  34. ; e.g Pass a Wed date, specify sunday? as pred and a period (days 2) will skip one Sunday.
  35. ([pred]
  36. (previous pred (now->utc)))
  37. ([pred date]
  38. (previous pred date (days 1)))
  39. ([pred date period]
  40. (if (pred date)
  41. date
  42. (recur pred (minus date period) period))))
  43. (defn- =date [date1 date2]
  44. (and
  45. (= (year date1) (year date2))
  46. (= (month date1) (month date2))
  47. (= (day date1) (day date2))))
  48. (defn- <=date [date1 date2]
  49. (or (=date date1 date2) (before? date1 date2)))
  50. (defn- >=date [date1 date2]
  51. (or (=date date1 date2) (after? date1 date2)))
  52. (def ^:private days-vector
  53. [{:key :Mo :short-name "M" :name "MON"}
  54. {:key :Tu :short-name "T" :name "TUE"}
  55. {:key :We :short-name "W" :name "WED"}
  56. {:key :Th :short-name "T" :name "THU"}
  57. {:key :Fr :short-name "F" :name "FRI"}
  58. {:key :Sa :short-name "S" :name "SAT"}
  59. {:key :Su :short-name "S" :name "SUN"}])
  60. (defn- rotate
  61. [n coll]
  62. (let [c (count coll)]
  63. (take c (drop (mod n c) (cycle coll)))))
  64. (defn- is-day-pred [d]
  65. #(= (day-of-week %) (inc d)))
  66. ;; ----------------------------------------------------------------------------
  67. (def *internal-model (rum/cursor state/state :date-picker/date))
  68. (defn- main-div-with
  69. [table-div class style attr]
  70. [:div.rc-datepicker-wrapper
  71. [:div {:style {:border-radius 4}}
  72. [:div (merge
  73. {:class (str "rc-datepicker datepicker noselect " class)
  74. :style (merge {:font-size "13px"
  75. :position "static"}
  76. style)}
  77. attr)
  78. table-div]]])
  79. (rum/defc table-thead
  80. "Answer 2 x rows showing month with nav buttons and days NOTE: not internationalized"
  81. [display-month {show-weeks? :show-weeks? minimum :minimum maximum :maximum start-of-week :start-of-week}]
  82. (let [prev-date (dec-month display-month)
  83. minimum (deref-or-value minimum)
  84. maximum (deref-or-value maximum)
  85. prev-enabled? (if minimum (after? prev-date (dec-month minimum)) true)
  86. next-date (inc-month display-month)
  87. next-enabled? (if maximum (before? next-date maximum) true)
  88. template-row (if show-weeks? [:tr [:th]] [:tr])]
  89. [:thead
  90. (conj template-row
  91. [:th {:class (str "prev " (if prev-enabled? "available selectable" "disabled"))
  92. :style {:padding "0px"}
  93. :on-click #(when prev-enabled? (reset! *internal-model prev-date))}
  94. [:span.font-bold "<"]]
  95. [:th {:class "month" :col-span "5"} (month-label display-month)]
  96. [:th {:class (str "next " (if next-enabled? "available selectable" "disabled"))
  97. :style {:padding "0px"}
  98. :on-click #(when next-enabled? (reset! *internal-model next-date))}
  99. [:span.font-bold ">"]])
  100. (conj template-row
  101. (for [day (rotate start-of-week days-vector)]
  102. ^{:key (:key day)} [:th {:class "day-enabled"} (str (:name day))]))]))
  103. (defn- table-td
  104. [date focus-month selected today {minimum :minimum maximum :maximum :as attributes} disabled? on-change]
  105. ;;following can be simplified and terse
  106. (let [minimum (deref-or-value minimum)
  107. maximum (deref-or-value maximum)
  108. enabled-min (if minimum (>=date date minimum) true)
  109. enabled-max (if maximum (<=date date maximum) true)
  110. enabled-day (and enabled-min enabled-max)
  111. disabled-day? (if enabled-day
  112. (not ((:selectable-fn attributes) date))
  113. true)
  114. classes (cond disabled? "off"
  115. disabled-day? "off"
  116. (= focus-month (month date)) "available"
  117. :else "available off")
  118. classes (cond (and selected (=date selected date)) (str classes " active start-date end-date")
  119. (and today (=date date today)) (str classes " today")
  120. :else classes)
  121. on-click (fn [e]
  122. (when-not (or disabled? disabled-day?)
  123. (reset! *internal-model date)
  124. (on-change e date)))]
  125. [:td {:class classes
  126. :on-click on-click} (day date)]))
  127. (defn- week-td [date]
  128. [:td {:class "week"} (unparse week-format date)])
  129. (defn- table-tr
  130. "Return 7 columns of date cells from date inclusive"
  131. [date focus-month selected attributes disabled? on-change]
  132. ; {:pre [(sunday? date)]}
  133. (let [table-row (if (:show-weeks? attributes) [:tr (week-td date)] [:tr])
  134. row-dates (map #(inc-date date %) (range 7))
  135. today (when (:show-today? attributes) (now->utc))]
  136. (into table-row (map #(table-td % focus-month selected today attributes disabled? on-change) row-dates))))
  137. (rum/defc table-tbody
  138. "Return matrix of 6 rows x 7 cols table cells representing 41 days from start-date inclusive"
  139. [display-month selected attributes disabled? on-change]
  140. (let [start-of-week (:start-of-week attributes)
  141. current-start (previous (is-day-pred start-of-week) display-month)
  142. focus-month (month display-month)
  143. row-start-dates (map #(inc-date current-start (* 7 %)) (range 6))]
  144. (into [:tbody] (map #(table-tr % focus-month selected attributes disabled? on-change) row-start-dates))))
  145. (defn- configure
  146. "Augment passed attributes with extra info/defaults"
  147. [attributes]
  148. (let [selectable-fn (if (-> attributes :selectable-fn fn?)
  149. (:selectable-fn attributes)
  150. (constantly true))]
  151. (merge attributes {:selectable-fn selectable-fn})))
  152. (rum/defc date-picker < rum/reactive
  153. {:init (fn [state]
  154. (reset! *internal-model (first (:rum/args state)))
  155. state)}
  156. (shortcut/mixin :shortcut.handler/date-picker)
  157. [model {:keys [on-change on-switch disabled? start-of-week class style attr]
  158. :or {start-of-week (state/get-start-of-week)} ;; Default to Sunday
  159. :as args}]
  160. (let [internal-model (util/react *internal-model)
  161. display-month (first-day-of-the-month (or internal-model (now->utc)))
  162. props-with-defaults (merge args {:start-of-week start-of-week})
  163. configuration (configure props-with-defaults)]
  164. (main-div-with
  165. [:table.table-auto {:class "table-condensed"}
  166. (table-thead display-month configuration)
  167. (table-tbody display-month internal-model configuration disabled? on-change)]
  168. class
  169. style
  170. attr)))