material-inspired-component-library 5.0.0 → 6.0.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 (204) hide show
  1. package/README.md +15 -1
  2. package/components/alert/index.scss +4 -4
  3. package/components/appbar/index.scss +3 -2
  4. package/components/badge/index.scss +2 -2
  5. package/components/bottomsheet/index.scss +6 -5
  6. package/components/button/README.md +9 -1
  7. package/components/button/index.scss +20 -20
  8. package/components/button/index.ts +21 -37
  9. package/components/card/index.scss +10 -9
  10. package/components/checkbox/index.scss +11 -11
  11. package/components/datepicker/README.md +146 -0
  12. package/components/datepicker/index.scss +436 -0
  13. package/components/datepicker/index.ts +701 -0
  14. package/components/dialog/README.md +6 -6
  15. package/components/dialog/index.scss +23 -17
  16. package/components/divider/index.scss +2 -0
  17. package/components/iconbutton/README.md +10 -1
  18. package/components/iconbutton/index.scss +18 -17
  19. package/components/iconbutton/index.ts +21 -37
  20. package/components/list/index.scss +10 -10
  21. package/components/menu/index.scss +2 -1
  22. package/components/navigationrail/index.scss +10 -9
  23. package/components/radio/README.md +0 -1
  24. package/components/radio/index.scss +11 -11
  25. package/components/select/index.scss +2 -1
  26. package/components/sidesheet/index.scss +3 -1
  27. package/components/slider/index.scss +7 -7
  28. package/components/stepper/index.scss +5 -4
  29. package/components/switch/README.md +0 -1
  30. package/components/switch/index.scss +21 -21
  31. package/components/textfield/index.scss +6 -5
  32. package/components/textfield/index.ts +63 -6
  33. package/components/timepicker/README.md +8 -9
  34. package/components/timepicker/index.scss +9 -8
  35. package/components/timepicker/index.ts +17 -17
  36. package/dist/alert.css +1 -1
  37. package/dist/appbar.css +1 -1
  38. package/dist/badge.css +1 -1
  39. package/dist/bottomsheet.css +1 -1
  40. package/dist/button.css +1 -1
  41. package/dist/card.css +1 -1
  42. package/dist/checkbox.css +1 -1
  43. package/dist/components/button/index.d.ts +2 -1
  44. package/dist/components/datepicker/index.d.ts +6 -0
  45. package/dist/components/iconbutton/index.d.ts +2 -1
  46. package/dist/datepicker.css +1 -0
  47. package/dist/datepicker.js +1 -0
  48. package/dist/dialog.css +1 -1
  49. package/dist/divider.css +1 -1
  50. package/dist/foundations.css +1 -0
  51. package/dist/foundations.js +1 -0
  52. package/dist/iconbutton.css +1 -1
  53. package/dist/layout.css +1 -1
  54. package/dist/list.css +1 -1
  55. package/dist/menu.css +1 -1
  56. package/dist/micl.css +1 -1
  57. package/dist/micl.js +1 -1
  58. package/dist/navigationrail.css +1 -1
  59. package/dist/radio.css +1 -1
  60. package/dist/scrollbar.css +1 -0
  61. package/dist/scrollbar.js +1 -0
  62. package/dist/select.css +1 -1
  63. package/dist/sidesheet.css +1 -1
  64. package/dist/slider.css +1 -1
  65. package/dist/stepper.css +1 -1
  66. package/dist/switch.css +1 -1
  67. package/dist/textfield.css +1 -1
  68. package/dist/timepicker.css +1 -1
  69. package/docs/accordion.html +3 -1
  70. package/docs/alert.html +3 -1
  71. package/docs/bottomsheet.html +6 -4
  72. package/docs/button.html +19 -17
  73. package/docs/card.html +3 -1
  74. package/docs/checkbox.html +3 -1
  75. package/docs/datepicker.html +275 -0
  76. package/docs/dialog.html +24 -10
  77. package/docs/divider.html +3 -1
  78. package/docs/docs.js +65 -1
  79. package/docs/iconbutton.html +9 -9
  80. package/docs/index.html +6 -3
  81. package/docs/list.html +3 -1
  82. package/docs/menu.html +3 -1
  83. package/docs/micl.css +1 -1
  84. package/docs/micl.js +1 -1
  85. package/docs/navigationrail.html +5 -3
  86. package/docs/radio.html +3 -1
  87. package/docs/select.html +3 -1
  88. package/docs/sidesheet.html +6 -4
  89. package/docs/slider.html +1 -1
  90. package/docs/stepper.html +3 -1
  91. package/docs/switch.html +3 -1
  92. package/docs/textfield.html +3 -1
  93. package/docs/themes/gray/dark-hc.css +51 -0
  94. package/docs/themes/gray/dark-mc.css +51 -0
  95. package/docs/themes/gray/dark.css +51 -0
  96. package/docs/themes/gray/light-hc.css +51 -0
  97. package/docs/themes/gray/light-mc.css +51 -0
  98. package/docs/themes/gray/light.css +51 -0
  99. package/docs/themes/gray/theme.css +306 -0
  100. package/docs/themes/greenery/dark-hc.css +51 -0
  101. package/docs/themes/greenery/dark-mc.css +51 -0
  102. package/docs/themes/greenery/dark.css +51 -0
  103. package/docs/themes/greenery/light-hc.css +51 -0
  104. package/docs/themes/greenery/light-mc.css +51 -0
  105. package/docs/themes/greenery/light.css +51 -0
  106. package/docs/themes/greenery/theme.css +306 -0
  107. package/docs/themes/hermana/dark-hc.css +51 -0
  108. package/docs/themes/hermana/dark-mc.css +51 -0
  109. package/docs/themes/hermana/dark.css +51 -0
  110. package/docs/themes/hermana/light-hc.css +51 -0
  111. package/docs/themes/hermana/light-mc.css +51 -0
  112. package/docs/themes/hermana/light.css +51 -0
  113. package/docs/themes/hermana/theme.css +306 -0
  114. package/docs/themes/illuminating/dark-hc.css +51 -0
  115. package/docs/themes/illuminating/dark-mc.css +51 -0
  116. package/docs/themes/illuminating/dark.css +51 -0
  117. package/docs/themes/illuminating/light-hc.css +51 -0
  118. package/docs/themes/illuminating/light-mc.css +51 -0
  119. package/docs/themes/illuminating/light.css +51 -0
  120. package/docs/themes/illuminating/theme.css +306 -0
  121. package/docs/themes/magenta/dark-hc.css +51 -0
  122. package/docs/themes/magenta/dark-mc.css +51 -0
  123. package/docs/themes/magenta/dark.css +51 -0
  124. package/docs/themes/magenta/light-hc.css +51 -0
  125. package/docs/themes/magenta/light-mc.css +51 -0
  126. package/docs/themes/magenta/light.css +51 -0
  127. package/docs/themes/magenta/theme.css +306 -0
  128. package/docs/themes/mocha/dark-hc.css +51 -0
  129. package/docs/themes/mocha/dark-mc.css +51 -0
  130. package/docs/themes/mocha/dark.css +51 -0
  131. package/docs/themes/mocha/light-hc.css +51 -0
  132. package/docs/themes/mocha/light-mc.css +51 -0
  133. package/docs/themes/mocha/light.css +51 -0
  134. package/docs/themes/mocha/theme.css +306 -0
  135. package/docs/themes/peri/dark-hc.css +51 -0
  136. package/docs/themes/peri/dark-mc.css +51 -0
  137. package/docs/themes/peri/dark.css +51 -0
  138. package/docs/themes/peri/light-hc.css +51 -0
  139. package/docs/themes/peri/light-mc.css +51 -0
  140. package/docs/themes/peri/light.css +51 -0
  141. package/docs/themes/peri/theme.css +306 -0
  142. package/docs/timepicker.html +5 -3
  143. package/foundations/index.scss +102 -0
  144. package/foundations/layout/index.scss +0 -52
  145. package/foundations/scrollbar/index.scss +46 -0
  146. package/intl.d.ts +9 -0
  147. package/micl.ts +18 -8
  148. package/package.json +2 -1
  149. package/styles/README.md +17 -8
  150. package/styles/motion.scss +3 -0
  151. package/styles/shapes.scss +23 -18
  152. package/styles/statelayer.scss +4 -0
  153. package/styles/typography.scss +2 -2
  154. package/styles.scss +3 -26
  155. package/themes/gray/dark-hc.css +51 -0
  156. package/themes/gray/dark-mc.css +51 -0
  157. package/themes/gray/dark.css +51 -0
  158. package/themes/gray/light-hc.css +51 -0
  159. package/themes/gray/light-mc.css +51 -0
  160. package/themes/gray/light.css +51 -0
  161. package/themes/gray/theme.css +306 -0
  162. package/themes/greenery/dark-hc.css +51 -0
  163. package/themes/greenery/dark-mc.css +51 -0
  164. package/themes/greenery/dark.css +51 -0
  165. package/themes/greenery/light-hc.css +51 -0
  166. package/themes/greenery/light-mc.css +51 -0
  167. package/themes/greenery/light.css +51 -0
  168. package/themes/greenery/theme.css +306 -0
  169. package/themes/hermana/dark-hc.css +51 -0
  170. package/themes/hermana/dark-mc.css +51 -0
  171. package/themes/hermana/dark.css +51 -0
  172. package/themes/hermana/light-hc.css +51 -0
  173. package/themes/hermana/light-mc.css +51 -0
  174. package/themes/hermana/light.css +51 -0
  175. package/themes/hermana/theme.css +306 -0
  176. package/themes/illuminating/dark-hc.css +51 -0
  177. package/themes/illuminating/dark-mc.css +51 -0
  178. package/themes/illuminating/dark.css +51 -0
  179. package/themes/illuminating/light-hc.css +51 -0
  180. package/themes/illuminating/light-mc.css +51 -0
  181. package/themes/illuminating/light.css +51 -0
  182. package/themes/illuminating/theme.css +306 -0
  183. package/themes/magenta/dark-hc.css +51 -0
  184. package/themes/magenta/dark-mc.css +51 -0
  185. package/themes/magenta/dark.css +51 -0
  186. package/themes/magenta/light-hc.css +51 -0
  187. package/themes/magenta/light-mc.css +51 -0
  188. package/themes/magenta/light.css +51 -0
  189. package/themes/magenta/theme.css +306 -0
  190. package/themes/mocha/dark-hc.css +51 -0
  191. package/themes/mocha/dark-mc.css +51 -0
  192. package/themes/mocha/dark.css +51 -0
  193. package/themes/mocha/light-hc.css +51 -0
  194. package/themes/mocha/light-mc.css +51 -0
  195. package/themes/mocha/light.css +51 -0
  196. package/themes/mocha/theme.css +306 -0
  197. package/themes/peri/dark-hc.css +51 -0
  198. package/themes/peri/dark-mc.css +51 -0
  199. package/themes/peri/dark.css +51 -0
  200. package/themes/peri/light-hc.css +51 -0
  201. package/themes/peri/light-mc.css +51 -0
  202. package/themes/peri/light.css +51 -0
  203. package/themes/peri/theme.css +306 -0
  204. package/tsconfig.json +2 -2
