@ohos-ports/flowbite-datepicker 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.js +73 -0
  3. package/.stylelintrc +12 -0
  4. package/CHANGELOG.md +72 -0
  5. package/LICENSE +21 -0
  6. package/README.md +71 -0
  7. package/demo/index.html +582 -0
  8. package/demo/live-demo.js +471 -0
  9. package/dist/DateRangePicker.cjs.js +2996 -0
  10. package/dist/DateRangePicker.esm.js +2994 -0
  11. package/dist/DateRangePicker.umd.js +3002 -0
  12. package/dist/Datepicker.cjs.js +2778 -0
  13. package/dist/Datepicker.esm.js +2776 -0
  14. package/dist/Datepicker.umd.js +2784 -0
  15. package/dist/css/datepicker-bs4.css +276 -0
  16. package/dist/css/datepicker-bs4.min.css +1 -0
  17. package/dist/css/datepicker-bs5.css +276 -0
  18. package/dist/css/datepicker-bs5.min.css +1 -0
  19. package/dist/css/datepicker-bulma.css +258 -0
  20. package/dist/css/datepicker-bulma.min.css +1 -0
  21. package/dist/css/datepicker-foundation.css +262 -0
  22. package/dist/css/datepicker-foundation.min.css +1 -0
  23. package/dist/css/datepicker.css +306 -0
  24. package/dist/css/datepicker.min.css +1403 -0
  25. package/dist/js/datepicker-full.js +2777 -0
  26. package/dist/js/datepicker-full.min.js +1 -0
  27. package/dist/js/datepicker.js +2569 -0
  28. package/dist/js/datepicker.min.js +1 -0
  29. package/dist/js/locales/ar-tn.js +15 -0
  30. package/dist/js/locales/ar.js +15 -0
  31. package/dist/js/locales/az.js +14 -0
  32. package/dist/js/locales/bg.js +14 -0
  33. package/dist/js/locales/bm.js +18 -0
  34. package/dist/js/locales/bn.js +19 -0
  35. package/dist/js/locales/br.js +18 -0
  36. package/dist/js/locales/bs.js +15 -0
  37. package/dist/js/locales/ca.js +18 -0
  38. package/dist/js/locales/cs.js +19 -0
  39. package/dist/js/locales/cy.js +14 -0
  40. package/dist/js/locales/da.js +19 -0
  41. package/dist/js/locales/de.js +18 -0
  42. package/dist/js/locales/el.js +16 -0
  43. package/dist/js/locales/en-AU.js +18 -0
  44. package/dist/js/locales/en-CA.js +18 -0
  45. package/dist/js/locales/en-GB.js +18 -0
  46. package/dist/js/locales/en-IE.js +17 -0
  47. package/dist/js/locales/en-NZ.js +17 -0
  48. package/dist/js/locales/en-ZA.js +17 -0
  49. package/dist/js/locales/eo.js +17 -0
  50. package/dist/js/locales/es.js +18 -0
  51. package/dist/js/locales/et.js +18 -0
  52. package/dist/js/locales/eu.js +18 -0
  53. package/dist/js/locales/fa.js +17 -0
  54. package/dist/js/locales/fi.js +17 -0
  55. package/dist/js/locales/fo.js +15 -0
  56. package/dist/js/locales/fr-CH.js +21 -0
  57. package/dist/js/locales/fr.js +18 -0
  58. package/dist/js/locales/gl.js +16 -0
  59. package/dist/js/locales/he.js +15 -0
  60. package/dist/js/locales/hi.js +18 -0
  61. package/dist/js/locales/hr.js +13 -0
  62. package/dist/js/locales/hu.js +18 -0
  63. package/dist/js/locales/hy.js +18 -0
  64. package/dist/js/locales/id.js +15 -0
  65. package/dist/js/locales/is.js +14 -0
  66. package/dist/js/locales/it-CH.js +20 -0
  67. package/dist/js/locales/it.js +18 -0
  68. package/dist/js/locales/ja.js +17 -0
  69. package/dist/js/locales/ka.js +17 -0
  70. package/dist/js/locales/kk.js +15 -0
  71. package/dist/js/locales/km.js +15 -0
  72. package/dist/js/locales/ko.js +18 -0
  73. package/dist/js/locales/lt.js +19 -0
  74. package/dist/js/locales/lv.js +18 -0
  75. package/dist/js/locales/me.js +17 -0
  76. package/dist/js/locales/mk.js +15 -0
  77. package/dist/js/locales/mn.js +17 -0
  78. package/dist/js/locales/mr.js +18 -0
  79. package/dist/js/locales/ms.js +15 -0
  80. package/dist/js/locales/nl-BE.js +18 -0
  81. package/dist/js/locales/nl.js +18 -0
  82. package/dist/js/locales/no.js +18 -0
  83. package/dist/js/locales/oc.js +17 -0
  84. package/dist/js/locales/pl.js +17 -0
  85. package/dist/js/locales/pt-BR.js +17 -0
  86. package/dist/js/locales/pt.js +18 -0
  87. package/dist/js/locales/ro.js +17 -0
  88. package/dist/js/locales/ru.js +18 -0
  89. package/dist/js/locales/si.js +18 -0
  90. package/dist/js/locales/sk.js +18 -0
  91. package/dist/js/locales/sl.js +15 -0
  92. package/dist/js/locales/sq.js +18 -0
  93. package/dist/js/locales/sr-latn.js +16 -0
  94. package/dist/js/locales/sr.js +16 -0
  95. package/dist/js/locales/sv.js +17 -0
  96. package/dist/js/locales/sw.js +15 -0
  97. package/dist/js/locales/ta.js +18 -0
  98. package/dist/js/locales/tg.js +19 -0
  99. package/dist/js/locales/th.js +14 -0
  100. package/dist/js/locales/tk.js +18 -0
  101. package/dist/js/locales/tr.js +17 -0
  102. package/dist/js/locales/uk.js +17 -0
  103. package/dist/js/locales/uz-cyrl.js +18 -0
  104. package/dist/js/locales/uz-latn.js +18 -0
  105. package/dist/js/locales/vi.js +16 -0
  106. package/dist/js/locales/zh-CN.js +19 -0
  107. package/dist/js/locales/zh-TW.js +18 -0
  108. package/dist/locales/index.cjs.js +1028 -0
  109. package/dist/locales/index.esm.js +1028 -0
  110. package/dist/main.cjs.js +2999 -0
  111. package/dist/main.esm.js +2994 -0
  112. package/dist/main.umd.js +3005 -0
  113. package/docs/.nojekyll +0 -0
  114. package/docs/README.md +266 -0
  115. package/docs/_sidebar.md +6 -0
  116. package/docs/api.md +284 -0
  117. package/docs/date-string+format.md +90 -0
  118. package/docs/i18n.md +99 -0
  119. package/docs/images/datepicker-inline.jpg +0 -0
  120. package/docs/images/datepicker.jpg +0 -0
  121. package/docs/images/multidate.jpg +0 -0
  122. package/docs/images/picker-structure.jpg +0 -0
  123. package/docs/images/rangepicker.jpg +0 -0
  124. package/docs/images/view-days.jpg +0 -0
  125. package/docs/images/view-decades.jpg +0 -0
  126. package/docs/images/view-months.jpg +0 -0
  127. package/docs/images/view-years.jpg +0 -0
  128. package/docs/index.html +26 -0
  129. package/docs/options.md +354 -0
  130. package/docs/overview.md +174 -0
  131. package/extras/twoDigitToFullYear/test/twoDigitToFullYear.js +58 -0
  132. package/extras/twoDigitToFullYear/twoDigitToFullYear.js +26 -0
  133. package/js/DateRangePicker.js +210 -0
  134. package/js/Datepicker.js +487 -0
  135. package/js/datepicker-full.js +5 -0
  136. package/js/events/functions.js +48 -0
  137. package/js/events/inputFieldListeners.js +206 -0
  138. package/js/events/otherListeners.js +15 -0
  139. package/js/events/pickerListeners.js +70 -0
  140. package/js/i18n/base-locales.js +13 -0
  141. package/js/i18n/locales/ar-tn.js +15 -0
  142. package/js/i18n/locales/ar.js +15 -0
  143. package/js/i18n/locales/az.js +14 -0
  144. package/js/i18n/locales/bg.js +14 -0
  145. package/js/i18n/locales/bm.js +18 -0
  146. package/js/i18n/locales/bn.js +19 -0
  147. package/js/i18n/locales/br.js +18 -0
  148. package/js/i18n/locales/bs.js +15 -0
  149. package/js/i18n/locales/ca.js +18 -0
  150. package/js/i18n/locales/cs.js +19 -0
  151. package/js/i18n/locales/cy.js +14 -0
  152. package/js/i18n/locales/da.js +19 -0
  153. package/js/i18n/locales/de.js +18 -0
  154. package/js/i18n/locales/el.js +16 -0
  155. package/js/i18n/locales/en-AU.js +18 -0
  156. package/js/i18n/locales/en-CA.js +18 -0
  157. package/js/i18n/locales/en-GB.js +18 -0
  158. package/js/i18n/locales/en-IE.js +17 -0
  159. package/js/i18n/locales/en-NZ.js +17 -0
  160. package/js/i18n/locales/en-ZA.js +17 -0
  161. package/js/i18n/locales/eo.js +17 -0
  162. package/js/i18n/locales/es.js +18 -0
  163. package/js/i18n/locales/et.js +18 -0
  164. package/js/i18n/locales/eu.js +18 -0
  165. package/js/i18n/locales/fa.js +17 -0
  166. package/js/i18n/locales/fi.js +17 -0
  167. package/js/i18n/locales/fo.js +15 -0
  168. package/js/i18n/locales/fr-CH.js +21 -0
  169. package/js/i18n/locales/fr.js +18 -0
  170. package/js/i18n/locales/gl.js +16 -0
  171. package/js/i18n/locales/he.js +15 -0
  172. package/js/i18n/locales/hi.js +18 -0
  173. package/js/i18n/locales/hr.js +13 -0
  174. package/js/i18n/locales/hu.js +18 -0
  175. package/js/i18n/locales/hy.js +18 -0
  176. package/js/i18n/locales/id.js +15 -0
  177. package/js/i18n/locales/is.js +14 -0
  178. package/js/i18n/locales/it-CH.js +20 -0
  179. package/js/i18n/locales/it.js +18 -0
  180. package/js/i18n/locales/ja.js +17 -0
  181. package/js/i18n/locales/ka.js +17 -0
  182. package/js/i18n/locales/kk.js +15 -0
  183. package/js/i18n/locales/km.js +15 -0
  184. package/js/i18n/locales/ko.js +18 -0
  185. package/js/i18n/locales/lt.js +19 -0
  186. package/js/i18n/locales/lv.js +18 -0
  187. package/js/i18n/locales/me.js +17 -0
  188. package/js/i18n/locales/mk.js +15 -0
  189. package/js/i18n/locales/mn.js +17 -0
  190. package/js/i18n/locales/mr.js +18 -0
  191. package/js/i18n/locales/ms.js +15 -0
  192. package/js/i18n/locales/nl-BE.js +18 -0
  193. package/js/i18n/locales/nl.js +18 -0
  194. package/js/i18n/locales/no.js +18 -0
  195. package/js/i18n/locales/oc.js +17 -0
  196. package/js/i18n/locales/pl.js +17 -0
  197. package/js/i18n/locales/pt-BR.js +17 -0
  198. package/js/i18n/locales/pt.js +18 -0
  199. package/js/i18n/locales/ro.js +17 -0
  200. package/js/i18n/locales/ru.js +18 -0
  201. package/js/i18n/locales/si.js +18 -0
  202. package/js/i18n/locales/sk.js +18 -0
  203. package/js/i18n/locales/sl.js +15 -0
  204. package/js/i18n/locales/sq.js +18 -0
  205. package/js/i18n/locales/sr-latn.js +16 -0
  206. package/js/i18n/locales/sr.js +16 -0
  207. package/js/i18n/locales/sv.js +17 -0
  208. package/js/i18n/locales/sw.js +15 -0
  209. package/js/i18n/locales/ta.js +18 -0
  210. package/js/i18n/locales/tg.js +19 -0
  211. package/js/i18n/locales/th.js +14 -0
  212. package/js/i18n/locales/tk.js +18 -0
  213. package/js/i18n/locales/tr.js +17 -0
  214. package/js/i18n/locales/uk.js +17 -0
  215. package/js/i18n/locales/uz-cyrl.js +18 -0
  216. package/js/i18n/locales/uz-latn.js +18 -0
  217. package/js/i18n/locales/vi.js +16 -0
  218. package/js/i18n/locales/zh-CN.js +19 -0
  219. package/js/i18n/locales/zh-TW.js +18 -0
  220. package/js/lib/date-format.js +185 -0
  221. package/js/lib/date.js +82 -0
  222. package/js/lib/dom.js +55 -0
  223. package/js/lib/event.js +68 -0
  224. package/js/lib/utils.js +61 -0
  225. package/js/main.js +4 -0
  226. package/js/options/defaultOptions.js +38 -0
  227. package/js/options/processOptions.js +263 -0
  228. package/js/picker/Picker.js +334 -0
  229. package/js/picker/templates/calendarWeeksTemplate.js +8 -0
  230. package/js/picker/templates/daysTemplate.js +8 -0
  231. package/js/picker/templates/pickerTemplate.js +23 -0
  232. package/js/picker/views/DaysView.js +250 -0
  233. package/js/picker/views/MonthsView.js +198 -0
  234. package/js/picker/views/View.js +55 -0
  235. package/js/picker/views/YearsView.js +170 -0
  236. package/package.json +81 -0
  237. package/postcss.config.js +5 -0
  238. package/rollup.config.js +75 -0
  239. package/sass/_mixins.scss +12 -0
  240. package/sass/datepicker-bs4.scss +91 -0
  241. package/sass/datepicker-bs5.scss +91 -0
  242. package/sass/datepicker-bulma.scss +69 -0
  243. package/sass/datepicker-foundation.scss +66 -0
  244. package/sass/datepicker.scss +436 -0
  245. package/sass/index-bs4.scss +5 -0
  246. package/sass/index-bs5.scss +5 -0
  247. package/sass/index-bulma.scss +6 -0
  248. package/sass/index-foundation.scss +3 -0
  249. package/scripts/build-css.js +39 -0
  250. package/scripts/build-locales.js +57 -0
  251. package/scripts/convert-bsdp-locales.js +65 -0
  252. package/scripts/minify-css.js +31 -0
  253. package/scripts/minify-js.js +17 -0
  254. package/style.css +10 -0
  255. package/tailwind.config.js +16 -0
  256. package/tailwind.css +1389 -0
  257. package/test/DateRangePicker/DateRangePicker.js +547 -0
  258. package/test/DateRangePicker/api-methods.js +415 -0
  259. package/test/DateRangePicker/date-selection.js +472 -0
  260. package/test/DateRangePicker/options.js +308 -0
  261. package/test/Datepicker/Datepicker-object.js +236 -0
  262. package/test/Datepicker/api-methods.js +328 -0
  263. package/test/_utils/date.js +102 -0
  264. package/test/_utils/dom.js +70 -0
  265. package/test/events.js +785 -0
  266. package/test/inline-mode.js +45 -0
  267. package/test/keyboard-operation/arrow-down.js +293 -0
  268. package/test/keyboard-operation/arrow-left.js +255 -0
  269. package/test/keyboard-operation/arrow-right.js +215 -0
  270. package/test/keyboard-operation/arrow-up.js +461 -0
  271. package/test/keyboard-operation/edit-mode.js +458 -0
  272. package/test/keyboard-operation/keyboard-operation.js +146 -0
  273. package/test/mouse-operation.js +580 -0
  274. package/test/options/before-show.js +635 -0
  275. package/test/options/bulma-datepicker.sublime-workspace +1280 -0
  276. package/test/options/buttons.js +383 -0
  277. package/test/options/date-restrictions.js +802 -0
  278. package/test/options/format.js +281 -0
  279. package/test/options/multidate.js +380 -0
  280. package/test/options/options.js +727 -0
  281. package/test/options/orientation.js +410 -0
  282. package/test/options/pick-levle+view.js +357 -0
  283. package/test/test.html +94 -0
  284. package/test/unit/DateRangePicker.js +192 -0
  285. package/test/unit/Datepicker.js +307 -0
  286. package/test/unit/_setup.js +28 -0
  287. package/test/unit/lib/date-format.js +310 -0
  288. package/test/unit/lib/date.js +211 -0
  289. package/test/unit/lib/dom.js +142 -0
  290. package/test/unit/lib/event.js +207 -0
  291. package/test/unit/lib/utils.js +135 -0
@@ -0,0 +1,2569 @@
1
+ var Datepicker = (function () {
2
+ 'use strict';
3
+
4
+ function hasProperty(obj, prop) {
5
+ return Object.prototype.hasOwnProperty.call(obj, prop);
6
+ }
7
+
8
+ function lastItemOf(arr) {
9
+ return arr[arr.length - 1];
10
+ }
11
+
12
+ // push only the items not included in the array
13
+ function pushUnique(arr, ...items) {
14
+ items.forEach((item) => {
15
+ if (arr.includes(item)) {
16
+ return;
17
+ }
18
+ arr.push(item);
19
+ });
20
+ return arr;
21
+ }
22
+
23
+ function stringToArray(str, separator) {
24
+ // convert empty string to an empty array
25
+ return str ? str.split(separator) : [];
26
+ }
27
+
28
+ function isInRange(testVal, min, max) {
29
+ const minOK = min === undefined || testVal >= min;
30
+ const maxOK = max === undefined || testVal <= max;
31
+ return minOK && maxOK;
32
+ }
33
+
34
+ function limitToRange(val, min, max) {
35
+ if (val < min) {
36
+ return min;
37
+ }
38
+ if (val > max) {
39
+ return max;
40
+ }
41
+ return val;
42
+ }
43
+
44
+ function createTagRepeat(tagName, repeat, attributes = {}, index = 0, html = '') {
45
+ const openTagSrc = Object.keys(attributes).reduce((src, attr) => {
46
+ let val = attributes[attr];
47
+ if (typeof val === 'function') {
48
+ val = val(index);
49
+ }
50
+ return `${src} ${attr}="${val}"`;
51
+ }, tagName);
52
+ html += `<${openTagSrc}></${tagName}>`;
53
+
54
+ const next = index + 1;
55
+ return next < repeat
56
+ ? createTagRepeat(tagName, repeat, attributes, next, html)
57
+ : html;
58
+ }
59
+
60
+ // Remove the spacing surrounding tags for HTML parser not to create text nodes
61
+ // before/after elements
62
+ function optimizeTemplateHTML(html) {
63
+ return html.replace(/>\s+/g, '>').replace(/\s+</, '<');
64
+ }
65
+
66
+ function stripTime(timeValue) {
67
+ return new Date(timeValue).setHours(0, 0, 0, 0);
68
+ }
69
+
70
+ function today() {
71
+ return new Date().setHours(0, 0, 0, 0);
72
+ }
73
+
74
+ // Get the time value of the start of given date or year, month and day
75
+ function dateValue(...args) {
76
+ switch (args.length) {
77
+ case 0:
78
+ return today();
79
+ case 1:
80
+ return stripTime(args[0]);
81
+ }
82
+
83
+ // use setFullYear() to keep 2-digit year from being mapped to 1900-1999
84
+ const newDate = new Date(0);
85
+ newDate.setFullYear(...args);
86
+ return newDate.setHours(0, 0, 0, 0);
87
+ }
88
+
89
+ function addDays(date, amount) {
90
+ const newDate = new Date(date);
91
+ return newDate.setDate(newDate.getDate() + amount);
92
+ }
93
+
94
+ function addWeeks(date, amount) {
95
+ return addDays(date, amount * 7);
96
+ }
97
+
98
+ function addMonths(date, amount) {
99
+ // If the day of the date is not in the new month, the last day of the new
100
+ // month will be returned. e.g. Jan 31 + 1 month → Feb 28 (not Mar 03)
101
+ const newDate = new Date(date);
102
+ const monthsToSet = newDate.getMonth() + amount;
103
+ let expectedMonth = monthsToSet % 12;
104
+ if (expectedMonth < 0) {
105
+ expectedMonth += 12;
106
+ }
107
+
108
+ const time = newDate.setMonth(monthsToSet);
109
+ return newDate.getMonth() !== expectedMonth ? newDate.setDate(0) : time;
110
+ }
111
+
112
+ function addYears(date, amount) {
113
+ // If the date is Feb 29 and the new year is not a leap year, Feb 28 of the
114
+ // new year will be returned.
115
+ const newDate = new Date(date);
116
+ const expectedMonth = newDate.getMonth();
117
+ const time = newDate.setFullYear(newDate.getFullYear() + amount);
118
+ return expectedMonth === 1 && newDate.getMonth() === 2 ? newDate.setDate(0) : time;
119
+ }
120
+
121
+ // Calculate the distance bettwen 2 days of the week
122
+ function dayDiff(day, from) {
123
+ return (day - from + 7) % 7;
124
+ }
125
+
126
+ // Get the date of the specified day of the week of given base date
127
+ function dayOfTheWeekOf(baseDate, dayOfWeek, weekStart = 0) {
128
+ const baseDay = new Date(baseDate).getDay();
129
+ return addDays(baseDate, dayDiff(dayOfWeek, weekStart) - dayDiff(baseDay, weekStart));
130
+ }
131
+
132
+ // Get the ISO week of a date
133
+ function getWeek(date) {
134
+ // start of ISO week is Monday
135
+ const thuOfTheWeek = dayOfTheWeekOf(date, 4, 1);
136
+ // 1st week == the week where the 4th of January is in
137
+ const firstThu = dayOfTheWeekOf(new Date(thuOfTheWeek).setMonth(0, 4), 4, 1);
138
+ return Math.round((thuOfTheWeek - firstThu) / 604800000) + 1;
139
+ }
140
+
141
+ // Get the start year of the period of years that includes given date
142
+ // years: length of the year period
143
+ function startOfYearPeriod(date, years) {
144
+ /* @see https://en.wikipedia.org/wiki/Year_zero#ISO_8601 */
145
+ const year = new Date(date).getFullYear();
146
+ return Math.floor(year / years) * years;
147
+ }
148
+
149
+ // pattern for format parts
150
+ const reFormatTokens = /dd?|DD?|mm?|MM?|yy?(?:yy)?/;
151
+ // pattern for non date parts
152
+ const reNonDateParts = /[\s!-/:-@[-`{-~年月日]+/;
153
+ // cache for persed formats
154
+ let knownFormats = {};
155
+ // parse funtions for date parts
156
+ const parseFns = {
157
+ y(date, year) {
158
+ return new Date(date).setFullYear(parseInt(year, 10));
159
+ },
160
+ m(date, month, locale) {
161
+ const newDate = new Date(date);
162
+ let monthIndex = parseInt(month, 10) - 1;
163
+
164
+ if (isNaN(monthIndex)) {
165
+ if (!month) {
166
+ return NaN;
167
+ }
168
+
169
+ const monthName = month.toLowerCase();
170
+ const compareNames = name => name.toLowerCase().startsWith(monthName);
171
+ // compare with both short and full names because some locales have periods
172
+ // in the short names (not equal to the first X letters of the full names)
173
+ monthIndex = locale.monthsShort.findIndex(compareNames);
174
+ if (monthIndex < 0) {
175
+ monthIndex = locale.months.findIndex(compareNames);
176
+ }
177
+ if (monthIndex < 0) {
178
+ return NaN;
179
+ }
180
+ }
181
+
182
+ newDate.setMonth(monthIndex);
183
+ return newDate.getMonth() !== normalizeMonth(monthIndex)
184
+ ? newDate.setDate(0)
185
+ : newDate.getTime();
186
+ },
187
+ d(date, day) {
188
+ return new Date(date).setDate(parseInt(day, 10));
189
+ },
190
+ };
191
+ // format functions for date parts
192
+ const formatFns = {
193
+ d(date) {
194
+ return date.getDate();
195
+ },
196
+ dd(date) {
197
+ return padZero(date.getDate(), 2);
198
+ },
199
+ D(date, locale) {
200
+ return locale.daysShort[date.getDay()];
201
+ },
202
+ DD(date, locale) {
203
+ return locale.days[date.getDay()];
204
+ },
205
+ m(date) {
206
+ return date.getMonth() + 1;
207
+ },
208
+ mm(date) {
209
+ return padZero(date.getMonth() + 1, 2);
210
+ },
211
+ M(date, locale) {
212
+ return locale.monthsShort[date.getMonth()];
213
+ },
214
+ MM(date, locale) {
215
+ return locale.months[date.getMonth()];
216
+ },
217
+ y(date) {
218
+ return date.getFullYear();
219
+ },
220
+ yy(date) {
221
+ return padZero(date.getFullYear(), 2).slice(-2);
222
+ },
223
+ yyyy(date) {
224
+ return padZero(date.getFullYear(), 4);
225
+ },
226
+ };
227
+
228
+ // get month index in normal range (0 - 11) from any number
229
+ function normalizeMonth(monthIndex) {
230
+ return monthIndex > -1 ? monthIndex % 12 : normalizeMonth(monthIndex + 12);
231
+ }
232
+
233
+ function padZero(num, length) {
234
+ return num.toString().padStart(length, '0');
235
+ }
236
+
237
+ function parseFormatString(format) {
238
+ if (typeof format !== 'string') {
239
+ throw new Error("Invalid date format.");
240
+ }
241
+ if (format in knownFormats) {
242
+ return knownFormats[format];
243
+ }
244
+
245
+ // sprit the format string into parts and seprators
246
+ const separators = format.split(reFormatTokens);
247
+ const parts = format.match(new RegExp(reFormatTokens, 'g'));
248
+ if (separators.length === 0 || !parts) {
249
+ throw new Error("Invalid date format.");
250
+ }
251
+
252
+ // collect format functions used in the format
253
+ const partFormatters = parts.map(token => formatFns[token]);
254
+
255
+ // collect parse function keys used in the format
256
+ // iterate over parseFns' keys in order to keep the order of the keys.
257
+ const partParserKeys = Object.keys(parseFns).reduce((keys, key) => {
258
+ const token = parts.find(part => part[0] !== 'D' && part[0].toLowerCase() === key);
259
+ if (token) {
260
+ keys.push(key);
261
+ }
262
+ return keys;
263
+ }, []);
264
+
265
+ return knownFormats[format] = {
266
+ parser(dateStr, locale) {
267
+ const dateParts = dateStr.split(reNonDateParts).reduce((dtParts, part, index) => {
268
+ if (part.length > 0 && parts[index]) {
269
+ const token = parts[index][0];
270
+ if (token === 'M') {
271
+ dtParts.m = part;
272
+ } else if (token !== 'D') {
273
+ dtParts[token] = part;
274
+ }
275
+ }
276
+ return dtParts;
277
+ }, {});
278
+
279
+ // iterate over partParserkeys so that the parsing is made in the oder
280
+ // of year, month and day to prevent the day parser from correcting last
281
+ // day of month wrongly
282
+ return partParserKeys.reduce((origDate, key) => {
283
+ const newDate = parseFns[key](origDate, dateParts[key], locale);
284
+ // ingnore the part failed to parse
285
+ return isNaN(newDate) ? origDate : newDate;
286
+ }, today());
287
+ },
288
+ formatter(date, locale) {
289
+ let dateStr = partFormatters.reduce((str, fn, index) => {
290
+ return str += `${separators[index]}${fn(date, locale)}`;
291
+ }, '');
292
+ // separators' length is always parts' length + 1,
293
+ return dateStr += lastItemOf(separators);
294
+ },
295
+ };
296
+ }
297
+
298
+ function parseDate(dateStr, format, locale) {
299
+ if (dateStr instanceof Date || typeof dateStr === 'number') {
300
+ const date = stripTime(dateStr);
301
+ return isNaN(date) ? undefined : date;
302
+ }
303
+ if (!dateStr) {
304
+ return undefined;
305
+ }
306
+ if (dateStr === 'today') {
307
+ return today();
308
+ }
309
+
310
+ if (format && format.toValue) {
311
+ const date = format.toValue(dateStr, format, locale);
312
+ return isNaN(date) ? undefined : stripTime(date);
313
+ }
314
+
315
+ return parseFormatString(format).parser(dateStr, locale);
316
+ }
317
+
318
+ function formatDate(date, format, locale) {
319
+ if (isNaN(date) || (!date && date !== 0)) {
320
+ return '';
321
+ }
322
+
323
+ const dateObj = typeof date === 'number' ? new Date(date) : date;
324
+
325
+ if (format.toDisplay) {
326
+ return format.toDisplay(dateObj, format, locale);
327
+ }
328
+
329
+ return parseFormatString(format).formatter(dateObj, locale);
330
+ }
331
+
332
+ const listenerRegistry = new WeakMap();
333
+ const {addEventListener, removeEventListener} = EventTarget.prototype;
334
+
335
+ // Register event listeners to a key object
336
+ // listeners: array of listener definitions;
337
+ // - each definition must be a flat array of event target and the arguments
338
+ // used to call addEventListener() on the target
339
+ function registerListeners(keyObj, listeners) {
340
+ let registered = listenerRegistry.get(keyObj);
341
+ if (!registered) {
342
+ registered = [];
343
+ listenerRegistry.set(keyObj, registered);
344
+ }
345
+ listeners.forEach((listener) => {
346
+ addEventListener.call(...listener);
347
+ registered.push(listener);
348
+ });
349
+ }
350
+
351
+ function unregisterListeners(keyObj) {
352
+ let listeners = listenerRegistry.get(keyObj);
353
+ if (!listeners) {
354
+ return;
355
+ }
356
+ listeners.forEach((listener) => {
357
+ removeEventListener.call(...listener);
358
+ });
359
+ listenerRegistry.delete(keyObj);
360
+ }
361
+
362
+ // Event.composedPath() polyfill for Edge
363
+ // based on https://gist.github.com/kleinfreund/e9787d73776c0e3750dcfcdc89f100ec
364
+ if (!Event.prototype.composedPath) {
365
+ const getComposedPath = (node, path = []) => {
366
+ path.push(node);
367
+
368
+ let parent;
369
+ if (node.parentNode) {
370
+ parent = node.parentNode;
371
+ } else if (node.host) { // ShadowRoot
372
+ parent = node.host;
373
+ } else if (node.defaultView) { // Document
374
+ parent = node.defaultView;
375
+ }
376
+ return parent ? getComposedPath(parent, path) : path;
377
+ };
378
+
379
+ Event.prototype.composedPath = function () {
380
+ return getComposedPath(this.target);
381
+ };
382
+ }
383
+
384
+ function findFromPath(path, criteria, currentTarget, index = 0) {
385
+ const el = path[index];
386
+ if (criteria(el)) {
387
+ return el;
388
+ } else if (el === currentTarget || !el.parentElement) {
389
+ // stop when reaching currentTarget or <html>
390
+ return;
391
+ }
392
+ return findFromPath(path, criteria, currentTarget, index + 1);
393
+ }
394
+
395
+ // Search for the actual target of a delegated event
396
+ function findElementInEventPath(ev, selector) {
397
+ const criteria = typeof selector === 'function' ? selector : el => el.matches(selector);
398
+ return findFromPath(ev.composedPath(), criteria, ev.currentTarget);
399
+ }
400
+
401
+ // default locales
402
+ const locales = {
403
+ en: {
404
+ days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
405
+ daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
406
+ daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
407
+ months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
408
+ monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
409
+ today: "Today",
410
+ clear: "Clear",
411
+ titleFormat: "MM y"
412
+ }
413
+ };
414
+
415
+ // config options updatable by setOptions() and their default values
416
+ const defaultOptions = {
417
+ autohide: false,
418
+ beforeShowDay: null,
419
+ beforeShowDecade: null,
420
+ beforeShowMonth: null,
421
+ beforeShowYear: null,
422
+ calendarWeeks: false,
423
+ clearBtn: false,
424
+ dateDelimiter: ',',
425
+ datesDisabled: [],
426
+ daysOfWeekDisabled: [],
427
+ daysOfWeekHighlighted: [],
428
+ defaultViewDate: undefined, // placeholder, defaults to today() by the program
429
+ disableTouchKeyboard: false,
430
+ format: 'mm/dd/yyyy',
431
+ language: 'en',
432
+ maxDate: null,
433
+ maxNumberOfDates: 1,
434
+ maxView: 3,
435
+ minDate: null,
436
+ nextArrow: '<svg class="w-4 h-4 rtl:rotate-180 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/></svg>',
437
+ orientation: 'auto',
438
+ pickLevel: 0,
439
+ prevArrow: '<svg class="w-4 h-4 rtl:rotate-180 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/></svg>',
440
+ showDaysOfWeek: true,
441
+ showOnClick: true,
442
+ showOnFocus: true,
443
+ startView: 0,
444
+ title: '',
445
+ todayBtn: false,
446
+ todayBtnMode: 0,
447
+ todayHighlight: false,
448
+ updateOnBlur: true,
449
+ weekStart: 0,
450
+ };
451
+
452
+ const range = document.createRange();
453
+
454
+ function parseHTML(html) {
455
+ return range.createContextualFragment(html);
456
+ }
457
+
458
+ function hideElement(el) {
459
+ if (el.style.display === 'none') {
460
+ return;
461
+ }
462
+ // back up the existing display setting in data-style-display
463
+ if (el.style.display) {
464
+ el.dataset.styleDisplay = el.style.display;
465
+ }
466
+ el.style.display = 'none';
467
+ }
468
+
469
+ function showElement(el) {
470
+ if (el.style.display !== 'none') {
471
+ return;
472
+ }
473
+ if (el.dataset.styleDisplay) {
474
+ // restore backed-up dispay property
475
+ el.style.display = el.dataset.styleDisplay;
476
+ delete el.dataset.styleDisplay;
477
+ } else {
478
+ el.style.display = '';
479
+ }
480
+ }
481
+
482
+ function emptyChildNodes(el) {
483
+ if (el.firstChild) {
484
+ el.removeChild(el.firstChild);
485
+ emptyChildNodes(el);
486
+ }
487
+ }
488
+
489
+ function replaceChildNodes(el, newChildNodes) {
490
+ emptyChildNodes(el);
491
+ if (newChildNodes instanceof DocumentFragment) {
492
+ el.appendChild(newChildNodes);
493
+ } else if (typeof newChildNodes === 'string') {
494
+ el.appendChild(parseHTML(newChildNodes));
495
+ } else if (typeof newChildNodes.forEach === 'function') {
496
+ newChildNodes.forEach((node) => {
497
+ el.appendChild(node);
498
+ });
499
+ }
500
+ }
501
+
502
+ const {
503
+ language: defaultLang,
504
+ format: defaultFormat,
505
+ weekStart: defaultWeekStart,
506
+ } = defaultOptions;
507
+
508
+ // Reducer function to filter out invalid day-of-week from the input
509
+ function sanitizeDOW(dow, day) {
510
+ return dow.length < 6 && day >= 0 && day < 7
511
+ ? pushUnique(dow, day)
512
+ : dow;
513
+ }
514
+
515
+ function calcEndOfWeek(startOfWeek) {
516
+ return (startOfWeek + 6) % 7;
517
+ }
518
+
519
+ // validate input date. if invalid, fallback to the original value
520
+ function validateDate(value, format, locale, origValue) {
521
+ const date = parseDate(value, format, locale);
522
+ return date !== undefined ? date : origValue;
523
+ }
524
+
525
+ // Validate viewId. if invalid, fallback to the original value
526
+ function validateViewId(value, origValue, max = 3) {
527
+ const viewId = parseInt(value, 10);
528
+ return viewId >= 0 && viewId <= max ? viewId : origValue;
529
+ }
530
+
531
+ // Create Datepicker configuration to set
532
+ function processOptions(options, datepicker) {
533
+ const inOpts = Object.assign({}, options);
534
+ const config = {};
535
+ const locales = datepicker.constructor.locales;
536
+ let {
537
+ format,
538
+ language,
539
+ locale,
540
+ maxDate,
541
+ maxView,
542
+ minDate,
543
+ pickLevel,
544
+ startView,
545
+ weekStart,
546
+ } = datepicker.config || {};
547
+
548
+ if (inOpts.language) {
549
+ let lang;
550
+ if (inOpts.language !== language) {
551
+ if (locales[inOpts.language]) {
552
+ lang = inOpts.language;
553
+ } else {
554
+ // Check if langauge + region tag can fallback to the one without
555
+ // region (e.g. fr-CA → fr)
556
+ lang = inOpts.language.split('-')[0];
557
+ if (locales[lang] === undefined) {
558
+ lang = false;
559
+ }
560
+ }
561
+ }
562
+ delete inOpts.language;
563
+ if (lang) {
564
+ language = config.language = lang;
565
+
566
+ // update locale as well when updating language
567
+ const origLocale = locale || locales[defaultLang];
568
+ // use default language's properties for the fallback
569
+ locale = Object.assign({
570
+ format: defaultFormat,
571
+ weekStart: defaultWeekStart
572
+ }, locales[defaultLang]);
573
+ if (language !== defaultLang) {
574
+ Object.assign(locale, locales[language]);
575
+ }
576
+ config.locale = locale;
577
+ // if format and/or weekStart are the same as old locale's defaults,
578
+ // update them to new locale's defaults
579
+ if (format === origLocale.format) {
580
+ format = config.format = locale.format;
581
+ }
582
+ if (weekStart === origLocale.weekStart) {
583
+ weekStart = config.weekStart = locale.weekStart;
584
+ config.weekEnd = calcEndOfWeek(locale.weekStart);
585
+ }
586
+ }
587
+ }
588
+
589
+ if (inOpts.format) {
590
+ const hasToDisplay = typeof inOpts.format.toDisplay === 'function';
591
+ const hasToValue = typeof inOpts.format.toValue === 'function';
592
+ const validFormatString = reFormatTokens.test(inOpts.format);
593
+ if ((hasToDisplay && hasToValue) || validFormatString) {
594
+ format = config.format = inOpts.format;
595
+ }
596
+ delete inOpts.format;
597
+ }
598
+
599
+ //*** dates ***//
600
+ // while min and maxDate for "no limit" in the options are better to be null
601
+ // (especially when updating), the ones in the config have to be undefined
602
+ // because null is treated as 0 (= unix epoch) when comparing with time value
603
+ let minDt = minDate;
604
+ let maxDt = maxDate;
605
+ if (inOpts.minDate !== undefined) {
606
+ minDt = inOpts.minDate === null
607
+ ? dateValue(0, 0, 1) // set 0000-01-01 to prevent negative values for year
608
+ : validateDate(inOpts.minDate, format, locale, minDt);
609
+ delete inOpts.minDate;
610
+ }
611
+ if (inOpts.maxDate !== undefined) {
612
+ maxDt = inOpts.maxDate === null
613
+ ? undefined
614
+ : validateDate(inOpts.maxDate, format, locale, maxDt);
615
+ delete inOpts.maxDate;
616
+ }
617
+ if (maxDt < minDt) {
618
+ minDate = config.minDate = maxDt;
619
+ maxDate = config.maxDate = minDt;
620
+ } else {
621
+ if (minDate !== minDt) {
622
+ minDate = config.minDate = minDt;
623
+ }
624
+ if (maxDate !== maxDt) {
625
+ maxDate = config.maxDate = maxDt;
626
+ }
627
+ }
628
+
629
+ if (inOpts.datesDisabled) {
630
+ config.datesDisabled = inOpts.datesDisabled.reduce((dates, dt) => {
631
+ const date = parseDate(dt, format, locale);
632
+ return date !== undefined ? pushUnique(dates, date) : dates;
633
+ }, []);
634
+ delete inOpts.datesDisabled;
635
+ }
636
+ if (inOpts.defaultViewDate !== undefined) {
637
+ const viewDate = parseDate(inOpts.defaultViewDate, format, locale);
638
+ if (viewDate !== undefined) {
639
+ config.defaultViewDate = viewDate;
640
+ }
641
+ delete inOpts.defaultViewDate;
642
+ }
643
+
644
+ //*** days of week ***//
645
+ if (inOpts.weekStart !== undefined) {
646
+ const wkStart = Number(inOpts.weekStart) % 7;
647
+ if (!isNaN(wkStart)) {
648
+ weekStart = config.weekStart = wkStart;
649
+ config.weekEnd = calcEndOfWeek(wkStart);
650
+ }
651
+ delete inOpts.weekStart;
652
+ }
653
+ if (inOpts.daysOfWeekDisabled) {
654
+ config.daysOfWeekDisabled = inOpts.daysOfWeekDisabled.reduce(sanitizeDOW, []);
655
+ delete inOpts.daysOfWeekDisabled;
656
+ }
657
+ if (inOpts.daysOfWeekHighlighted) {
658
+ config.daysOfWeekHighlighted = inOpts.daysOfWeekHighlighted.reduce(sanitizeDOW, []);
659
+ delete inOpts.daysOfWeekHighlighted;
660
+ }
661
+
662
+ //*** multi date ***//
663
+ if (inOpts.maxNumberOfDates !== undefined) {
664
+ const maxNumberOfDates = parseInt(inOpts.maxNumberOfDates, 10);
665
+ if (maxNumberOfDates >= 0) {
666
+ config.maxNumberOfDates = maxNumberOfDates;
667
+ config.multidate = maxNumberOfDates !== 1;
668
+ }
669
+ delete inOpts.maxNumberOfDates;
670
+ }
671
+ if (inOpts.dateDelimiter) {
672
+ config.dateDelimiter = String(inOpts.dateDelimiter);
673
+ delete inOpts.dateDelimiter;
674
+ }
675
+
676
+ //*** pick level & view ***//
677
+ let newPickLevel = pickLevel;
678
+ if (inOpts.pickLevel !== undefined) {
679
+ newPickLevel = validateViewId(inOpts.pickLevel, 2);
680
+ delete inOpts.pickLevel;
681
+ }
682
+ if (newPickLevel !== pickLevel) {
683
+ pickLevel = config.pickLevel = newPickLevel;
684
+ }
685
+
686
+ let newMaxView = maxView;
687
+ if (inOpts.maxView !== undefined) {
688
+ newMaxView = validateViewId(inOpts.maxView, maxView);
689
+ delete inOpts.maxView;
690
+ }
691
+ // ensure max view >= pick level
692
+ newMaxView = pickLevel > newMaxView ? pickLevel : newMaxView;
693
+ if (newMaxView !== maxView) {
694
+ maxView = config.maxView = newMaxView;
695
+ }
696
+
697
+ let newStartView = startView;
698
+ if (inOpts.startView !== undefined) {
699
+ newStartView = validateViewId(inOpts.startView, newStartView);
700
+ delete inOpts.startView;
701
+ }
702
+ // ensure pick level <= start view <= max view
703
+ if (newStartView < pickLevel) {
704
+ newStartView = pickLevel;
705
+ } else if (newStartView > maxView) {
706
+ newStartView = maxView;
707
+ }
708
+ if (newStartView !== startView) {
709
+ config.startView = newStartView;
710
+ }
711
+
712
+ //*** template ***//
713
+ if (inOpts.prevArrow) {
714
+ const prevArrow = parseHTML(inOpts.prevArrow);
715
+ if (prevArrow.childNodes.length > 0) {
716
+ config.prevArrow = prevArrow.childNodes;
717
+ }
718
+ delete inOpts.prevArrow;
719
+ }
720
+ if (inOpts.nextArrow) {
721
+ const nextArrow = parseHTML(inOpts.nextArrow);
722
+ if (nextArrow.childNodes.length > 0) {
723
+ config.nextArrow = nextArrow.childNodes;
724
+ }
725
+ delete inOpts.nextArrow;
726
+ }
727
+
728
+ //*** misc ***//
729
+ if (inOpts.disableTouchKeyboard !== undefined) {
730
+ config.disableTouchKeyboard = 'ontouchstart' in document && !!inOpts.disableTouchKeyboard;
731
+ delete inOpts.disableTouchKeyboard;
732
+ }
733
+ if (inOpts.orientation) {
734
+ const orientation = inOpts.orientation.toLowerCase().split(/\s+/g);
735
+ config.orientation = {
736
+ x: orientation.find(x => (x === 'left' || x === 'right')) || 'auto',
737
+ y: orientation.find(y => (y === 'top' || y === 'bottom')) || 'auto',
738
+ };
739
+ delete inOpts.orientation;
740
+ }
741
+ if (inOpts.todayBtnMode !== undefined) {
742
+ switch(inOpts.todayBtnMode) {
743
+ case 0:
744
+ case 1:
745
+ config.todayBtnMode = inOpts.todayBtnMode;
746
+ }
747
+ delete inOpts.todayBtnMode;
748
+ }
749
+
750
+ //*** copy the rest ***//
751
+ Object.keys(inOpts).forEach((key) => {
752
+ if (inOpts[key] !== undefined && hasProperty(defaultOptions, key)) {
753
+ config[key] = inOpts[key];
754
+ }
755
+ });
756
+
757
+ return config;
758
+ }
759
+
760
+ const pickerTemplate = optimizeTemplateHTML(`<div class="datepicker hidden">
761
+ <div class="datepicker-picker inline-block rounded-base bg-white dark:bg-gray-700 shadow-lg p-4">
762
+ <div class="datepicker-header">
763
+ <div class="datepicker-title bg-white dark:bg-gray-700 dark:text-white px-2 py-3 text-center font-medium"></div>
764
+ <div class="datepicker-controls flex justify-between mb-2">
765
+ <button type="button" class="bg-white dark:bg-gray-700 rounded-base text-fg-disabled hover:bg-neutral-tertiary-medium hover:text-body dark:hover:text-white text-lg p-2.5 focus:outline-none focus:ring-2 focus:ring-neutral-tertiary prev-btn"></button>
766
+ <button type="button" class="text-sm rounded-base text-body bg-white dark:bg-gray-700 font-medium py-2.5 px-5 hover:bg-neutral-tertiary-medium focus:outline-none focus:ring-2 focus:ring-neutral-tertiary view-switch"></button>
767
+ <button type="button" class="bg-white dark:bg-gray-700 rounded-base text-fg-disabled hover:bg-neutral-tertiary-medium hover:text-body dark:hover:text-white text-lg p-2.5 focus:outline-none focus:ring-2 focus:ring-neutral-tertiary next-btn"></button>
768
+ </div>
769
+ </div>
770
+ <div class="datepicker-main p-1"></div>
771
+ <div class="datepicker-footer">
772
+ <div class="datepicker-controls flex space-x-2 rtl:space-x-reverse mt-2">
773
+ <button type="button" class="%buttonClass% today-btn text-white bg-blue-700 !bg-primary-700 dark:bg-blue-600 dark:!bg-primary-600 hover:bg-blue-800 hover:!bg-primary-800 dark:hover:bg-blue-700 dark:hover:!bg-primary-700 focus:ring-4 focus:ring-brand-medium font-medium rounded-base text-sm px-5 py-2 text-center w-1/2"></button>
774
+ <button type="button" class="%buttonClass% clear-btn text-body bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-neutral-tertiary-medium focus:ring-4 focus:ring-brand-medium font-medium rounded-base text-sm px-5 py-2 text-center w-1/2"></button>
775
+ </div>
776
+ </div>
777
+ </div>
778
+ </div>`);
779
+
780
+ const daysTemplate = optimizeTemplateHTML(`<div class="days">
781
+ <div class="days-of-week grid grid-cols-7 mb-1">${createTagRepeat('span', 7, {class: 'dow block flex-1 leading-9 border-0 rounded-base cursor-default text-center text-body font-medium text-sm'})}</div>
782
+ <div class="datepicker-grid w-64 grid grid-cols-7">${createTagRepeat('span', 42 , {class: 'block flex-1 leading-9 border-0 rounded-base cursor-default text-center text-body font-medium text-sm h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400'})}</div>
783
+ </div>`);
784
+
785
+ const calendarWeeksTemplate = optimizeTemplateHTML(`<div class="calendar-weeks">
786
+ <div class="days-of-week flex"><span class="dow h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400"></span></div>
787
+ <div class="weeks">${createTagRepeat('span', 6, {class: 'week block flex-1 leading-9 border-0 rounded-base cursor-default text-center text-body font-medium text-sm'})}</div>
788
+ </div>`);
789
+
790
+ // Base class of the view classes
791
+ class View {
792
+ constructor(picker, config) {
793
+ Object.assign(this, config, {
794
+ picker,
795
+ element: parseHTML(`<div class="datepicker-view flex"></div>`).firstChild,
796
+ selected: [],
797
+ });
798
+ this.init(this.picker.datepicker.config);
799
+ }
800
+
801
+ init(options) {
802
+ if (options.pickLevel !== undefined) {
803
+ this.isMinView = this.id === options.pickLevel;
804
+ }
805
+ this.setOptions(options);
806
+ this.updateFocus();
807
+ this.updateSelection();
808
+ }
809
+
810
+ // Execute beforeShow() callback and apply the result to the element
811
+ // args:
812
+ // - current - current value on the iteration on view rendering
813
+ // - timeValue - time value of the date to pass to beforeShow()
814
+ performBeforeHook(el, current, timeValue) {
815
+ let result = this.beforeShow(new Date(timeValue));
816
+ switch (typeof result) {
817
+ case 'boolean':
818
+ result = {enabled: result};
819
+ break;
820
+ case 'string':
821
+ result = {classes: result};
822
+ }
823
+
824
+ if (result) {
825
+ if (result.enabled === false) {
826
+ el.classList.add('disabled');
827
+ pushUnique(this.disabled, current);
828
+ }
829
+ if (result.classes) {
830
+ const extraClasses = result.classes.split(/\s+/);
831
+ el.classList.add(...extraClasses);
832
+ if (extraClasses.includes('disabled')) {
833
+ pushUnique(this.disabled, current);
834
+ }
835
+ }
836
+ if (result.content) {
837
+ replaceChildNodes(el, result.content);
838
+ }
839
+ }
840
+ }
841
+ }
842
+
843
+ class DaysView extends View {
844
+ constructor(picker) {
845
+ super(picker, {
846
+ id: 0,
847
+ name: 'days',
848
+ cellClass: 'day',
849
+ });
850
+ }
851
+
852
+ init(options, onConstruction = true) {
853
+ if (onConstruction) {
854
+ const inner = parseHTML(daysTemplate).firstChild;
855
+ this.dow = inner.firstChild;
856
+ this.grid = inner.lastChild;
857
+ this.element.appendChild(inner);
858
+ }
859
+ super.init(options);
860
+ }
861
+
862
+ setOptions(options) {
863
+ let updateDOW;
864
+
865
+ if (hasProperty(options, 'minDate')) {
866
+ this.minDate = options.minDate;
867
+ }
868
+ if (hasProperty(options, 'maxDate')) {
869
+ this.maxDate = options.maxDate;
870
+ }
871
+ if (options.datesDisabled) {
872
+ this.datesDisabled = options.datesDisabled;
873
+ }
874
+ if (options.daysOfWeekDisabled) {
875
+ this.daysOfWeekDisabled = options.daysOfWeekDisabled;
876
+ updateDOW = true;
877
+ }
878
+ if (options.daysOfWeekHighlighted) {
879
+ this.daysOfWeekHighlighted = options.daysOfWeekHighlighted;
880
+ }
881
+ if (options.todayHighlight !== undefined) {
882
+ this.todayHighlight = options.todayHighlight;
883
+ }
884
+ if (options.weekStart !== undefined) {
885
+ this.weekStart = options.weekStart;
886
+ this.weekEnd = options.weekEnd;
887
+ updateDOW = true;
888
+ }
889
+ if (options.locale) {
890
+ const locale = this.locale = options.locale;
891
+ this.dayNames = locale.daysMin;
892
+ this.switchLabelFormat = locale.titleFormat;
893
+ updateDOW = true;
894
+ }
895
+ if (options.beforeShowDay !== undefined) {
896
+ this.beforeShow = typeof options.beforeShowDay === 'function'
897
+ ? options.beforeShowDay
898
+ : undefined;
899
+ }
900
+
901
+ if (options.calendarWeeks !== undefined) {
902
+ if (options.calendarWeeks && !this.calendarWeeks) {
903
+ const weeksElem = parseHTML(calendarWeeksTemplate).firstChild;
904
+ this.calendarWeeks = {
905
+ element: weeksElem,
906
+ dow: weeksElem.firstChild,
907
+ weeks: weeksElem.lastChild,
908
+ };
909
+ this.element.insertBefore(weeksElem, this.element.firstChild);
910
+ } else if (this.calendarWeeks && !options.calendarWeeks) {
911
+ this.element.removeChild(this.calendarWeeks.element);
912
+ this.calendarWeeks = null;
913
+ }
914
+ }
915
+ if (options.showDaysOfWeek !== undefined) {
916
+ if (options.showDaysOfWeek) {
917
+ showElement(this.dow);
918
+ if (this.calendarWeeks) {
919
+ showElement(this.calendarWeeks.dow);
920
+ }
921
+ } else {
922
+ hideElement(this.dow);
923
+ if (this.calendarWeeks) {
924
+ hideElement(this.calendarWeeks.dow);
925
+ }
926
+ }
927
+ }
928
+
929
+ // update days-of-week when locale, daysOfweekDisabled or weekStart is changed
930
+ if (updateDOW) {
931
+ Array.from(this.dow.children).forEach((el, index) => {
932
+ const dow = (this.weekStart + index) % 7;
933
+ el.textContent = this.dayNames[dow];
934
+ el.className = this.daysOfWeekDisabled.includes(dow) ? 'dow disabled text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400 cursor-not-allowed' : 'dow text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400';
935
+ });
936
+ }
937
+ }
938
+
939
+ // Apply update on the focused date to view's settings
940
+ updateFocus() {
941
+ const viewDate = new Date(this.picker.viewDate);
942
+ const viewYear = viewDate.getFullYear();
943
+ const viewMonth = viewDate.getMonth();
944
+ const firstOfMonth = dateValue(viewYear, viewMonth, 1);
945
+ const start = dayOfTheWeekOf(firstOfMonth, this.weekStart, this.weekStart);
946
+
947
+ this.first = firstOfMonth;
948
+ this.last = dateValue(viewYear, viewMonth + 1, 0);
949
+ this.start = start;
950
+ this.focused = this.picker.viewDate;
951
+ }
952
+
953
+ // Apply update on the selected dates to view's settings
954
+ updateSelection() {
955
+ const {dates, rangepicker} = this.picker.datepicker;
956
+ this.selected = dates;
957
+ if (rangepicker) {
958
+ this.range = rangepicker.dates;
959
+ }
960
+ }
961
+
962
+ // Update the entire view UI
963
+ render() {
964
+ // update today marker on ever render
965
+ this.today = this.todayHighlight ? today() : undefined;
966
+ // refresh disabled dates on every render in order to clear the ones added
967
+ // by beforeShow hook at previous render
968
+ this.disabled = [...this.datesDisabled];
969
+
970
+ const switchLabel = formatDate(this.focused, this.switchLabelFormat, this.locale);
971
+ this.picker.setViewSwitchLabel(switchLabel);
972
+ this.picker.setPrevBtnDisabled(this.first <= this.minDate);
973
+ this.picker.setNextBtnDisabled(this.last >= this.maxDate);
974
+
975
+ if (this.calendarWeeks) {
976
+ // start of the UTC week (Monday) of the 1st of the month
977
+ const startOfWeek = dayOfTheWeekOf(this.first, 1, 1);
978
+ Array.from(this.calendarWeeks.weeks.children).forEach((el, index) => {
979
+ el.textContent = getWeek(addWeeks(startOfWeek, index));
980
+ });
981
+ }
982
+ Array.from(this.grid.children).forEach((el, index) => {
983
+ const classList = el.classList;
984
+ const current = addDays(this.start, index);
985
+ const date = new Date(current);
986
+ const day = date.getDay();
987
+
988
+ el.className = `datepicker-cell hover:bg-neutral-tertiary-medium block flex-1 leading-9 border-0 rounded-base cursor-pointer text-center text-body font-medium text-sm ${this.cellClass}`;
989
+ el.dataset.date = current;
990
+ el.textContent = date.getDate();
991
+
992
+ if (current < this.first) {
993
+ classList.add('prev', 'text-gray-500', 'dark:text-white');
994
+ } else if (current > this.last) {
995
+ classList.add('next', 'text-gray-500', 'dark:text-white');
996
+ }
997
+ if (this.today === current) {
998
+ classList.add('today', 'bg-gray-100', 'dark:bg-gray-600');
999
+ }
1000
+ if (current < this.minDate || current > this.maxDate || this.disabled.includes(current)) {
1001
+ classList.add('disabled', 'cursor-not-allowed', 'text-gray-400', 'dark:text-gray-500');
1002
+ classList.remove('hover:bg-neutral-tertiary-medium', 'text-body', 'dark:text-white', 'cursor-pointer');
1003
+ }
1004
+ if (this.daysOfWeekDisabled.includes(day)) {
1005
+ classList.add('disabled', 'cursor-not-allowed', 'text-gray-400', 'dark:text-gray-500');
1006
+ classList.remove('hover:bg-neutral-tertiary-medium', 'text-body', 'dark:text-white', 'cursor-pointer');
1007
+ pushUnique(this.disabled, current);
1008
+ }
1009
+ if (this.daysOfWeekHighlighted.includes(day)) {
1010
+ classList.add('highlighted');
1011
+ }
1012
+ if (this.range) {
1013
+ const [rangeStart, rangeEnd] = this.range;
1014
+ if (current > rangeStart && current < rangeEnd) {
1015
+ classList.add('range', 'bg-gray-200', 'dark:bg-gray-600');
1016
+ classList.remove('rounded-base', 'rounded-s-base', 'rounded-e-base');
1017
+ }
1018
+ if (current === rangeStart) {
1019
+ classList.add('range-start', 'bg-gray-100', 'dark:bg-gray-600', 'rounded-s-base');
1020
+ classList.remove('rounded-base', 'rounded-e-base');
1021
+ }
1022
+ if (current === rangeEnd) {
1023
+ classList.add('range-end', 'bg-gray-100', 'dark:bg-gray-600', 'rounded-e-base');
1024
+ classList.remove('rounded-base', 'rounded-s-base');
1025
+ }
1026
+ }
1027
+ if (this.selected.includes(current)) {
1028
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1029
+ classList.remove('text-body', 'text-gray-500', 'hover:bg-neutral-tertiary-medium', 'dark:text-white', 'dark:bg-gray-600', 'bg-gray-100', 'bg-gray-200');
1030
+ }
1031
+ if (current === this.focused) {
1032
+ classList.add('focused');
1033
+ }
1034
+
1035
+ if (this.beforeShow) {
1036
+ this.performBeforeHook(el, current, current);
1037
+ }
1038
+ });
1039
+ }
1040
+
1041
+ // Update the view UI by applying the changes of selected and focused items
1042
+ refresh() {
1043
+ const [rangeStart, rangeEnd] = this.range || [];
1044
+ this.grid
1045
+ .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
1046
+ .forEach((el) => {
1047
+ el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white', 'focused');
1048
+ el.classList.add('text-body', 'rounded-base', 'dark:text-white');
1049
+ });
1050
+ Array.from(this.grid.children).forEach((el) => {
1051
+ const current = Number(el.dataset.date);
1052
+ const classList = el.classList;
1053
+ classList.remove('bg-gray-200', 'dark:bg-gray-600', 'rounded-s-base', 'rounded-e-base');
1054
+ if (current > rangeStart && current < rangeEnd) {
1055
+ classList.add('range', 'bg-gray-200', 'dark:bg-gray-600');
1056
+ classList.remove('rounded-base');
1057
+ }
1058
+ if (current === rangeStart) {
1059
+ classList.add('range-start', 'bg-gray-200', 'dark:bg-gray-600', 'rounded-s-base');
1060
+ classList.remove('rounded-base',);
1061
+ }
1062
+ if (current === rangeEnd) {
1063
+ classList.add('range-end', 'bg-gray-200', 'dark:bg-gray-600', 'rounded-e-base');
1064
+ classList.remove('rounded-base',);
1065
+ }
1066
+ if (this.selected.includes(current)) {
1067
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1068
+ classList.remove('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white', 'bg-gray-100', 'bg-gray-200', 'dark:bg-gray-600');
1069
+ }
1070
+ if (current === this.focused) {
1071
+ classList.add('focused');
1072
+ }
1073
+ });
1074
+ }
1075
+
1076
+ // Update the view UI by applying the change of focused item
1077
+ refreshFocus() {
1078
+ const index = Math.round((this.focused - this.start) / 86400000);
1079
+ this.grid.querySelectorAll('.focused').forEach((el) => {
1080
+ el.classList.remove('focused');
1081
+ });
1082
+ this.grid.children[index].classList.add('focused');
1083
+ }
1084
+ }
1085
+
1086
+ function computeMonthRange(range, thisYear) {
1087
+ if (!range || !range[0] || !range[1]) {
1088
+ return;
1089
+ }
1090
+
1091
+ const [[startY, startM], [endY, endM]] = range;
1092
+ if (startY > thisYear || endY < thisYear) {
1093
+ return;
1094
+ }
1095
+ return [
1096
+ startY === thisYear ? startM : -1,
1097
+ endY === thisYear ? endM : 12,
1098
+ ];
1099
+ }
1100
+
1101
+ class MonthsView extends View {
1102
+ constructor(picker) {
1103
+ super(picker, {
1104
+ id: 1,
1105
+ name: 'months',
1106
+ cellClass: 'month',
1107
+ });
1108
+ }
1109
+
1110
+ init(options, onConstruction = true) {
1111
+ if (onConstruction) {
1112
+ this.grid = this.element;
1113
+ this.element.classList.add('months', 'datepicker-grid', 'w-64', 'grid', 'grid-cols-4');
1114
+ this.grid.appendChild(parseHTML(createTagRepeat('span', 12, {'data-month': ix => ix})));
1115
+ }
1116
+ super.init(options);
1117
+ }
1118
+
1119
+ setOptions(options) {
1120
+ if (options.locale) {
1121
+ this.monthNames = options.locale.monthsShort;
1122
+ }
1123
+ if (hasProperty(options, 'minDate')) {
1124
+ if (options.minDate === undefined) {
1125
+ this.minYear = this.minMonth = this.minDate = undefined;
1126
+ } else {
1127
+ const minDateObj = new Date(options.minDate);
1128
+ this.minYear = minDateObj.getFullYear();
1129
+ this.minMonth = minDateObj.getMonth();
1130
+ this.minDate = minDateObj.setDate(1);
1131
+ }
1132
+ }
1133
+ if (hasProperty(options, 'maxDate')) {
1134
+ if (options.maxDate === undefined) {
1135
+ this.maxYear = this.maxMonth = this.maxDate = undefined;
1136
+ } else {
1137
+ const maxDateObj = new Date(options.maxDate);
1138
+ this.maxYear = maxDateObj.getFullYear();
1139
+ this.maxMonth = maxDateObj.getMonth();
1140
+ this.maxDate = dateValue(this.maxYear, this.maxMonth + 1, 0);
1141
+ }
1142
+ }
1143
+ if (options.beforeShowMonth !== undefined) {
1144
+ this.beforeShow = typeof options.beforeShowMonth === 'function'
1145
+ ? options.beforeShowMonth
1146
+ : undefined;
1147
+ }
1148
+ }
1149
+
1150
+ // Update view's settings to reflect the viewDate set on the picker
1151
+ updateFocus() {
1152
+ const viewDate = new Date(this.picker.viewDate);
1153
+ this.year = viewDate.getFullYear();
1154
+ this.focused = viewDate.getMonth();
1155
+ }
1156
+
1157
+ // Update view's settings to reflect the selected dates
1158
+ updateSelection() {
1159
+ const {dates, rangepicker} = this.picker.datepicker;
1160
+ this.selected = dates.reduce((selected, timeValue) => {
1161
+ const date = new Date(timeValue);
1162
+ const year = date.getFullYear();
1163
+ const month = date.getMonth();
1164
+ if (selected[year] === undefined) {
1165
+ selected[year] = [month];
1166
+ } else {
1167
+ pushUnique(selected[year], month);
1168
+ }
1169
+ return selected;
1170
+ }, {});
1171
+ if (rangepicker && rangepicker.dates) {
1172
+ this.range = rangepicker.dates.map(timeValue => {
1173
+ const date = new Date(timeValue);
1174
+ return isNaN(date) ? undefined : [date.getFullYear(), date.getMonth()];
1175
+ });
1176
+ }
1177
+ }
1178
+
1179
+ // Update the entire view UI
1180
+ render() {
1181
+ // refresh disabled months on every render in order to clear the ones added
1182
+ // by beforeShow hook at previous render
1183
+ this.disabled = [];
1184
+
1185
+ this.picker.setViewSwitchLabel(this.year);
1186
+ this.picker.setPrevBtnDisabled(this.year <= this.minYear);
1187
+ this.picker.setNextBtnDisabled(this.year >= this.maxYear);
1188
+
1189
+ const selected = this.selected[this.year] || [];
1190
+ const yrOutOfRange = this.year < this.minYear || this.year > this.maxYear;
1191
+ const isMinYear = this.year === this.minYear;
1192
+ const isMaxYear = this.year === this.maxYear;
1193
+ const range = computeMonthRange(this.range, this.year);
1194
+
1195
+ Array.from(this.grid.children).forEach((el, index) => {
1196
+ const classList = el.classList;
1197
+ const date = dateValue(this.year, index, 1);
1198
+
1199
+ el.className = `datepicker-cell hover:bg-neutral-tertiary-medium block flex-1 leading-9 border-0 rounded-base cursor-pointer text-center text-body font-medium text-sm ${this.cellClass}`;
1200
+ if (this.isMinView) {
1201
+ el.dataset.date = date;
1202
+ }
1203
+ // reset text on every render to clear the custom content set
1204
+ // by beforeShow hook at previous render
1205
+ el.textContent = this.monthNames[index];
1206
+
1207
+ if (
1208
+ yrOutOfRange
1209
+ || isMinYear && index < this.minMonth
1210
+ || isMaxYear && index > this.maxMonth
1211
+ ) {
1212
+ classList.add('disabled');
1213
+ }
1214
+ if (range) {
1215
+ const [rangeStart, rangeEnd] = range;
1216
+ if (index > rangeStart && index < rangeEnd) {
1217
+ classList.add('range');
1218
+ }
1219
+ if (index === rangeStart) {
1220
+ classList.add('range-start');
1221
+ }
1222
+ if (index === rangeEnd) {
1223
+ classList.add('range-end');
1224
+ }
1225
+ }
1226
+ if (selected.includes(index)) {
1227
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1228
+ classList.remove('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white');
1229
+ }
1230
+ if (index === this.focused) {
1231
+ classList.add('focused');
1232
+ }
1233
+
1234
+ if (this.beforeShow) {
1235
+ this.performBeforeHook(el, index, date);
1236
+ }
1237
+ });
1238
+ }
1239
+
1240
+ // Update the view UI by applying the changes of selected and focused items
1241
+ refresh() {
1242
+ const selected = this.selected[this.year] || [];
1243
+ const [rangeStart, rangeEnd] = computeMonthRange(this.range, this.year) || [];
1244
+ this.grid
1245
+ .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
1246
+ .forEach((el) => {
1247
+ el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'dark:bg-blue-600', 'dark:!bg-primary-700', 'dark:text-white', 'text-white', 'focused');
1248
+ el.classList.add('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white');
1249
+ });
1250
+ Array.from(this.grid.children).forEach((el, index) => {
1251
+ const classList = el.classList;
1252
+ if (index > rangeStart && index < rangeEnd) {
1253
+ classList.add('range');
1254
+ }
1255
+ if (index === rangeStart) {
1256
+ classList.add('range-start');
1257
+ }
1258
+ if (index === rangeEnd) {
1259
+ classList.add('range-end');
1260
+ }
1261
+ if (selected.includes(index)) {
1262
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1263
+ classList.remove('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white');
1264
+ }
1265
+ if (index === this.focused) {
1266
+ classList.add('focused');
1267
+ }
1268
+ });
1269
+ }
1270
+
1271
+ // Update the view UI by applying the change of focused item
1272
+ refreshFocus() {
1273
+ this.grid.querySelectorAll('.focused').forEach((el) => {
1274
+ el.classList.remove('focused');
1275
+ });
1276
+ this.grid.children[this.focused].classList.add('focused');
1277
+ }
1278
+ }
1279
+
1280
+ function toTitleCase(word) {
1281
+ return [...word].reduce((str, ch, ix) => str += ix ? ch : ch.toUpperCase(), '');
1282
+ }
1283
+
1284
+ // Class representing the years and decades view elements
1285
+ class YearsView extends View {
1286
+ constructor(picker, config) {
1287
+ super(picker, config);
1288
+ }
1289
+
1290
+ init(options, onConstruction = true) {
1291
+ if (onConstruction) {
1292
+ this.navStep = this.step * 10;
1293
+ this.beforeShowOption = `beforeShow${toTitleCase(this.cellClass)}`;
1294
+ this.grid = this.element;
1295
+ this.element.classList.add(this.name, 'datepicker-grid', 'w-64', 'grid', 'grid-cols-4');
1296
+ this.grid.appendChild(parseHTML(createTagRepeat('span', 12)));
1297
+ }
1298
+ super.init(options);
1299
+ }
1300
+
1301
+ setOptions(options) {
1302
+ if (hasProperty(options, 'minDate')) {
1303
+ if (options.minDate === undefined) {
1304
+ this.minYear = this.minDate = undefined;
1305
+ } else {
1306
+ this.minYear = startOfYearPeriod(options.minDate, this.step);
1307
+ this.minDate = dateValue(this.minYear, 0, 1);
1308
+ }
1309
+ }
1310
+ if (hasProperty(options, 'maxDate')) {
1311
+ if (options.maxDate === undefined) {
1312
+ this.maxYear = this.maxDate = undefined;
1313
+ } else {
1314
+ this.maxYear = startOfYearPeriod(options.maxDate, this.step);
1315
+ this.maxDate = dateValue(this.maxYear, 11, 31);
1316
+ }
1317
+ }
1318
+ if (options[this.beforeShowOption] !== undefined) {
1319
+ const beforeShow = options[this.beforeShowOption];
1320
+ this.beforeShow = typeof beforeShow === 'function' ? beforeShow : undefined;
1321
+ }
1322
+ }
1323
+
1324
+ // Update view's settings to reflect the viewDate set on the picker
1325
+ updateFocus() {
1326
+ const viewDate = new Date(this.picker.viewDate);
1327
+ const first = startOfYearPeriod(viewDate, this.navStep);
1328
+ const last = first + 9 * this.step;
1329
+
1330
+ this.first = first;
1331
+ this.last = last;
1332
+ this.start = first - this.step;
1333
+ this.focused = startOfYearPeriod(viewDate, this.step);
1334
+ }
1335
+
1336
+ // Update view's settings to reflect the selected dates
1337
+ updateSelection() {
1338
+ const {dates, rangepicker} = this.picker.datepicker;
1339
+ this.selected = dates.reduce((years, timeValue) => {
1340
+ return pushUnique(years, startOfYearPeriod(timeValue, this.step));
1341
+ }, []);
1342
+ if (rangepicker && rangepicker.dates) {
1343
+ this.range = rangepicker.dates.map(timeValue => {
1344
+ if (timeValue !== undefined) {
1345
+ return startOfYearPeriod(timeValue, this.step);
1346
+ }
1347
+ });
1348
+ }
1349
+ }
1350
+
1351
+ // Update the entire view UI
1352
+ render() {
1353
+ // refresh disabled years on every render in order to clear the ones added
1354
+ // by beforeShow hook at previous render
1355
+ this.disabled = [];
1356
+
1357
+ this.picker.setViewSwitchLabel(`${this.first}-${this.last}`);
1358
+ this.picker.setPrevBtnDisabled(this.first <= this.minYear);
1359
+ this.picker.setNextBtnDisabled(this.last >= this.maxYear);
1360
+
1361
+ Array.from(this.grid.children).forEach((el, index) => {
1362
+ const classList = el.classList;
1363
+ const current = this.start + (index * this.step);
1364
+ const date = dateValue(current, 0, 1);
1365
+
1366
+ el.className = `datepicker-cell hover:bg-neutral-tertiary-medium block flex-1 leading-9 border-0 rounded-base cursor-pointer text-center text-body font-medium text-sm ${this.cellClass}`;
1367
+ if (this.isMinView) {
1368
+ el.dataset.date = date;
1369
+ }
1370
+ el.textContent = el.dataset.year = current;
1371
+
1372
+ if (index === 0) {
1373
+ classList.add('prev');
1374
+ } else if (index === 11) {
1375
+ classList.add('next');
1376
+ }
1377
+ if (current < this.minYear || current > this.maxYear) {
1378
+ classList.add('disabled');
1379
+ }
1380
+ if (this.range) {
1381
+ const [rangeStart, rangeEnd] = this.range;
1382
+ if (current > rangeStart && current < rangeEnd) {
1383
+ classList.add('range');
1384
+ }
1385
+ if (current === rangeStart) {
1386
+ classList.add('range-start');
1387
+ }
1388
+ if (current === rangeEnd) {
1389
+ classList.add('range-end');
1390
+ }
1391
+ }
1392
+ if (this.selected.includes(current)) {
1393
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1394
+ classList.remove('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white');
1395
+ }
1396
+ if (current === this.focused) {
1397
+ classList.add('focused');
1398
+ }
1399
+
1400
+ if (this.beforeShow) {
1401
+ this.performBeforeHook(el, current, date);
1402
+ }
1403
+ });
1404
+ }
1405
+
1406
+ // Update the view UI by applying the changes of selected and focused items
1407
+ refresh() {
1408
+ const [rangeStart, rangeEnd] = this.range || [];
1409
+ this.grid
1410
+ .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
1411
+ .forEach((el) => {
1412
+ el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark!bg-primary-600', 'dark:text-white', 'focused');
1413
+ });
1414
+ Array.from(this.grid.children).forEach((el) => {
1415
+ const current = Number(el.textContent);
1416
+ const classList = el.classList;
1417
+ if (current > rangeStart && current < rangeEnd) {
1418
+ classList.add('range');
1419
+ }
1420
+ if (current === rangeStart) {
1421
+ classList.add('range-start');
1422
+ }
1423
+ if (current === rangeEnd) {
1424
+ classList.add('range-end');
1425
+ }
1426
+ if (this.selected.includes(current)) {
1427
+ classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
1428
+ classList.remove('text-body', 'hover:bg-neutral-tertiary-medium', 'dark:text-white');
1429
+ }
1430
+ if (current === this.focused) {
1431
+ classList.add('focused');
1432
+ }
1433
+ });
1434
+ }
1435
+
1436
+ // Update the view UI by applying the change of focused item
1437
+ refreshFocus() {
1438
+ const index = Math.round((this.focused - this.start) / this.step);
1439
+ this.grid.querySelectorAll('.focused').forEach((el) => {
1440
+ el.classList.remove('focused');
1441
+ });
1442
+ this.grid.children[index].classList.add('focused');
1443
+ }
1444
+ }
1445
+
1446
+ function triggerDatepickerEvent(datepicker, type) {
1447
+ const detail = {
1448
+ date: datepicker.getDate(),
1449
+ viewDate: new Date(datepicker.picker.viewDate),
1450
+ viewId: datepicker.picker.currentView.id,
1451
+ datepicker,
1452
+ };
1453
+ datepicker.element.dispatchEvent(new CustomEvent(type, {detail}));
1454
+ }
1455
+
1456
+ // direction: -1 (to previous), 1 (to next)
1457
+ function goToPrevOrNext(datepicker, direction) {
1458
+ const {minDate, maxDate} = datepicker.config;
1459
+ const {currentView, viewDate} = datepicker.picker;
1460
+ let newViewDate;
1461
+ switch (currentView.id) {
1462
+ case 0:
1463
+ newViewDate = addMonths(viewDate, direction);
1464
+ break;
1465
+ case 1:
1466
+ newViewDate = addYears(viewDate, direction);
1467
+ break;
1468
+ default:
1469
+ newViewDate = addYears(viewDate, direction * currentView.navStep);
1470
+ }
1471
+ newViewDate = limitToRange(newViewDate, minDate, maxDate);
1472
+ datepicker.picker.changeFocus(newViewDate).render();
1473
+ }
1474
+
1475
+ function switchView(datepicker) {
1476
+ const viewId = datepicker.picker.currentView.id;
1477
+ if (viewId === datepicker.config.maxView) {
1478
+ return;
1479
+ }
1480
+ datepicker.picker.changeView(viewId + 1).render();
1481
+ }
1482
+
1483
+ function unfocus(datepicker) {
1484
+ if (datepicker.config.updateOnBlur) {
1485
+ datepicker.update({autohide: true});
1486
+ } else {
1487
+ datepicker.refresh('input');
1488
+ datepicker.hide();
1489
+ }
1490
+ }
1491
+
1492
+ function goToSelectedMonthOrYear(datepicker, selection) {
1493
+ const picker = datepicker.picker;
1494
+ const viewDate = new Date(picker.viewDate);
1495
+ const viewId = picker.currentView.id;
1496
+ const newDate = viewId === 1
1497
+ ? addMonths(viewDate, selection - viewDate.getMonth())
1498
+ : addYears(viewDate, selection - viewDate.getFullYear());
1499
+
1500
+ picker.changeFocus(newDate).changeView(viewId - 1).render();
1501
+ }
1502
+
1503
+ function onClickTodayBtn(datepicker) {
1504
+ const picker = datepicker.picker;
1505
+ const currentDate = today();
1506
+ if (datepicker.config.todayBtnMode === 1) {
1507
+ if (datepicker.config.autohide) {
1508
+ datepicker.setDate(currentDate);
1509
+ return;
1510
+ }
1511
+ datepicker.setDate(currentDate, {render: false});
1512
+ picker.update();
1513
+ }
1514
+ if (picker.viewDate !== currentDate) {
1515
+ picker.changeFocus(currentDate);
1516
+ }
1517
+ picker.changeView(0).render();
1518
+ }
1519
+
1520
+ function onClickClearBtn(datepicker) {
1521
+ datepicker.setDate({clear: true});
1522
+ }
1523
+
1524
+ function onClickViewSwitch(datepicker) {
1525
+ switchView(datepicker);
1526
+ }
1527
+
1528
+ function onClickPrevBtn(datepicker) {
1529
+ goToPrevOrNext(datepicker, -1);
1530
+ }
1531
+
1532
+ function onClickNextBtn(datepicker) {
1533
+ goToPrevOrNext(datepicker, 1);
1534
+ }
1535
+
1536
+ // For the picker's main block to delegete the events from `datepicker-cell`s
1537
+ function onClickView(datepicker, ev) {
1538
+ const target = findElementInEventPath(ev, '.datepicker-cell');
1539
+ if (!target || target.classList.contains('disabled')) {
1540
+ return;
1541
+ }
1542
+
1543
+ const {id, isMinView} = datepicker.picker.currentView;
1544
+ if (isMinView) {
1545
+ datepicker.setDate(Number(target.dataset.date));
1546
+ } else if (id === 1) {
1547
+ goToSelectedMonthOrYear(datepicker, Number(target.dataset.month));
1548
+ } else {
1549
+ goToSelectedMonthOrYear(datepicker, Number(target.dataset.year));
1550
+ }
1551
+ }
1552
+
1553
+ function onClickPicker(datepicker) {
1554
+ if (!datepicker.inline && !datepicker.config.disableTouchKeyboard) {
1555
+ datepicker.inputField.focus();
1556
+ }
1557
+ }
1558
+
1559
+ function processPickerOptions(picker, options) {
1560
+ if (options.title !== undefined) {
1561
+ if (options.title) {
1562
+ picker.controls.title.textContent = options.title;
1563
+ showElement(picker.controls.title);
1564
+ } else {
1565
+ picker.controls.title.textContent = '';
1566
+ hideElement(picker.controls.title);
1567
+ }
1568
+ }
1569
+ if (options.prevArrow) {
1570
+ const prevBtn = picker.controls.prevBtn;
1571
+ emptyChildNodes(prevBtn);
1572
+ options.prevArrow.forEach((node) => {
1573
+ prevBtn.appendChild(node.cloneNode(true));
1574
+ });
1575
+ }
1576
+ if (options.nextArrow) {
1577
+ const nextBtn = picker.controls.nextBtn;
1578
+ emptyChildNodes(nextBtn);
1579
+ options.nextArrow.forEach((node) => {
1580
+ nextBtn.appendChild(node.cloneNode(true));
1581
+ });
1582
+ }
1583
+ if (options.locale) {
1584
+ picker.controls.todayBtn.textContent = options.locale.today;
1585
+ picker.controls.clearBtn.textContent = options.locale.clear;
1586
+ }
1587
+ if (options.todayBtn !== undefined) {
1588
+ if (options.todayBtn) {
1589
+ showElement(picker.controls.todayBtn);
1590
+ } else {
1591
+ hideElement(picker.controls.todayBtn);
1592
+ }
1593
+ }
1594
+ if (hasProperty(options, 'minDate') || hasProperty(options, 'maxDate')) {
1595
+ const {minDate, maxDate} = picker.datepicker.config;
1596
+ picker.controls.todayBtn.disabled = !isInRange(today(), minDate, maxDate);
1597
+ }
1598
+ if (options.clearBtn !== undefined) {
1599
+ if (options.clearBtn) {
1600
+ showElement(picker.controls.clearBtn);
1601
+ } else {
1602
+ hideElement(picker.controls.clearBtn);
1603
+ }
1604
+ }
1605
+ }
1606
+
1607
+ // Compute view date to reset, which will be...
1608
+ // - the last item of the selected dates or defaultViewDate if no selection
1609
+ // - limitted to minDate or maxDate if it exceeds the range
1610
+ function computeResetViewDate(datepicker) {
1611
+ const {dates, config} = datepicker;
1612
+ const viewDate = dates.length > 0 ? lastItemOf(dates) : config.defaultViewDate;
1613
+ return limitToRange(viewDate, config.minDate, config.maxDate);
1614
+ }
1615
+
1616
+ // Change current view's view date
1617
+ function setViewDate(picker, newDate) {
1618
+ const oldViewDate = new Date(picker.viewDate);
1619
+ const newViewDate = new Date(newDate);
1620
+ const {id, year, first, last} = picker.currentView;
1621
+ const viewYear = newViewDate.getFullYear();
1622
+
1623
+ picker.viewDate = newDate;
1624
+ if (viewYear !== oldViewDate.getFullYear()) {
1625
+ triggerDatepickerEvent(picker.datepicker, 'changeYear');
1626
+ }
1627
+ if (newViewDate.getMonth() !== oldViewDate.getMonth()) {
1628
+ triggerDatepickerEvent(picker.datepicker, 'changeMonth');
1629
+ }
1630
+
1631
+ // return whether the new date is in different period on time from the one
1632
+ // displayed in the current view
1633
+ // when true, the view needs to be re-rendered on the next UI refresh.
1634
+ switch (id) {
1635
+ case 0:
1636
+ return newDate < first || newDate > last;
1637
+ case 1:
1638
+ return viewYear !== year;
1639
+ default:
1640
+ return viewYear < first || viewYear > last;
1641
+ }
1642
+ }
1643
+
1644
+ function getTextDirection(el) {
1645
+ return window.getComputedStyle(el).direction;
1646
+ }
1647
+
1648
+ // Class representing the picker UI
1649
+ class Picker {
1650
+ constructor(datepicker) {
1651
+ this.datepicker = datepicker;
1652
+
1653
+ const template = pickerTemplate.replace(/%buttonClass%/g, datepicker.config.buttonClass);
1654
+ const element = this.element = parseHTML(template).firstChild;
1655
+ const [header, main, footer] = element.firstChild.children;
1656
+ const title = header.firstElementChild;
1657
+ const [prevBtn, viewSwitch, nextBtn] = header.lastElementChild.children;
1658
+ const [todayBtn, clearBtn] = footer.firstChild.children;
1659
+ const controls = {
1660
+ title,
1661
+ prevBtn,
1662
+ viewSwitch,
1663
+ nextBtn,
1664
+ todayBtn,
1665
+ clearBtn,
1666
+ };
1667
+ this.main = main;
1668
+ this.controls = controls;
1669
+
1670
+ const elementClass = datepicker.inline ? 'inline' : 'dropdown';
1671
+ element.classList.add(`datepicker-${elementClass}`);
1672
+ elementClass === 'dropdown' ? element.classList.add('dropdown', 'absolute', 'top-0', 'left-0', 'z-50', 'pt-2') : null;
1673
+
1674
+ processPickerOptions(this, datepicker.config);
1675
+ this.viewDate = computeResetViewDate(datepicker);
1676
+
1677
+ // set up event listeners
1678
+ registerListeners(datepicker, [
1679
+ [element, 'click', onClickPicker.bind(null, datepicker), {capture: true}],
1680
+ [main, 'click', onClickView.bind(null, datepicker)],
1681
+ [controls.viewSwitch, 'click', onClickViewSwitch.bind(null, datepicker)],
1682
+ [controls.prevBtn, 'click', onClickPrevBtn.bind(null, datepicker)],
1683
+ [controls.nextBtn, 'click', onClickNextBtn.bind(null, datepicker)],
1684
+ [controls.todayBtn, 'click', onClickTodayBtn.bind(null, datepicker)],
1685
+ [controls.clearBtn, 'click', onClickClearBtn.bind(null, datepicker)],
1686
+ ]);
1687
+
1688
+ // set up views
1689
+ this.views = [
1690
+ new DaysView(this),
1691
+ new MonthsView(this),
1692
+ new YearsView(this, {id: 2, name: 'years', cellClass: 'year', step: 1}),
1693
+ new YearsView(this, {id: 3, name: 'decades', cellClass: 'decade', step: 10}),
1694
+ ];
1695
+ this.currentView = this.views[datepicker.config.startView];
1696
+
1697
+ this.currentView.render();
1698
+ this.main.appendChild(this.currentView.element);
1699
+ datepicker.config.container.appendChild(this.element);
1700
+ }
1701
+
1702
+ setOptions(options) {
1703
+ processPickerOptions(this, options);
1704
+ this.views.forEach((view) => {
1705
+ view.init(options, false);
1706
+ });
1707
+ this.currentView.render();
1708
+ }
1709
+
1710
+ detach() {
1711
+ this.datepicker.config.container.removeChild(this.element);
1712
+ }
1713
+
1714
+ show() {
1715
+ if (this.active) {
1716
+ return;
1717
+ }
1718
+ this.element.classList.add('active', 'block');
1719
+ this.element.classList.remove('hidden');
1720
+ this.active = true;
1721
+
1722
+ const datepicker = this.datepicker;
1723
+ if (!datepicker.inline) {
1724
+ // ensure picker's direction matches input's
1725
+ const inputDirection = getTextDirection(datepicker.inputField);
1726
+ if (inputDirection !== getTextDirection(datepicker.config.container)) {
1727
+ this.element.dir = inputDirection;
1728
+ } else if (this.element.dir) {
1729
+ this.element.removeAttribute('dir');
1730
+ }
1731
+
1732
+ this.place();
1733
+ if (datepicker.config.disableTouchKeyboard) {
1734
+ datepicker.inputField.blur();
1735
+ }
1736
+ }
1737
+ triggerDatepickerEvent(datepicker, 'show');
1738
+ }
1739
+
1740
+ hide() {
1741
+ if (!this.active) {
1742
+ return;
1743
+ }
1744
+ this.datepicker.exitEditMode();
1745
+ this.element.classList.remove('active', 'block');
1746
+ this.element.classList.add('active', 'block', 'hidden');
1747
+ this.active = false;
1748
+ triggerDatepickerEvent(this.datepicker, 'hide');
1749
+ }
1750
+
1751
+ place() {
1752
+ const {classList, style} = this.element;
1753
+ const {config, inputField} = this.datepicker;
1754
+ const container = config.container;
1755
+ const {
1756
+ width: calendarWidth,
1757
+ height: calendarHeight,
1758
+ } = this.element.getBoundingClientRect();
1759
+ const {
1760
+ left: containerLeft,
1761
+ top: containerTop,
1762
+ width: containerWidth,
1763
+ } = container.getBoundingClientRect();
1764
+ const {
1765
+ left: inputLeft,
1766
+ top: inputTop,
1767
+ width: inputWidth,
1768
+ height: inputHeight
1769
+ } = inputField.getBoundingClientRect();
1770
+ let {x: orientX, y: orientY} = config.orientation;
1771
+ let scrollTop;
1772
+ let left;
1773
+ let top;
1774
+
1775
+ if (container === document.body) {
1776
+ scrollTop = window.scrollY;
1777
+ left = inputLeft + window.scrollX;
1778
+ top = inputTop + scrollTop;
1779
+ } else {
1780
+ scrollTop = container.scrollTop;
1781
+ left = inputLeft - containerLeft;
1782
+ top = inputTop - containerTop + scrollTop;
1783
+ }
1784
+
1785
+ if (orientX === 'auto') {
1786
+ if (left < 0) {
1787
+ // align to the left and move into visible area if input's left edge < window's
1788
+ orientX = 'left';
1789
+ left = 10;
1790
+ } else if (left + calendarWidth > containerWidth) {
1791
+ // align to the right if canlendar's right edge > container's
1792
+ orientX = 'right';
1793
+ } else {
1794
+ orientX = getTextDirection(inputField) === 'rtl' ? 'right' : 'left';
1795
+ }
1796
+ }
1797
+ if (orientX === 'right') {
1798
+ left -= calendarWidth - inputWidth;
1799
+ }
1800
+
1801
+ if (orientY === 'auto') {
1802
+ orientY = top - calendarHeight < scrollTop ? 'bottom' : 'top';
1803
+ }
1804
+ if (orientY === 'top') {
1805
+ top -= calendarHeight;
1806
+ } else {
1807
+ top += inputHeight;
1808
+ }
1809
+
1810
+ classList.remove(
1811
+ 'datepicker-orient-top',
1812
+ 'datepicker-orient-bottom',
1813
+ 'datepicker-orient-right',
1814
+ 'datepicker-orient-left'
1815
+ );
1816
+ classList.add(`datepicker-orient-${orientY}`, `datepicker-orient-${orientX}`);
1817
+
1818
+ style.top = top ? `${top}px` : top;
1819
+ style.left = left ? `${left}px` : left;
1820
+ }
1821
+
1822
+ setViewSwitchLabel(labelText) {
1823
+ this.controls.viewSwitch.textContent = labelText;
1824
+ }
1825
+
1826
+ setPrevBtnDisabled(disabled) {
1827
+ this.controls.prevBtn.disabled = disabled;
1828
+ }
1829
+
1830
+ setNextBtnDisabled(disabled) {
1831
+ this.controls.nextBtn.disabled = disabled;
1832
+ }
1833
+
1834
+ changeView(viewId) {
1835
+ const oldView = this.currentView;
1836
+ const newView = this.views[viewId];
1837
+ if (newView.id !== oldView.id) {
1838
+ this.currentView = newView;
1839
+ this._renderMethod = 'render';
1840
+ triggerDatepickerEvent(this.datepicker, 'changeView');
1841
+ this.main.replaceChild(newView.element, oldView.element);
1842
+ }
1843
+ return this;
1844
+ }
1845
+
1846
+ // Change the focused date (view date)
1847
+ changeFocus(newViewDate) {
1848
+ this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refreshFocus';
1849
+ this.views.forEach((view) => {
1850
+ view.updateFocus();
1851
+ });
1852
+ return this;
1853
+ }
1854
+
1855
+ // Apply the change of the selected dates
1856
+ update() {
1857
+ const newViewDate = computeResetViewDate(this.datepicker);
1858
+ this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refresh';
1859
+ this.views.forEach((view) => {
1860
+ view.updateFocus();
1861
+ view.updateSelection();
1862
+ });
1863
+ return this;
1864
+ }
1865
+
1866
+ // Refresh the picker UI
1867
+ render(quickRender = true) {
1868
+ const renderMethod = (quickRender && this._renderMethod) || 'render';
1869
+ delete this._renderMethod;
1870
+
1871
+ this.currentView[renderMethod]();
1872
+ }
1873
+ }
1874
+
1875
+ // Find the closest date that doesn't meet the condition for unavailable date
1876
+ // Returns undefined if no available date is found
1877
+ // addFn: function to calculate the next date
1878
+ // - args: time value, amount
1879
+ // increase: amount to pass to addFn
1880
+ // testFn: function to test the unavailablity of the date
1881
+ // - args: time value; retun: true if unavailable
1882
+ function findNextAvailableOne(date, addFn, increase, testFn, min, max) {
1883
+ if (!isInRange(date, min, max)) {
1884
+ return;
1885
+ }
1886
+ if (testFn(date)) {
1887
+ const newDate = addFn(date, increase);
1888
+ return findNextAvailableOne(newDate, addFn, increase, testFn, min, max);
1889
+ }
1890
+ return date;
1891
+ }
1892
+
1893
+ // direction: -1 (left/up), 1 (right/down)
1894
+ // vertical: true for up/down, false for left/right
1895
+ function moveByArrowKey(datepicker, ev, direction, vertical) {
1896
+ const picker = datepicker.picker;
1897
+ const currentView = picker.currentView;
1898
+ const step = currentView.step || 1;
1899
+ let viewDate = picker.viewDate;
1900
+ let addFn;
1901
+ let testFn;
1902
+ switch (currentView.id) {
1903
+ case 0:
1904
+ if (vertical) {
1905
+ viewDate = addDays(viewDate, direction * 7);
1906
+ } else if (ev.ctrlKey || ev.metaKey) {
1907
+ viewDate = addYears(viewDate, direction);
1908
+ } else {
1909
+ viewDate = addDays(viewDate, direction);
1910
+ }
1911
+ addFn = addDays;
1912
+ testFn = (date) => currentView.disabled.includes(date);
1913
+ break;
1914
+ case 1:
1915
+ viewDate = addMonths(viewDate, vertical ? direction * 4 : direction);
1916
+ addFn = addMonths;
1917
+ testFn = (date) => {
1918
+ const dt = new Date(date);
1919
+ const {year, disabled} = currentView;
1920
+ return dt.getFullYear() === year && disabled.includes(dt.getMonth());
1921
+ };
1922
+ break;
1923
+ default:
1924
+ viewDate = addYears(viewDate, direction * (vertical ? 4 : 1) * step);
1925
+ addFn = addYears;
1926
+ testFn = date => currentView.disabled.includes(startOfYearPeriod(date, step));
1927
+ }
1928
+ viewDate = findNextAvailableOne(
1929
+ viewDate,
1930
+ addFn,
1931
+ direction < 0 ? -step : step,
1932
+ testFn,
1933
+ currentView.minDate,
1934
+ currentView.maxDate
1935
+ );
1936
+ if (viewDate !== undefined) {
1937
+ picker.changeFocus(viewDate).render();
1938
+ }
1939
+ }
1940
+
1941
+ function onKeydown(datepicker, ev) {
1942
+ if (ev.key === 'Tab') {
1943
+ unfocus(datepicker);
1944
+ return;
1945
+ }
1946
+
1947
+ const picker = datepicker.picker;
1948
+ const {id, isMinView} = picker.currentView;
1949
+ if (!picker.active) {
1950
+ switch (ev.key) {
1951
+ case 'ArrowDown':
1952
+ case 'Escape':
1953
+ picker.show();
1954
+ break;
1955
+ case 'Enter':
1956
+ datepicker.update();
1957
+ break;
1958
+ default:
1959
+ return;
1960
+ }
1961
+ } else if (datepicker.editMode) {
1962
+ switch (ev.key) {
1963
+ case 'Escape':
1964
+ picker.hide();
1965
+ break;
1966
+ case 'Enter':
1967
+ datepicker.exitEditMode({update: true, autohide: datepicker.config.autohide});
1968
+ break;
1969
+ default:
1970
+ return;
1971
+ }
1972
+ } else {
1973
+ switch (ev.key) {
1974
+ case 'Escape':
1975
+ picker.hide();
1976
+ break;
1977
+ case 'ArrowLeft':
1978
+ if (ev.ctrlKey || ev.metaKey) {
1979
+ goToPrevOrNext(datepicker, -1);
1980
+ } else if (ev.shiftKey) {
1981
+ datepicker.enterEditMode();
1982
+ return;
1983
+ } else {
1984
+ moveByArrowKey(datepicker, ev, -1, false);
1985
+ }
1986
+ break;
1987
+ case 'ArrowRight':
1988
+ if (ev.ctrlKey || ev.metaKey) {
1989
+ goToPrevOrNext(datepicker, 1);
1990
+ } else if (ev.shiftKey) {
1991
+ datepicker.enterEditMode();
1992
+ return;
1993
+ } else {
1994
+ moveByArrowKey(datepicker, ev, 1, false);
1995
+ }
1996
+ break;
1997
+ case 'ArrowUp':
1998
+ if (ev.ctrlKey || ev.metaKey) {
1999
+ switchView(datepicker);
2000
+ } else if (ev.shiftKey) {
2001
+ datepicker.enterEditMode();
2002
+ return;
2003
+ } else {
2004
+ moveByArrowKey(datepicker, ev, -1, true);
2005
+ }
2006
+ break;
2007
+ case 'ArrowDown':
2008
+ if (ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
2009
+ datepicker.enterEditMode();
2010
+ return;
2011
+ }
2012
+ moveByArrowKey(datepicker, ev, 1, true);
2013
+ break;
2014
+ case 'Enter':
2015
+ if (isMinView) {
2016
+ datepicker.setDate(picker.viewDate);
2017
+ } else {
2018
+ picker.changeView(id - 1).render();
2019
+ }
2020
+ break;
2021
+ case 'Backspace':
2022
+ case 'Delete':
2023
+ datepicker.enterEditMode();
2024
+ return;
2025
+ default:
2026
+ if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey) {
2027
+ datepicker.enterEditMode();
2028
+ }
2029
+ return;
2030
+ }
2031
+ }
2032
+ ev.preventDefault();
2033
+ ev.stopPropagation();
2034
+ }
2035
+
2036
+ function onFocus(datepicker) {
2037
+ if (datepicker.config.showOnFocus && !datepicker._showing) {
2038
+ datepicker.show();
2039
+ }
2040
+ }
2041
+
2042
+ // for the prevention for entering edit mode while getting focus on click
2043
+ function onMousedown(datepicker, ev) {
2044
+ const el = ev.target;
2045
+ if (datepicker.picker.active || datepicker.config.showOnClick) {
2046
+ el._active = el === document.activeElement;
2047
+ el._clicking = setTimeout(() => {
2048
+ delete el._active;
2049
+ delete el._clicking;
2050
+ }, 2000);
2051
+ }
2052
+ }
2053
+
2054
+ function onClickInput(datepicker, ev) {
2055
+ const el = ev.target;
2056
+ if (!el._clicking) {
2057
+ return;
2058
+ }
2059
+ clearTimeout(el._clicking);
2060
+ delete el._clicking;
2061
+
2062
+ if (el._active) {
2063
+ datepicker.enterEditMode();
2064
+ }
2065
+ delete el._active;
2066
+
2067
+ if (datepicker.config.showOnClick) {
2068
+ datepicker.show();
2069
+ }
2070
+ }
2071
+
2072
+ function onPaste(datepicker, ev) {
2073
+ if (ev.clipboardData.types.includes('text/plain')) {
2074
+ datepicker.enterEditMode();
2075
+ }
2076
+ }
2077
+
2078
+ // for the `document` to delegate the events from outside the picker/input field
2079
+ function onClickOutside(datepicker, ev) {
2080
+ const element = datepicker.element;
2081
+ if (element !== document.activeElement) {
2082
+ return;
2083
+ }
2084
+ const pickerElem = datepicker.picker.element;
2085
+ if (findElementInEventPath(ev, el => el === element || el === pickerElem)) {
2086
+ return;
2087
+ }
2088
+ unfocus(datepicker);
2089
+ }
2090
+
2091
+ function stringifyDates(dates, config) {
2092
+ return dates
2093
+ .map(dt => formatDate(dt, config.format, config.locale))
2094
+ .join(config.dateDelimiter);
2095
+ }
2096
+
2097
+ // parse input dates and create an array of time values for selection
2098
+ // returns undefined if there are no valid dates in inputDates
2099
+ // when origDates (current selection) is passed, the function works to mix
2100
+ // the input dates into the current selection
2101
+ function processInputDates(datepicker, inputDates, clear = false) {
2102
+ const {config, dates: origDates, rangepicker} = datepicker;
2103
+ if (inputDates.length === 0) {
2104
+ // empty input is considered valid unless origiDates is passed
2105
+ return clear ? [] : undefined;
2106
+ }
2107
+
2108
+ const rangeEnd = rangepicker && datepicker === rangepicker.datepickers[1];
2109
+ let newDates = inputDates.reduce((dates, dt) => {
2110
+ let date = parseDate(dt, config.format, config.locale);
2111
+ if (date === undefined) {
2112
+ return dates;
2113
+ }
2114
+ if (config.pickLevel > 0) {
2115
+ // adjust to 1st of the month/Jan 1st of the year
2116
+ // or to the last day of the monh/Dec 31st of the year if the datepicker
2117
+ // is the range-end picker of a rangepicker
2118
+ const dt = new Date(date);
2119
+ if (config.pickLevel === 1) {
2120
+ date = rangeEnd
2121
+ ? dt.setMonth(dt.getMonth() + 1, 0)
2122
+ : dt.setDate(1);
2123
+ } else {
2124
+ date = rangeEnd
2125
+ ? dt.setFullYear(dt.getFullYear() + 1, 0, 0)
2126
+ : dt.setMonth(0, 1);
2127
+ }
2128
+ }
2129
+ if (
2130
+ isInRange(date, config.minDate, config.maxDate)
2131
+ && !dates.includes(date)
2132
+ && !config.datesDisabled.includes(date)
2133
+ && !config.daysOfWeekDisabled.includes(new Date(date).getDay())
2134
+ ) {
2135
+ dates.push(date);
2136
+ }
2137
+ return dates;
2138
+ }, []);
2139
+ if (newDates.length === 0) {
2140
+ return;
2141
+ }
2142
+ if (config.multidate && !clear) {
2143
+ // get the synmetric difference between origDates and newDates
2144
+ newDates = newDates.reduce((dates, date) => {
2145
+ if (!origDates.includes(date)) {
2146
+ dates.push(date);
2147
+ }
2148
+ return dates;
2149
+ }, origDates.filter(date => !newDates.includes(date)));
2150
+ }
2151
+ // do length check always because user can input multiple dates regardless of the mode
2152
+ return config.maxNumberOfDates && newDates.length > config.maxNumberOfDates
2153
+ ? newDates.slice(config.maxNumberOfDates * -1)
2154
+ : newDates;
2155
+ }
2156
+
2157
+ // refresh the UI elements
2158
+ // modes: 1: input only, 2, picker only, 3 both
2159
+ function refreshUI(datepicker, mode = 3, quickRender = true) {
2160
+ const {config, picker, inputField} = datepicker;
2161
+ if (mode & 2) {
2162
+ const newView = picker.active ? config.pickLevel : config.startView;
2163
+ picker.update().changeView(newView).render(quickRender);
2164
+ }
2165
+ if (mode & 1 && inputField) {
2166
+ inputField.value = stringifyDates(datepicker.dates, config);
2167
+ }
2168
+ }
2169
+
2170
+ function setDate(datepicker, inputDates, options) {
2171
+ let {clear, render, autohide} = options;
2172
+ if (render === undefined) {
2173
+ render = true;
2174
+ }
2175
+ if (!render) {
2176
+ autohide = false;
2177
+ } else if (autohide === undefined) {
2178
+ autohide = datepicker.config.autohide;
2179
+ }
2180
+
2181
+ const newDates = processInputDates(datepicker, inputDates, clear);
2182
+ if (!newDates) {
2183
+ return;
2184
+ }
2185
+ if (newDates.toString() !== datepicker.dates.toString()) {
2186
+ datepicker.dates = newDates;
2187
+ refreshUI(datepicker, render ? 3 : 1);
2188
+ triggerDatepickerEvent(datepicker, 'changeDate');
2189
+ } else {
2190
+ refreshUI(datepicker, 1);
2191
+ }
2192
+ if (autohide) {
2193
+ datepicker.hide();
2194
+ }
2195
+ }
2196
+
2197
+ /**
2198
+ * Class representing a date picker
2199
+ */
2200
+ class Datepicker {
2201
+ /**
2202
+ * Create a date picker
2203
+ * @param {Element} element - element to bind a date picker
2204
+ * @param {Object} [options] - config options
2205
+ * @param {DateRangePicker} [rangepicker] - DateRangePicker instance the
2206
+ * date picker belongs to. Use this only when creating date picker as a part
2207
+ * of date range picker
2208
+ */
2209
+ constructor(element, options = {}, rangepicker = undefined) {
2210
+ element.datepicker = this;
2211
+ this.element = element;
2212
+
2213
+ // set up config
2214
+ const config = this.config = Object.assign({
2215
+ buttonClass: (options.buttonClass && String(options.buttonClass)) || 'button',
2216
+ container: document.body,
2217
+ defaultViewDate: today(),
2218
+ maxDate: undefined,
2219
+ minDate: undefined,
2220
+ }, processOptions(defaultOptions, this));
2221
+ this._options = options;
2222
+ Object.assign(config, processOptions(options, this));
2223
+
2224
+ // configure by type
2225
+ const inline = this.inline = element.tagName !== 'INPUT';
2226
+ let inputField;
2227
+ let initialDates;
2228
+
2229
+ if (inline) {
2230
+ config.container = element;
2231
+ initialDates = stringToArray(element.dataset.date, config.dateDelimiter);
2232
+ delete element.dataset.date;
2233
+ } else {
2234
+ const container = options.container ? document.querySelector(options.container) : null;
2235
+ if (container) {
2236
+ config.container = container;
2237
+ }
2238
+ inputField = this.inputField = element;
2239
+ inputField.classList.add('datepicker-input');
2240
+ initialDates = stringToArray(inputField.value, config.dateDelimiter);
2241
+ }
2242
+ if (rangepicker) {
2243
+ // check validiry
2244
+ const index = rangepicker.inputs.indexOf(inputField);
2245
+ const datepickers = rangepicker.datepickers;
2246
+ if (index < 0 || index > 1 || !Array.isArray(datepickers)) {
2247
+ throw Error('Invalid rangepicker object.');
2248
+ }
2249
+ // attach itaelf to the rangepicker here so that processInputDates() can
2250
+ // determine if this is the range-end picker of the rangepicker while
2251
+ // setting inital values when pickLevel > 0
2252
+ datepickers[index] = this;
2253
+ // add getter for rangepicker
2254
+ Object.defineProperty(this, 'rangepicker', {
2255
+ get() {
2256
+ return rangepicker;
2257
+ },
2258
+ });
2259
+ }
2260
+
2261
+ // set initial dates
2262
+ this.dates = [];
2263
+ // process initial value
2264
+ const inputDateValues = processInputDates(this, initialDates);
2265
+ if (inputDateValues && inputDateValues.length > 0) {
2266
+ this.dates = inputDateValues;
2267
+ }
2268
+ if (inputField) {
2269
+ inputField.value = stringifyDates(this.dates, config);
2270
+ }
2271
+
2272
+ const picker = this.picker = new Picker(this);
2273
+
2274
+ if (inline) {
2275
+ this.show();
2276
+ } else {
2277
+ // set up event listeners in other modes
2278
+ const onMousedownDocument = onClickOutside.bind(null, this);
2279
+ const listeners = [
2280
+ [inputField, 'keydown', onKeydown.bind(null, this)],
2281
+ [inputField, 'focus', onFocus.bind(null, this)],
2282
+ [inputField, 'mousedown', onMousedown.bind(null, this)],
2283
+ [inputField, 'click', onClickInput.bind(null, this)],
2284
+ [inputField, 'paste', onPaste.bind(null, this)],
2285
+ [document, 'mousedown', onMousedownDocument],
2286
+ [document, 'touchstart', onMousedownDocument],
2287
+ [window, 'resize', picker.place.bind(picker)]
2288
+ ];
2289
+ registerListeners(this, listeners);
2290
+ }
2291
+ }
2292
+
2293
+ /**
2294
+ * Format Date object or time value in given format and language
2295
+ * @param {Date|Number} date - date or time value to format
2296
+ * @param {String|Object} format - format string or object that contains
2297
+ * toDisplay() custom formatter, whose signature is
2298
+ * - args:
2299
+ * - date: {Date} - Date instance of the date passed to the method
2300
+ * - format: {Object} - the format object passed to the method
2301
+ * - locale: {Object} - locale for the language specified by `lang`
2302
+ * - return:
2303
+ * {String} formatted date
2304
+ * @param {String} [lang=en] - language code for the locale to use
2305
+ * @return {String} formatted date
2306
+ */
2307
+ static formatDate(date, format, lang) {
2308
+ return formatDate(date, format, lang && locales[lang] || locales.en);
2309
+ }
2310
+
2311
+ /**
2312
+ * Parse date string
2313
+ * @param {String|Date|Number} dateStr - date string, Date object or time
2314
+ * value to parse
2315
+ * @param {String|Object} format - format string or object that contains
2316
+ * toValue() custom parser, whose signature is
2317
+ * - args:
2318
+ * - dateStr: {String|Date|Number} - the dateStr passed to the method
2319
+ * - format: {Object} - the format object passed to the method
2320
+ * - locale: {Object} - locale for the language specified by `lang`
2321
+ * - return:
2322
+ * {Date|Number} parsed date or its time value
2323
+ * @param {String} [lang=en] - language code for the locale to use
2324
+ * @return {Number} time value of parsed date
2325
+ */
2326
+ static parseDate(dateStr, format, lang) {
2327
+ return parseDate(dateStr, format, lang && locales[lang] || locales.en);
2328
+ }
2329
+
2330
+ /**
2331
+ * @type {Object} - Installed locales in `[languageCode]: localeObject` format
2332
+ * en`:_English (US)_ is pre-installed.
2333
+ */
2334
+ static get locales() {
2335
+ return locales;
2336
+ }
2337
+
2338
+ /**
2339
+ * @type {Boolean} - Whether the picker element is shown. `true` whne shown
2340
+ */
2341
+ get active() {
2342
+ return !!(this.picker && this.picker.active);
2343
+ }
2344
+
2345
+ /**
2346
+ * @type {HTMLDivElement} - DOM object of picker element
2347
+ */
2348
+ get pickerElement() {
2349
+ return this.picker ? this.picker.element : undefined;
2350
+ }
2351
+
2352
+ /**
2353
+ * Set new values to the config options
2354
+ * @param {Object} options - config options to update
2355
+ */
2356
+ setOptions(options) {
2357
+ const picker = this.picker;
2358
+ const newOptions = processOptions(options, this);
2359
+ Object.assign(this._options, options);
2360
+ Object.assign(this.config, newOptions);
2361
+ picker.setOptions(newOptions);
2362
+
2363
+ refreshUI(this, 3);
2364
+ }
2365
+
2366
+ /**
2367
+ * Show the picker element
2368
+ */
2369
+ show() {
2370
+ if (this.inputField) {
2371
+ if (this.inputField.disabled) {
2372
+ return;
2373
+ }
2374
+ if (this.inputField !== document.activeElement) {
2375
+ this._showing = true;
2376
+ this.inputField.focus();
2377
+ delete this._showing;
2378
+ }
2379
+ }
2380
+ this.picker.show();
2381
+ }
2382
+
2383
+ /**
2384
+ * Hide the picker element
2385
+ * Not available on inline picker
2386
+ */
2387
+ hide() {
2388
+ if (this.inline) {
2389
+ return;
2390
+ }
2391
+ this.picker.hide();
2392
+ this.picker.update().changeView(this.config.startView).render();
2393
+ }
2394
+
2395
+ /**
2396
+ * Destroy the Datepicker instance
2397
+ * @return {Detepicker} - the instance destroyed
2398
+ */
2399
+ destroy() {
2400
+ this.hide();
2401
+ unregisterListeners(this);
2402
+ this.picker.detach();
2403
+ if (!this.inline) {
2404
+ this.inputField.classList.remove('datepicker-input');
2405
+ }
2406
+ delete this.element.datepicker;
2407
+ return this;
2408
+ }
2409
+
2410
+ /**
2411
+ * Get the selected date(s)
2412
+ *
2413
+ * The method returns a Date object of selected date by default, and returns
2414
+ * an array of selected dates in multidate mode. If format string is passed,
2415
+ * it returns date string(s) formatted in given format.
2416
+ *
2417
+ * @param {String} [format] - Format string to stringify the date(s)
2418
+ * @return {Date|String|Date[]|String[]} - selected date(s), or if none is
2419
+ * selected, empty array in multidate mode and untitled in sigledate mode
2420
+ */
2421
+ getDate(format = undefined) {
2422
+ const callback = format
2423
+ ? date => formatDate(date, format, this.config.locale)
2424
+ : date => new Date(date);
2425
+
2426
+ if (this.config.multidate) {
2427
+ return this.dates.map(callback);
2428
+ }
2429
+ if (this.dates.length > 0) {
2430
+ return callback(this.dates[0]);
2431
+ }
2432
+ }
2433
+
2434
+ /**
2435
+ * Set selected date(s)
2436
+ *
2437
+ * In multidate mode, you can pass multiple dates as a series of arguments
2438
+ * or an array. (Since each date is parsed individually, the type of the
2439
+ * dates doesn't have to be the same.)
2440
+ * The given dates are used to toggle the select status of each date. The
2441
+ * number of selected dates is kept from exceeding the length set to
2442
+ * maxNumberOfDates.
2443
+ *
2444
+ * With clear: true option, the method can be used to clear the selection
2445
+ * and to replace the selection instead of toggling in multidate mode.
2446
+ * If the option is passed with no date arguments or an empty dates array,
2447
+ * it works as "clear" (clear the selection then set nothing), and if the
2448
+ * option is passed with new dates to select, it works as "replace" (clear
2449
+ * the selection then set the given dates)
2450
+ *
2451
+ * When render: false option is used, the method omits re-rendering the
2452
+ * picker element. In this case, you need to call refresh() method later in
2453
+ * order for the picker element to reflect the changes. The input field is
2454
+ * refreshed always regardless of this option.
2455
+ *
2456
+ * When invalid (unparsable, repeated, disabled or out-of-range) dates are
2457
+ * passed, the method ignores them and applies only valid ones. In the case
2458
+ * that all the given dates are invalid, which is distinguished from passing
2459
+ * no dates, the method considers it as an error and leaves the selection
2460
+ * untouched.
2461
+ *
2462
+ * @param {...(Date|Number|String)|Array} [dates] - Date strings, Date
2463
+ * objects, time values or mix of those for new selection
2464
+ * @param {Object} [options] - function options
2465
+ * - clear: {boolean} - Whether to clear the existing selection
2466
+ * defualt: false
2467
+ * - render: {boolean} - Whether to re-render the picker element
2468
+ * default: true
2469
+ * - autohide: {boolean} - Whether to hide the picker element after re-render
2470
+ * Ignored when used with render: false
2471
+ * default: config.autohide
2472
+ */
2473
+ setDate(...args) {
2474
+ const dates = [...args];
2475
+ const opts = {};
2476
+ const lastArg = lastItemOf(args);
2477
+ if (
2478
+ typeof lastArg === 'object'
2479
+ && !Array.isArray(lastArg)
2480
+ && !(lastArg instanceof Date)
2481
+ && lastArg
2482
+ ) {
2483
+ Object.assign(opts, dates.pop());
2484
+ }
2485
+
2486
+ const inputDates = Array.isArray(dates[0]) ? dates[0] : dates;
2487
+ setDate(this, inputDates, opts);
2488
+ }
2489
+
2490
+ /**
2491
+ * Update the selected date(s) with input field's value
2492
+ * Not available on inline picker
2493
+ *
2494
+ * The input field will be refreshed with properly formatted date string.
2495
+ *
2496
+ * @param {Object} [options] - function options
2497
+ * - autohide: {boolean} - whether to hide the picker element after refresh
2498
+ * default: false
2499
+ */
2500
+ update(options = undefined) {
2501
+ if (this.inline) {
2502
+ return;
2503
+ }
2504
+
2505
+ const opts = {clear: true, autohide: !!(options && options.autohide)};
2506
+ const inputDates = stringToArray(this.inputField.value, this.config.dateDelimiter);
2507
+ setDate(this, inputDates, opts);
2508
+ }
2509
+
2510
+ /**
2511
+ * Refresh the picker element and the associated input field
2512
+ * @param {String} [target] - target item when refreshing one item only
2513
+ * 'picker' or 'input'
2514
+ * @param {Boolean} [forceRender] - whether to re-render the picker element
2515
+ * regardless of its state instead of optimized refresh
2516
+ */
2517
+ refresh(target = undefined, forceRender = false) {
2518
+ if (target && typeof target !== 'string') {
2519
+ forceRender = target;
2520
+ target = undefined;
2521
+ }
2522
+
2523
+ let mode;
2524
+ if (target === 'picker') {
2525
+ mode = 2;
2526
+ } else if (target === 'input') {
2527
+ mode = 1;
2528
+ } else {
2529
+ mode = 3;
2530
+ }
2531
+ refreshUI(this, mode, !forceRender);
2532
+ }
2533
+
2534
+ /**
2535
+ * Enter edit mode
2536
+ * Not available on inline picker or when the picker element is hidden
2537
+ */
2538
+ enterEditMode() {
2539
+ if (this.inline || !this.picker.active || this.editMode) {
2540
+ return;
2541
+ }
2542
+ this.editMode = true;
2543
+ this.inputField.classList.add('in-edit', 'border-blue-700', '!border-primary-700');
2544
+ }
2545
+
2546
+ /**
2547
+ * Exit from edit mode
2548
+ * Not available on inline picker
2549
+ * @param {Object} [options] - function options
2550
+ * - update: {boolean} - whether to call update() after exiting
2551
+ * If false, input field is revert to the existing selection
2552
+ * default: false
2553
+ */
2554
+ exitEditMode(options = undefined) {
2555
+ if (this.inline || !this.editMode) {
2556
+ return;
2557
+ }
2558
+ const opts = Object.assign({update: false}, options);
2559
+ delete this.editMode;
2560
+ this.inputField.classList.remove('in-edit', 'border-blue-700', '!border-primary-700');
2561
+ if (opts.update) {
2562
+ this.update(opts);
2563
+ }
2564
+ }
2565
+ }
2566
+
2567
+ return Datepicker;
2568
+
2569
+ })();