@@ -0,0 +1,701 @@
1
+ //
2
+ // Copyright © 2025 Hermana AS
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+
22
+ export const datepickerSelector = 'dialog.micl-dialog.micl-datepicker';
23
+
24
+ type ValueElement = HTMLInputElement | HTMLButtonElement;
25
+
26
+ interface DatePickerState {
27
+ invoker : ValueElement | null;
28
+ selected: Date;
29
+ viewDate: Date; // the month/year currently being viewed
30
+ min : Date;
31
+ max : Date;
32
+ }
33
+
34
+ const stateMap = new WeakMap<HTMLDialogElement, DatePickerState>();
35
+ const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
36
+
37
+ const formatters = {
38
+ input: new Intl.DateTimeFormat(locale, {
39
+ year : 'numeric',
40
+ month: '2-digit',
41
+ day : '2-digit'
42
+ }),
43
+ header: new Intl.DateTimeFormat(locale, { weekday: 'short', day: 'numeric', month: 'short' }),
44
+ monthLong: new Intl.DateTimeFormat(undefined, { month: 'long' }),
45
+ monthShort: new Intl.DateTimeFormat(locale, { month: 'short' }),
46
+ weekdayNarrow: new Intl.DateTimeFormat(locale, { weekday: 'narrow' }),
47
+ weekdayLong: new Intl.DateTimeFormat(locale, { weekday: 'long' })
48
+ };
49
+
50
+ const toLocalMidnight = (date: Date): Date =>
51
+ {
52
+ const d = new Date(date);
53
+ d.setHours(0, 0, 0, 0);
54
+ return d;
55
+ };
56
+
57
+ const formatToInputDateValue = (d: Date): string =>
58
+ {
59
+ const month = (d.getMonth() + 1).toString().padStart(2, '0');
60
+ const day = d.getDate().toString().padStart(2, '0');
61
+ return `${d.getFullYear()}-${month}-${day}`;
62
+ };
63
+
64
+ const getDateFormat = (): string =>
65
+ {
66
+ return formatters.input.formatToParts(new Date(2025, 0, 15)).map(part =>
67
+ {
68
+ switch (part.type) {
69
+ case 'day' : return 'DD';
70
+ case 'month' : return 'MM';
71
+ case 'year' : return 'YYYY';
72
+ case 'literal': return part.value;
73
+ default: return '';
74
+ }
75
+ }).join('').trim();
76
+ };
77
+
78
+ const getFirstDayOfWeek = (): number =>
79
+ {
80
+ try {
81
+ const info = (new Intl.Locale(locale) as any).getWeekInfo?.();
82
+ if (info) {
83
+ return info.firstDay === 7 ? 0 : info.firstDay;
84
+ }
85
+ }
86
+ catch {}
87
+
88
+ return /US|CA|MX/i.test(locale) ? 0 : 1; // Sunday for USA, Mexico and Canada, Monday as default
89
+ };
90
+
91
+ const firstDayOfWeek = getFirstDayOfWeek();
92
+ const isValidDate = (date: Date): boolean => !isNaN(date.getTime());
93
+
94
+ const setText = (parent: Element | null, text: string): void =>
95
+ {
96
+ if (!parent) {
97
+ return;
98
+ }
99
+ if (parent.firstElementChild) {
100
+ let node = parent.firstChild;
101
+ while (node) {
102
+ if (node.nodeType === Node.TEXT_NODE) {
103
+ node.nodeValue = text;
104
+ return;
105
+ }
106
+ node = node.nextSibling;
107
+ }
108
+ parent.appendChild(document.createTextNode(text));
109
+ }
110
+ else {
111
+ parent.textContent = text;
112
+ }
113
+ };
114
+
115
+ const getCalendarDays = (
116
+ year : number,
117
+ month: number
118
+ ): Array<{ date: Date, val: string, isCurrentMonth: boolean }> => {
119
+
120
+ const firstOfMonth = new Date(year, month, 1);
121
+ const dayOfWeek = firstOfMonth.getDay();
122
+ const offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
123
+
124
+ const startDate = new Date(year, month, 1 - offset);
125
+
126
+ return Array.from({ length: 42 }, (_, i) => {
127
+ const current = new Date(startDate);
128
+ current.setDate(startDate.getDate() + i);
129
+ return {
130
+ date : current,
131
+ val : formatToInputDateValue(current),
132
+ isCurrentMonth: current.getMonth() === month
133
+ };
134
+ });
135
+ };
136
+
137
+ const renderCalendarHeader = (): DocumentFragment =>
138
+ {
139
+ const tempDate = new Date();
140
+ const startOffset = tempDate.getDay() - firstDayOfWeek;
141
+ tempDate.setDate(tempDate.getDate() - startOffset);
142
+
143
+ const fragment = document.createDocumentFragment();
144
+
145
+ for (let i = 0; i < 7; i++) {
146
+ const span = document.createElement('span');
147
+ span.style.gridArea = `1 / ${i + 1}`;
148
+ span.textContent = formatters.weekdayNarrow.format(tempDate);
149
+ span.title = formatters.weekdayLong.format(tempDate);
150
+ fragment.appendChild(span);
151
+ tempDate.setDate(tempDate.getDate() + 1);
152
+ }
153
+ return fragment;
154
+ };
155
+
156
+ const populateContainerWithDays = (
157
+ container: HTMLElement,
158
+ days : Array<{ date: Date, val: string, isCurrentMonth: boolean }>,
159
+ state : DatePickerState,
160
+ isEmpty : boolean = false
161
+ ): void => {
162
+ if (isEmpty) {
163
+ const fragment = renderCalendarHeader();
164
+
165
+ days.forEach((_, index) => {
166
+ const time = document.createElement('time');
167
+ const row = Math.floor(index / 7) + 2;
168
+ const col = (index % 7) + 1;
169
+ time.style.gridArea = `${row} / ${col}`;
170
+ fragment.appendChild(time);
171
+ });
172
+
173
+ container.appendChild(fragment);
174
+ }
175
+
176
+ const today = toLocalMidnight(new Date());
177
+ container.querySelectorAll('time').forEach((el, index) =>
178
+ {
179
+ const day = days[index];
180
+ el.dateTime = day.val;
181
+ el.textContent = day.date.getDate().toString();
182
+
183
+ const isSelected = day.date.getTime() === state.selected.getTime();
184
+ const isToday = day.date.getTime() === today.getTime();
185
+
186
+ el.className = '';
187
+ if (!day.isCurrentMonth) el.classList.add('micl-datepicker__outside');
188
+ if (isSelected) el.classList.add('micl-datepicker__selected');
189
+ if (isToday) el.classList.add('micl-datepicker__today');
190
+ });
191
+ };
192
+
193
+ const renderCalendar = (
194
+ dialog: HTMLDialogElement,
195
+ state : DatePickerState,
196
+ amount: number = 0
197
+ ): void => {
198
+
199
+ const content = dialog.querySelector<HTMLElement>('.micl-dialog__content');
200
+ const calendars = content?.querySelector<HTMLElement>('.micl-datepicker__calendars');
201
+ if (!calendars) {
202
+ return;
203
+ }
204
+
205
+ const startClass = 'micl-startleft';
206
+ const endClass = 'micl-startright';
207
+ const moveLeftClass = 'micl-moveleft';
208
+ const moveRightClass = 'micl-moveright';
209
+
210
+ calendars.classList.remove(moveLeftClass, moveRightClass, startClass, endClass);
211
+ void calendars.offsetWidth;
212
+
213
+ if (amount !== 0) {
214
+ const oldCalendar = calendars.querySelector<HTMLElement>('.micl-datepicker__calendar');
215
+ if (!oldCalendar) {
216
+ return;
217
+ }
218
+
219
+ const newCalendar = document.createElement('div');
220
+ const newCalendarInner = document.createElement('div');
221
+ newCalendar.classList.add('micl-datepicker__calendar');
222
+ newCalendarInner.classList.add('micl-datepicker__calendar-inner');
223
+
224
+ const days = getCalendarDays(state.viewDate.getFullYear(), state.viewDate.getMonth());
225
+ populateContainerWithDays(newCalendarInner, days, state, true);
226
+
227
+ const isNextMonth = amount > 0;
228
+ const startPositionClass = isNextMonth ? startClass : endClass;
229
+ const endTransformClass = isNextMonth ? moveLeftClass : moveRightClass;
230
+
231
+ newCalendar.appendChild(newCalendarInner);
232
+ if (isNextMonth) {
233
+ calendars.appendChild(newCalendar);
234
+ }
235
+ else {
236
+ calendars.prepend(newCalendar);
237
+ }
238
+
239
+ calendars.classList.add('micl-no-transition', startPositionClass);
240
+ void calendars.offsetWidth;
241
+
242
+ requestAnimationFrame(() => {
243
+ calendars.classList.remove('micl-no-transition', startPositionClass);
244
+ calendars.classList.add(endTransformClass);
245
+ });
246
+
247
+ const onTransitionEnd = () =>
248
+ {
249
+ calendars.removeEventListener('transitionend', onTransitionEnd);
250
+
251
+ setTimeout(() =>
252
+ {
253
+ calendars.classList.remove(endTransformClass);
254
+ if (oldCalendar.parentElement === calendars) {
255
+ oldCalendar.remove();
256
+ }
257
+ calendars.classList.add('micl-no-transition', startClass);
258
+ void calendars.offsetWidth;
259
+
260
+ calendars.classList.remove('micl-no-transition', startClass);
261
+ }, 0);
262
+ };
263
+ calendars.addEventListener('transitionend', onTransitionEnd);
264
+ }
265
+ else {
266
+ let calendar = calendars.querySelector<HTMLElement>('.micl-datepicker__calendar');
267
+ if (!calendar) {
268
+ calendar = document.createElement('div');
269
+ calendar.classList.add('micl-datepicker__calendar');
270
+ calendar = calendars.appendChild(calendar);
271
+ }
272
+ let inner = calendar.querySelector<HTMLElement>('.micl-datepicker__calendar-inner');
273
+ if (!inner) {
274
+ inner = document.createElement('div');
275
+ inner.classList.add('micl-datepicker__calendar-inner');
276
+ calendar.appendChild(inner);
277
+ }
278
+ const days = getCalendarDays(state.viewDate.getFullYear(), state.viewDate.getMonth());
279
+ populateContainerWithDays(inner, days, state, inner.querySelectorAll('time').length === 0);
280
+ }
281
+
282
+ const input = content?.querySelector<HTMLInputElement>('.micl-datepicker__input input');
283
+ if (input) {
284
+ input.value = formatters.input.format(state.selected);
285
+ if (input.value) {
286
+ input.dataset.miclvalue = '1';
287
+ }
288
+ else {
289
+ delete input.dataset.miclvalue;
290
+ }
291
+ if (!input.dataset.micldateformat) {
292
+ input.dataset.micldateformat = getDateFormat();
293
+ }
294
+ }
295
+
296
+ setText(dialog.querySelector('h1, h2, h3, h4, h5, h6, .micl-heading'), formatters.header.format(state.selected));
297
+ setText(dialog.querySelector('.micl-datepicker__month'), formatters.monthShort.format(state.viewDate));
298
+ setText(
299
+ dialog.querySelector('.micl-datepicker__year'),
300
+ state.viewDate.toLocaleDateString(locale, dialog.classList.contains('micl-dialog--docked') ?
301
+ { year: 'numeric' } : { month: 'long', year: 'numeric' })
302
+ );
303
+
304
+ const monthInput = dialog.querySelector<HTMLInputElement>(`.micl-datepicker__months input[value="${state.viewDate.getMonth()}"]`);
305
+ if (monthInput) monthInput.checked = true;
306
+
307
+ const yearInput = dialog.querySelector<HTMLInputElement>(`.micl-datepicker__years input[value="${state.viewDate.getFullYear()}"]`);
308
+ if (yearInput) yearInput.checked = true;
309
+ };
310
+
311
+ const initPeriodPickers = (dialog: HTMLDialogElement, min: Date, max: Date): void =>
312
+ {
313
+ const minYear = min.getFullYear();
314
+ const maxYear = max.getFullYear();
315
+
316
+ ['months', 'years'].forEach(period =>
317
+ {
318
+ const container = dialog.querySelector(`.micl-datepicker__${period}`);
319
+ if (!container) {
320
+ return;
321
+ }
322
+ container.innerHTML = '';
323
+ const frag = document.createDocumentFragment();
324
+
325
+ const maxMonth = max.getMonth();
326
+
327
+ if (period === 'months') {
328
+ const months: number[] = [];
329
+
330
+ let current = new Date(min.getFullYear(), min.getMonth(), 1);
331
+ while (
332
+ current <= max
333
+ || (current.getMonth() === maxMonth && current.getFullYear() === maxYear)
334
+ ) {
335
+ months.push(current.getMonth());
336
+ current.setMonth(current.getMonth() + 1);
337
+ }
338
+
339
+ [...new Set(months.sort((a, b) => a - b))].forEach(m => {
340
+ const label = document.createElement('label');
341
+ label.innerHTML = `<span class="material-symbols-outlined">check</span><input type="radio" name="miclmonth" value="${m}"> ${formatters.monthLong.format(new Date(2000, m, 1))}`;
342
+ frag.appendChild(label);
343
+ });
344
+ }
345
+ else {
346
+ for (let y = minYear; y <= maxYear; y++) {
347
+ const label = document.createElement('label');
348
+ label.innerHTML = `<input type="radio" name="miclyear" value="${y}"> ${y}`;
349
+ frag.appendChild(label);
350
+ }
351
+ }
352
+
353
+ const inner = document.createElement('div');
354
+ inner.classList.add(`micl-datepicker__${period}-inner`);
355
+ container.appendChild(inner).appendChild(frag);
356
+ });
357
+ };
358
+
359
+ const toggleView = (dialog: HTMLDialogElement, view: 'calendars' | 'months' | 'years' | 'input'): void =>
360
+ {
361
+ if (view === 'months' || view === 'years') {
362
+ if (!dialog.querySelector(`.micl-datepicker__${view}.micl-datepicker__view-hidden`)) {
363
+ view = 'calendars';
364
+ }
365
+ }
366
+
367
+ ['calendars', 'input', 'month-selector', 'year-selector'].forEach(name =>
368
+ {
369
+ let doHide = view === 'input';
370
+ if (name === 'calendars' || name === 'input') {
371
+ doHide = view !== name;
372
+ }
373
+ dialog.querySelector(`.micl-datepicker__${name}`)?.classList.toggle(
374
+ 'micl-datepicker__view-hidden',
375
+ doHide
376
+ );
377
+ });
378
+
379
+ const content = dialog.querySelector<HTMLElement>('.micl-dialog__content');
380
+ if (!content) {
381
+ return;
382
+ }
383
+ const contentHeight = parseInt(window.getComputedStyle(content).getPropertyValue('max-block-size'), 10);
384
+
385
+ ['.micl-datepicker__months', '.micl-datepicker__years'].forEach(selector =>
386
+ {
387
+ const period = content.querySelector<HTMLElement>(selector);
388
+ if (!period) {
389
+ return;
390
+ }
391
+ const selected = period.querySelector<HTMLInputElement>('input:checked');
392
+ const height = 48;
393
+ let doHide: boolean | null = false;
394
+
395
+ if (selected && (selector.substring(18) === view)) {
396
+ const property = window.getComputedStyle(period).getPropertyValue('transition-duration');
397
+ const duration = parseFloat(property) * (property.includes('ms') ? 1 : 1000);
398
+ const maxScrollDistance = period.scrollHeight - contentHeight;
399
+ const centerTop = (contentHeight - height) / 2;
400
+
401
+ if (selected.offsetTop > centerTop) {
402
+ let scrollDistance = selected.offsetTop - centerTop - (height / 2);
403
+ if (scrollDistance > maxScrollDistance) {
404
+ scrollDistance = maxScrollDistance;
405
+ }
406
+
407
+ const startTime = performance.now();
408
+ const animateScroll = (currentTime: number) => {
409
+ const progress = Math.min((currentTime - startTime) / duration, 1);
410
+ content.scrollTop = scrollDistance * progress;
411
+ if (progress < 1) {
412
+ requestAnimationFrame(animateScroll);
413
+ }
414
+ };
415
+ period.classList.remove('micl-datepicker__view-hidden');
416
+ requestAnimationFrame(animateScroll);
417
+ doHide = null;
418
+
419
+ period.addEventListener('transitionend', function handler(event)
420
+ {
421
+ if (event.propertyName === 'height' || event.propertyName === 'block-size') {
422
+ content.scrollTop = scrollDistance;
423
+ period.removeEventListener('transitionend', handler);
424
+ }
425
+ });
426
+ }
427
+ }
428
+ else {
429
+ doHide = true;
430
+ }
431
+ if (doHide !== null) {
432
+ period.classList.toggle('micl-datepicker__view-hidden', doHide);
433
+ }
434
+ });
435
+
436
+ const mode = dialog.querySelector<HTMLElement>('.micl-datepicker__inputmode[data-miclalt]');
437
+ if (mode) {
438
+ if (!mode.dataset.miclalticon) {
439
+ mode.dataset.miclalticon = mode.textContent;
440
+ }
441
+ mode.textContent = (view === 'input' ? mode.dataset.miclalt : mode.dataset.miclalticon) || '';
442
+ }
443
+ };
444
+
445
+ const changePeriod = (dialog: HTMLDialogElement, amount: number, unit: 'month' | 'year'): void =>
446
+ {
447
+ const state = stateMap.get(dialog);
448
+ if (!state) {
449
+ return;
450
+ }
451
+
452
+ const newDate = new Date(state.viewDate);
453
+ if (unit === 'month') {
454
+ newDate.setMonth(newDate.getMonth() + amount);
455
+ }
456
+ else {
457
+ newDate.setFullYear(newDate.getFullYear() + amount);
458
+ }
459
+
460
+ const belowMin = state.min && newDate < state.min;
461
+ const aboveMax = state.max && newDate > state.max;
462
+
463
+ if (belowMin || aboveMax) {
464
+ dialog.querySelector('.micl-datepicker__calendars')?.animate([
465
+ { transform: 'translateX(0)' },
466
+ { transform: `translateX(${belowMin ? 8 : -8}px)` },
467
+ { transform: 'translateX(0)' }
468
+ ], { duration: 500, easing: 'ease-in-out' });
469
+ return;
470
+ }
471
+
472
+ state.viewDate = newDate;
473
+ renderCalendar(dialog, state, unit === 'month' ? amount : 0);
474
+ };
475
+
476
+ const selectDate = (dialog: HTMLDialogElement, dateStr: string, isLocaleFormatted = false): void =>
477
+ {
478
+ const state = stateMap.get(dialog);
479
+ if (!state) {
480
+ return;
481
+ }
482
+
483
+ let parts: number[] = [];
484
+ if (isLocaleFormatted) {
485
+ const dateformat = getDateFormat();
486
+ if (dateStr.length === dateformat.length) {
487
+ let d = '';
488
+ let m = '';
489
+ let y = '';
490
+ for (let i = 0; i < dateformat.length; i++) {
491
+ switch (dateformat[i]) {
492
+ case 'D': d += dateStr[i]; break;
493
+ case 'M': m += dateStr[i]; break;
494
+ case 'Y': y += dateStr[i]; break;
495
+ default:
496
+ }
497
+ }
498
+ parts = [parseInt(y, 10), parseInt(m, 10) - 1, parseInt(d, 10)];
499
+ }
500
+ }
501
+ else {
502
+ parts = dateStr.split('-').map(Number);
503
+ parts[1]--;
504
+ }
505
+
506
+ if (parts.length === 3) {
507
+ state.selected = new Date(parts[0], parts[1], parts[2]);
508
+ state.viewDate = new Date(state.selected);
509
+
510
+ renderCalendar(dialog, state);
511
+ }
512
+ };
513
+
514
+ export default (() =>
515
+ {
516
+ return {
517
+ keydown: (event: Event): void =>
518
+ {
519
+ if (
520
+ !(event instanceof KeyboardEvent)
521
+ || !(event.target instanceof Element)
522
+ ) {
523
+ return;
524
+ }
525
+ const dialog = event.target.closest(datepickerSelector) as HTMLDialogElement;
526
+ if (!dialog) {
527
+ return;
528
+ }
529
+
530
+ switch (event.key) {
531
+ case 'Enter':
532
+ case ' ':
533
+ if (event.target instanceof HTMLInputElement && event.target.type === 'date') {
534
+ event.preventDefault();
535
+ }
536
+ break;
537
+ case 'M':
538
+ toggleView(dialog, 'months');
539
+ break;
540
+ case 'Y':
541
+ toggleView(dialog, 'years');
542
+ break;
543
+ case 'PageUp':
544
+ case 'PageDown':
545
+ changePeriod(dialog, event.key === 'PageUp' ? 1 : -1, event.shiftKey ? 'year' : 'month');
546
+ break;
547
+ default:
548
+ }
549
+ },
550
+
551
+ initialize: (dialog: HTMLDialogElement): void =>
552
+ {
553
+ if (dialog.dataset.miclinitialized) {
554
+ return;
555
+ }
556
+
557
+ const form = dialog.querySelector('form');
558
+ const content = dialog.querySelector('.micl-dialog__content');
559
+ if (!form || !content) {
560
+ return;
561
+ }
562
+ dialog.dataset.miclinitialized = '1';
563
+
564
+ dialog.addEventListener('click', event =>
565
+ {
566
+ const target = event.target as HTMLElement;
567
+ const btn = target.closest('button');
568
+
569
+ if (btn) {
570
+ const forMonth = btn.parentElement?.classList.contains('micl-datepicker__month-selector');
571
+ const isNext = btn.classList.contains('micl-datepicker__next');
572
+ const isPrev = btn.classList.contains('micl-datepicker__previous');
573
+
574
+ if (isNext || isPrev) {
575
+ changePeriod(dialog, isNext ? 1 : -1, forMonth ? 'month' : 'year');
576
+ return;
577
+ }
578
+ }
579
+
580
+ if (target.closest('.micl-datepicker__month')) toggleView(dialog, 'months');
581
+ if (target.closest('.micl-datepicker__year')) toggleView(dialog, 'years');
582
+
583
+ const mode = target.closest('.micl-datepicker__inputmode') as HTMLElement;
584
+ if (mode) {
585
+ toggleView(dialog, !dialog.querySelector(
586
+ '.micl-datepicker__input.micl-datepicker__view-hidden'
587
+ ) ? 'calendars' : 'input');
588
+ }
589
+
590
+ const time = target.closest('time');
591
+ if (time && time.dateTime) {
592
+ selectDate(dialog, time.dateTime);
593
+ }
594
+
595
+ if (
596
+ target instanceof HTMLInputElement
597
+ && (target.name === 'miclmonth' || target.name === 'miclyear')
598
+ ) {
599
+ const state = stateMap.get(dialog);
600
+ if (state) {
601
+ const value = parseInt(target.value, 10);
602
+ if (target.name === 'miclmonth') {
603
+ state.viewDate.setMonth(value);
604
+ }
605
+ else {
606
+ state.viewDate.setFullYear(value);
607
+ }
608
+ if (state.viewDate < state.min) {
609
+ state.viewDate = state.min;
610
+ }
611
+ else if (state.viewDate > state.max) {
612
+ state.viewDate = state.max;
613
+ }
614
+ renderCalendar(dialog, state);
615
+ toggleView(dialog, 'calendars');
616
+ }
617
+ }
618
+ });
619
+
620
+ dialog.addEventListener('beforetoggle', (event: any): void =>
621
+ {
622
+ if (event.newState !== 'open') {
623
+ return;
624
+ }
625
+ const isInvoker = (e: Element | null): e is ValueElement => e instanceof HTMLInputElement || e instanceof HTMLButtonElement;
626
+
627
+ let invoker = document.activeElement;
628
+ if (
629
+ !isInvoker(invoker)
630
+ || (!invoker.dataset.datepicker && !invoker.popoverTargetElement && !(invoker as any).commandForElement)
631
+ ) {
632
+ invoker = document.querySelector(
633
+ `[data-datepicker="${dialog.id}"],[popovertarget="${dialog.id}"],[commandfor="${dialog.id}"]`
634
+ );
635
+ }
636
+ if (!isInvoker(invoker)) {
637
+ return;
638
+ }
639
+
640
+ let initialDate = new Date();
641
+ let min = new Date(1900, 0, 1);
642
+ let max = new Date(2099, 11, 31);
643
+
644
+ if (invoker instanceof HTMLInputElement) {
645
+ if (invoker.type === 'date' && invoker.valueAsDate) {
646
+ initialDate = invoker.valueAsDate;
647
+ }
648
+ else if (invoker.value) {
649
+ initialDate = new Date(invoker.value);
650
+ }
651
+ if (invoker.min) min = new Date(invoker.min);
652
+ if (invoker.max) max = new Date(invoker.max);
653
+ }
654
+ else {
655
+ const parsed = new Date(invoker.value || invoker.textContent);
656
+ if (isValidDate(parsed)) {
657
+ initialDate = parsed;
658
+ }
659
+ }
660
+ if (!isValidDate(initialDate)) initialDate = new Date();
661
+ initialDate = toLocalMidnight(initialDate);
662
+
663
+ const state: DatePickerState = {
664
+ invoker,
665
+ selected: initialDate,
666
+ viewDate: new Date(initialDate),
667
+ min,
668
+ max
669
+ };
670
+ stateMap.set(dialog, state);
671
+
672
+ initPeriodPickers(dialog, min, max);
673
+ toggleView(dialog, 'calendars');
674
+ renderCalendar(dialog, state);
675
+
676
+ dialog.querySelector('.micl-datepicker__input input')?.addEventListener('blur', e =>
677
+ {
678
+ const element = e.target as HTMLInputElement;
679
+ selectDate(dialog, element.value, true);
680
+ }, { once: true });
681
+ });
682
+
683
+ dialog.addEventListener('close', (): void =>
684
+ {
685
+ const state = stateMap.get(dialog);
686
+ if (!state?.invoker || dialog.returnValue === '') {
687
+ return;
688
+ }
689
+ state.invoker.value = formatToInputDateValue(state.selected);
690
+
691
+ if (state.invoker instanceof HTMLInputElement) {
692
+ state.invoker.dispatchEvent(new Event('change', { bubbles: true }));
693
+ state.invoker.dispatchEvent(new Event('input', { bubbles: true }));
694
+ }
695
+ else {
696
+ state.invoker.textContent = state.selected.toLocaleDateString();
697
+ }
698
+ });
699
+ }
700
+ };
701
+ })();