material-inspired-component-library 5.0.1 → 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 (127) hide show
  1. package/README.md +6 -2
  2. package/components/button/README.md +9 -1
  3. package/components/button/index.ts +21 -37
  4. package/components/datepicker/README.md +146 -0
  5. package/components/datepicker/index.scss +2 -1
  6. package/components/datepicker/index.ts +210 -109
  7. package/components/iconbutton/README.md +10 -1
  8. package/components/iconbutton/index.ts +21 -37
  9. package/components/textfield/index.ts +56 -0
  10. package/components/timepicker/README.md +8 -9
  11. package/components/timepicker/index.ts +5 -5
  12. package/dist/components/button/index.d.ts +2 -1
  13. package/dist/components/iconbutton/index.d.ts +2 -1
  14. package/dist/datepicker.css +1 -1
  15. package/dist/micl.css +1 -1
  16. package/dist/micl.js +1 -1
  17. package/docs/bottomsheet.html +3 -3
  18. package/docs/button.html +16 -16
  19. package/docs/datepicker.html +133 -9
  20. package/docs/dialog.html +5 -5
  21. package/docs/docs.js +22 -1
  22. package/docs/iconbutton.html +8 -8
  23. package/docs/index.html +3 -2
  24. package/docs/micl.css +1 -1
  25. package/docs/micl.js +1 -1
  26. package/docs/navigationrail.html +2 -2
  27. package/docs/sidesheet.html +3 -3
  28. package/docs/themes/gray/dark-hc.css +51 -0
  29. package/docs/themes/gray/dark-mc.css +51 -0
  30. package/docs/themes/gray/dark.css +51 -0
  31. package/docs/themes/gray/light-hc.css +51 -0
  32. package/docs/themes/gray/light-mc.css +51 -0
  33. package/docs/themes/gray/light.css +51 -0
  34. package/docs/themes/gray/theme.css +306 -0
  35. package/docs/themes/greenery/dark-hc.css +51 -0
  36. package/docs/themes/greenery/dark-mc.css +51 -0
  37. package/docs/themes/greenery/dark.css +51 -0
  38. package/docs/themes/greenery/light-hc.css +51 -0
  39. package/docs/themes/greenery/light-mc.css +51 -0
  40. package/docs/themes/greenery/light.css +51 -0
  41. package/docs/themes/greenery/theme.css +306 -0
  42. package/docs/themes/hermana/dark-hc.css +51 -0
  43. package/docs/themes/hermana/dark-mc.css +51 -0
  44. package/docs/themes/hermana/dark.css +51 -0
  45. package/docs/themes/hermana/light-hc.css +51 -0
  46. package/docs/themes/hermana/light-mc.css +51 -0
  47. package/docs/themes/hermana/light.css +51 -0
  48. package/docs/themes/hermana/theme.css +306 -0
  49. package/docs/themes/illuminating/dark-hc.css +51 -0
  50. package/docs/themes/illuminating/dark-mc.css +51 -0
  51. package/docs/themes/illuminating/dark.css +51 -0
  52. package/docs/themes/illuminating/light-hc.css +51 -0
  53. package/docs/themes/illuminating/light-mc.css +51 -0
  54. package/docs/themes/illuminating/light.css +51 -0
  55. package/docs/themes/illuminating/theme.css +306 -0
  56. package/docs/themes/magenta/dark-hc.css +51 -0
  57. package/docs/themes/magenta/dark-mc.css +51 -0
  58. package/docs/themes/magenta/dark.css +51 -0
  59. package/docs/themes/magenta/light-hc.css +51 -0
  60. package/docs/themes/magenta/light-mc.css +51 -0
  61. package/docs/themes/magenta/light.css +51 -0
  62. package/docs/themes/magenta/theme.css +306 -0
  63. package/docs/themes/mocha/dark-hc.css +51 -0
  64. package/docs/themes/mocha/dark-mc.css +51 -0
  65. package/docs/themes/mocha/dark.css +51 -0
  66. package/docs/themes/mocha/light-hc.css +51 -0
  67. package/docs/themes/mocha/light-mc.css +51 -0
  68. package/docs/themes/mocha/light.css +51 -0
  69. package/docs/themes/mocha/theme.css +306 -0
  70. package/docs/themes/peri/dark-hc.css +51 -0
  71. package/docs/themes/peri/dark-mc.css +51 -0
  72. package/docs/themes/peri/dark.css +51 -0
  73. package/docs/themes/peri/light-hc.css +51 -0
  74. package/docs/themes/peri/light-mc.css +51 -0
  75. package/docs/themes/peri/light.css +51 -0
  76. package/docs/themes/peri/theme.css +306 -0
  77. package/docs/timepicker.html +2 -2
  78. package/package.json +1 -1
  79. package/themes/gray/dark-hc.css +51 -0
  80. package/themes/gray/dark-mc.css +51 -0
  81. package/themes/gray/dark.css +51 -0
  82. package/themes/gray/light-hc.css +51 -0
  83. package/themes/gray/light-mc.css +51 -0
  84. package/themes/gray/light.css +51 -0
  85. package/themes/gray/theme.css +306 -0
  86. package/themes/greenery/dark-hc.css +51 -0
  87. package/themes/greenery/dark-mc.css +51 -0
  88. package/themes/greenery/dark.css +51 -0
  89. package/themes/greenery/light-hc.css +51 -0
  90. package/themes/greenery/light-mc.css +51 -0
  91. package/themes/greenery/light.css +51 -0
  92. package/themes/greenery/theme.css +306 -0
  93. package/themes/hermana/dark-hc.css +51 -0
  94. package/themes/hermana/dark-mc.css +51 -0
  95. package/themes/hermana/dark.css +51 -0
  96. package/themes/hermana/light-hc.css +51 -0
  97. package/themes/hermana/light-mc.css +51 -0
  98. package/themes/hermana/light.css +51 -0
  99. package/themes/hermana/theme.css +306 -0
  100. package/themes/illuminating/dark-hc.css +51 -0
  101. package/themes/illuminating/dark-mc.css +51 -0
  102. package/themes/illuminating/dark.css +51 -0
  103. package/themes/illuminating/light-hc.css +51 -0
  104. package/themes/illuminating/light-mc.css +51 -0
  105. package/themes/illuminating/light.css +51 -0
  106. package/themes/illuminating/theme.css +306 -0
  107. package/themes/magenta/dark-hc.css +51 -0
  108. package/themes/magenta/dark-mc.css +51 -0
  109. package/themes/magenta/dark.css +51 -0
  110. package/themes/magenta/light-hc.css +51 -0
  111. package/themes/magenta/light-mc.css +51 -0
  112. package/themes/magenta/light.css +51 -0
  113. package/themes/magenta/theme.css +306 -0
  114. package/themes/mocha/dark-hc.css +51 -0
  115. package/themes/mocha/dark-mc.css +51 -0
  116. package/themes/mocha/dark.css +51 -0
  117. package/themes/mocha/light-hc.css +51 -0
  118. package/themes/mocha/light-mc.css +51 -0
  119. package/themes/mocha/light.css +51 -0
  120. package/themes/mocha/theme.css +306 -0
  121. package/themes/peri/dark-hc.css +51 -0
  122. package/themes/peri/dark-mc.css +51 -0
  123. package/themes/peri/dark.css +51 -0
  124. package/themes/peri/light-hc.css +51 -0
  125. package/themes/peri/light-mc.css +51 -0
  126. package/themes/peri/light.css +51 -0
  127. package/themes/peri/theme.css +306 -0
@@ -27,17 +27,52 @@ interface DatePickerState {
27
27
  invoker : ValueElement | null;
28
28
  selected: Date;
29
29
  viewDate: Date; // the month/year currently being viewed
30
- min : Date | null;
31
- max : Date | null;
30
+ min : Date;
31
+ max : Date;
32
32
  }
33
33
 
34
34
  const stateMap = new WeakMap<HTMLDialogElement, DatePickerState>();
35
-
36
35
  const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
37
36
 
38
- const isValueElement = (element: Element | null): element is ValueElement =>
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 =>
39
65
  {
40
- return element instanceof HTMLInputElement || element instanceof HTMLButtonElement;
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();
41
76
  };
42
77
 
43
78
  const getFirstDayOfWeek = (): number =>
@@ -54,15 +89,7 @@ const getFirstDayOfWeek = (): number =>
54
89
  };
55
90
 
56
91
  const firstDayOfWeek = getFirstDayOfWeek();
57
-
58
- const toLocalMidnight = (date: Date): Date =>
59
- {
60
- const d = new Date(date);
61
- d.setHours(0, 0, 0, 0);
62
- return d;
63
- };
64
-
65
- const isValidDate = (d: Date): boolean => !isNaN(d.getTime());
92
+ const isValidDate = (date: Date): boolean => !isNaN(date.getTime());
66
93
 
67
94
  const setText = (parent: Element | null, text: string): void =>
68
95
  {
@@ -90,23 +117,40 @@ const getCalendarDays = (
90
117
  month: number
91
118
  ): Array<{ date: Date, val: string, isCurrentMonth: boolean }> => {
92
119
 
93
- const results = [];
94
120
  const firstOfMonth = new Date(year, month, 1);
95
121
  const dayOfWeek = firstOfMonth.getDay();
96
122
  const offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
97
- const current = new Date(year, month, 1 - offset);
98
- const pad = (n: number): string => n.toString().padStart(2, '0');
99
-
100
- // 6 weeks * 7 days
101
- for (let i = 0; i < 42; i++) {
102
- results.push({
103
- date: new Date(current),
104
- val: `${current.getFullYear()}-${pad(current.getMonth() + 1)}-${pad(current.getDate())}`,
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),
105
132
  isCurrentMonth: current.getMonth() === month
106
- });
107
- current.setDate(current.getDate() + 1);
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);
108
152
  }
109
- return results;
153
+ return fragment;
110
154
  };
111
155
 
112
156
  const populateContainerWithDays = (
@@ -116,19 +160,7 @@ const populateContainerWithDays = (
116
160
  isEmpty : boolean = false
117
161
  ): void => {
118
162
  if (isEmpty) {
119
- const fragment = document.createDocumentFragment();
120
- const tempDate = new Date();
121
- const startOffset = tempDate.getDay() - firstDayOfWeek;
122
- tempDate.setDate(tempDate.getDate() - startOffset);
123
-
124
- for (let i = 0; i < 7; i++) {
125
- const span = document.createElement('span');
126
- span.style.gridArea = `1 / ${i + 1}`;
127
- span.textContent = tempDate.toLocaleDateString(locale, { weekday: 'narrow' });
128
- span.title = tempDate.toLocaleDateString(locale, { weekday: 'long' });
129
- fragment.appendChild(span);
130
- tempDate.setDate(tempDate.getDate() + 1);
131
- }
163
+ const fragment = renderCalendarHeader();
132
164
 
133
165
  days.forEach((_, index) => {
134
166
  const time = document.createElement('time');
@@ -249,62 +281,78 @@ const renderCalendar = (
249
281
 
250
282
  const input = content?.querySelector<HTMLInputElement>('.micl-datepicker__input input');
251
283
  if (input) {
252
- const pad = (n: number): string => n.toString().padStart(2, '0');
253
- input.value = `${state.selected.getFullYear()}-${pad(state.selected.getMonth() + 1)}-${pad(state.selected.getDate())}`;
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
+ }
254
294
  }
255
-
256
- setText(
257
- dialog.querySelector('h1, h2, h3, h4, h5, h6, .micl-heading'),
258
- state.selected.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })
259
- );
260
- setText(
261
- dialog.querySelector('.micl-datepicker__month'),
262
- state.viewDate.toLocaleDateString(locale, { month: 'short' })
263
- );
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));
264
298
  setText(
265
299
  dialog.querySelector('.micl-datepicker__year'),
266
300
  state.viewDate.toLocaleDateString(locale, dialog.classList.contains('micl-dialog--docked') ?
267
301
  { year: 'numeric' } : { month: 'long', year: 'numeric' })
268
302
  );
269
303
 
270
- ['months', 'years'].forEach(period => {
271
- const value = period === 'months' ? state.viewDate.getMonth() : state.viewDate.getFullYear();
272
- const input = dialog.querySelector<HTMLInputElement>(`.micl-datepicker__${period} input[value="${value}"]`);
273
- if (input) {
274
- input.checked = true;
275
- }
276
- });
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;
277
309
  };
278
310
 
279
- const initPeriodPickers = (dialog: HTMLDialogElement, minYear: number, maxYear: number): void =>
311
+ const initPeriodPickers = (dialog: HTMLDialogElement, min: Date, max: Date): void =>
280
312
  {
281
- ['months', 'years'].forEach(period => {
313
+ const minYear = min.getFullYear();
314
+ const maxYear = max.getFullYear();
315
+
316
+ ['months', 'years'].forEach(period =>
317
+ {
282
318
  const container = dialog.querySelector(`.micl-datepicker__${period}`);
283
- if (container) {
284
- container.innerHTML = '';
285
- const frag = document.createDocumentFragment();
319
+ if (!container) {
320
+ return;
321
+ }
322
+ container.innerHTML = '';
323
+ const frag = document.createDocumentFragment();
286
324
 
287
- if (period === 'months') {
288
- const fmt = new Intl.DateTimeFormat(undefined, { month: 'long' });
325
+ const maxMonth = max.getMonth();
289
326
 
290
- for (let m = 0; m < 12; m++) {
291
- const label = document.createElement('label');
292
- label.innerHTML = `<span class="material-symbols-outlined">check</span><input type="radio" name="miclmonth" value="${m}"> ${fmt.format(new Date(2000, m, 1))}`;
293
- frag.appendChild(label);
294
- }
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);
295
337
  }
296
- else {
297
- for (let y = minYear; y <= maxYear; y++) {
298
- const label = document.createElement('label');
299
- label.innerHTML = `<input type="radio" name="miclyear" value="${y}"> ${y}`;
300
- frag.appendChild(label);
301
- }
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);
302
350
  }
303
-
304
- const inner = document.createElement('div');
305
- inner.classList.add(`micl-datepicker__${period}-inner`);
306
- container.appendChild(inner).appendChild(frag);
307
351
  }
352
+
353
+ const inner = document.createElement('div');
354
+ inner.classList.add(`micl-datepicker__${period}-inner`);
355
+ container.appendChild(inner).appendChild(frag);
308
356
  });
309
357
  };
310
358
 
@@ -384,6 +432,14 @@ const toggleView = (dialog: HTMLDialogElement, view: 'calendars' | 'months' | 'y
384
432
  period.classList.toggle('micl-datepicker__view-hidden', doHide);
385
433
  }
386
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
+ }
387
443
  };
388
444
 
389
445
  const changePeriod = (dialog: HTMLDialogElement, amount: number, unit: 'month' | 'year'): void =>
@@ -401,25 +457,58 @@ const changePeriod = (dialog: HTMLDialogElement, amount: number, unit: 'month' |
401
457
  newDate.setFullYear(newDate.getFullYear() + amount);
402
458
  }
403
459
 
404
- if (state.min && newDate < state.min) return;
405
- if (state.max && newDate > state.max) return;
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
+ }
406
471
 
407
472
  state.viewDate = newDate;
408
473
  renderCalendar(dialog, state, unit === 'month' ? amount : 0);
409
474
  };
410
475
 
411
- const selectDate = (dialog: HTMLDialogElement, dateStr: string): void =>
476
+ const selectDate = (dialog: HTMLDialogElement, dateStr: string, isLocaleFormatted = false): void =>
412
477
  {
413
478
  const state = stateMap.get(dialog);
414
479
  if (!state) {
415
480
  return;
416
481
  }
417
482
 
418
- const parts = dateStr.split('-').map(Number);
419
- state.selected = new Date(parts[0], parts[1] - 1, parts[2]);
420
- state.viewDate = new Date(state.selected);
421
-
422
- renderCalendar(dialog, state);
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
+ }
423
512
  };
424
513
 
425
514
  export default (() =>
@@ -446,8 +535,10 @@ export default (() =>
446
535
  }
447
536
  break;
448
537
  case 'M':
538
+ toggleView(dialog, 'months');
539
+ break;
449
540
  case 'Y':
450
- toggleView(dialog, event.key === 'M' ? 'months' : 'years');
541
+ toggleView(dialog, 'years');
451
542
  break;
452
543
  case 'PageUp':
453
544
  case 'PageDown':
@@ -491,11 +582,9 @@ export default (() =>
491
582
 
492
583
  const mode = target.closest('.micl-datepicker__inputmode') as HTMLElement;
493
584
  if (mode) {
494
- const icon = mode.textContent;
495
- mode.textContent = mode.dataset.alticon || icon;
496
- mode.dataset.alticon = icon;
497
- const inputHidden = !!dialog.querySelector('.micl-datepicker__input.micl-datepicker__view-hidden');
498
- toggleView(dialog, inputHidden ? 'input' : 'calendars');
585
+ toggleView(dialog, !dialog.querySelector(
586
+ '.micl-datepicker__input.micl-datepicker__view-hidden'
587
+ ) ? 'calendars' : 'input');
499
588
  }
500
589
 
501
590
  const time = target.closest('time');
@@ -516,6 +605,12 @@ export default (() =>
516
605
  else {
517
606
  state.viewDate.setFullYear(value);
518
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
+ }
519
614
  renderCalendar(dialog, state);
520
615
  toggleView(dialog, 'calendars');
521
616
  }
@@ -527,23 +622,24 @@ export default (() =>
527
622
  if (event.newState !== 'open') {
528
623
  return;
529
624
  }
625
+ const isInvoker = (e: Element | null): e is ValueElement => e instanceof HTMLInputElement || e instanceof HTMLButtonElement;
530
626
 
531
627
  let invoker = document.activeElement;
532
628
  if (
533
- !isValueElement(invoker)
534
- || (!invoker.dataset.datepicker && !invoker.getAttribute('popovertarget'))
629
+ !isInvoker(invoker)
630
+ || (!invoker.dataset.datepicker && !invoker.popoverTargetElement && !(invoker as any).commandForElement)
535
631
  ) {
536
632
  invoker = document.querySelector(
537
- `[data-datepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
633
+ `[data-datepicker="${dialog.id}"],[popovertarget="${dialog.id}"],[commandfor="${dialog.id}"]`
538
634
  );
539
635
  }
540
- if (!isValueElement(invoker)) {
636
+ if (!isInvoker(invoker)) {
541
637
  return;
542
638
  }
543
-
639
+
544
640
  let initialDate = new Date();
545
- let min: Date | null = null;
546
- let max: Date | null = null;
641
+ let min = new Date(1900, 0, 1);
642
+ let max = new Date(2099, 11, 31);
547
643
 
548
644
  if (invoker instanceof HTMLInputElement) {
549
645
  if (invoker.type === 'date' && invoker.valueAsDate) {
@@ -564,28 +660,33 @@ export default (() =>
564
660
  if (!isValidDate(initialDate)) initialDate = new Date();
565
661
  initialDate = toLocalMidnight(initialDate);
566
662
 
567
- stateMap.set(dialog, {
568
- invoker: invoker as ValueElement,
663
+ const state: DatePickerState = {
664
+ invoker,
569
665
  selected: initialDate,
570
666
  viewDate: new Date(initialDate),
571
667
  min,
572
668
  max
573
- });
669
+ };
670
+ stateMap.set(dialog, state);
574
671
 
575
- initPeriodPickers(dialog, min ? min.getFullYear() : 1900, max ? max.getFullYear() : 2099);
672
+ initPeriodPickers(dialog, min, max);
576
673
  toggleView(dialog, 'calendars');
577
- renderCalendar(dialog, stateMap.get(dialog)!);
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 });
578
681
  });
579
682
 
580
683
  dialog.addEventListener('close', (): void =>
581
684
  {
582
685
  const state = stateMap.get(dialog);
583
- if (!state || !state.invoker || dialog.returnValue === '') {
686
+ if (!state?.invoker || dialog.returnValue === '') {
584
687
  return;
585
688
  }
586
- const pad = (n: number): string => n.toString().padStart(2, '0');
587
- const date = `${state.selected.getFullYear()}-${pad(state.selected.getMonth() + 1)}-${pad(state.selected.getDate())}`;
588
- state.invoker.value = date;
689
+ state.invoker.value = formatToInputDateValue(state.selected);
589
690
 
590
691
  if (state.invoker instanceof HTMLInputElement) {
591
692
  state.invoker.dispatchEvent(new Event('change', { bubbles: true }));
@@ -67,9 +67,18 @@ A toggle button has two states: **on** (selected) and **off** (unselected). To c
67
67
  **Example: A selected toggle button**
68
68
 
69
69
  ```HTML
70
- <button type="button" class="micl-iconbutton-outlined-l micl-button--toggle micl-button--selected material-symbols-outlined" aria-label="Control Panel">settings</button>
70
+ <button
71
+ type="button"
72
+ id="id0"
73
+ class="micl-iconbutton-outlined-l micl-button--toggle micl-button--selected material-symbols-outlined"
74
+ commandfor="id0"
75
+ command="--micl-toggle"
76
+ aria-label="Control Panel"
77
+ >settings</button>
71
78
  ```
72
79
 
80
+ The self-targeting `command` property (`--micl-toggle`) toggles the button state whenever the user interacts with the button.
81
+
73
82
  ## Icons
74
83
  The examples above use [Google Material Symbols](https://fonts.google.com/icons). For buttons using these icons, a fill-style of `1` is applied when the button is active or hovered over. To enable this effect, ensure your `link` tag includes `FILL@0..1`.
75
84
 
@@ -19,58 +19,42 @@
19
19
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  // SOFTWARE.
21
21
 
22
- export const buttonSelector = 'button[popovertarget],button.micl-button--toggle';
22
+ export const buttonSelector = 'button.micl-button--toggle';
23
23
 
24
24
  export default (() =>
25
25
  {
26
- const onClick = (event: Event) =>
27
- {
28
- if (!event.target || !(event.target instanceof HTMLButtonElement)) {
29
- return;
30
- }
31
- if (event.target.popoverTargetElement instanceof HTMLDialogElement) {
32
- if (event.target.popoverTargetElement.open) {
33
- event.target.popoverTargetElement.close();
34
- }
35
- else {
36
- event.target.popoverTargetElement.showModal();
37
- }
38
- }
39
- if (event.target.classList.contains('micl-button--toggle')) {
40
- event.target.classList.add('micl-button--toggled');
41
- event.target.classList.toggle('micl-button--selected');
42
- if (!!event.target.dataset.miclalt) {
43
- [event.target.textContent, event.target.dataset.miclalt] =
44
- [event.target.dataset.miclalt, event.target.textContent];
26
+ return {
27
+ command: (event: Event): void =>
28
+ {
29
+ const target = event.target as HTMLButtonElement;
30
+
31
+ if (
32
+ target.matches(buttonSelector)
33
+ && !target.disabled
34
+ && (event as any).command === '--micl-toggle'
35
+ ) {
36
+ target.classList.add('micl-button--toggled');
37
+ target.classList.toggle('micl-button--selected');
45
38
  }
46
- }
47
- };
39
+ },
48
40
 
49
- return {
50
- initialize: (element: HTMLButtonElement) =>
41
+ initialize: function(element: HTMLButtonElement): void
51
42
  {
52
43
  if (
53
- !element.matches('button[popovertarget],button.micl-button--toggle')
44
+ !element.matches(buttonSelector)
54
45
  || element.dataset.miclinitialized
55
46
  ) {
56
47
  return;
57
48
  }
58
49
  element.dataset.miclinitialized = '1';
59
50
 
60
- if (
61
- (element.popoverTargetElement instanceof HTMLDialogElement)
62
- && !element.popoverTargetElement.hasAttribute('popover')
63
- ) {
64
- element.addEventListener('click', onClick);
65
- }
66
- else if (element.classList.contains('micl-button--toggle')) {
67
- element.addEventListener('click', onClick);
68
- }
51
+ element.addEventListener('command', this.command);
69
52
  },
70
- cleanup: (element: HTMLButtonElement) =>
53
+
54
+ cleanup: function(element: HTMLButtonElement): void
71
55
  {
72
- if (element.matches('button[popovertarget],button.micl-button--toggle')) {
73
- document.removeEventListener('click', onClick);
56
+ if (element.matches(buttonSelector)) {
57
+ document.removeEventListener('command', this.command);
74
58
  delete element.dataset.miclinitialized;
75
59
  }
76
60
  }
@@ -44,6 +44,59 @@ export default (() =>
44
44
  }
45
45
  };
46
46
 
47
+ const formatAsDate = (input: HTMLInputElement, inputType: string): void =>
48
+ {
49
+ const partsRegex = /([DMY]{2,4})([^DMY])?([DMY]{2,4})([^DMY])?([DMY]{2,4})/;
50
+ const match = (input.dataset.micldateformat || '').match(partsRegex);
51
+ if (!match) {
52
+ return;
53
+ }
54
+
55
+ const components = [
56
+ { type: match[1], length: match[1].length, separator: match[2] || '' },
57
+ { type: match[3], length: match[3].length, separator: match[4] || '' },
58
+ { type: match[5], length: match[5].length, separator: '' }
59
+ ];
60
+
61
+ input.maxLength = components.reduce((sum, c) => sum + c.length + (c.separator ? 1 : 0), 0);
62
+
63
+ let value = input.value.replace(/\D/g, ''); // remove all non-digits
64
+ let formattedValue = '';
65
+ let valueIndex = 0;
66
+ let cursorPosition = input.selectionStart || 0;
67
+
68
+ for (let i = 0; i < components.length; i++) {
69
+ const comp = components[i];
70
+ if (value.length < valueIndex) break;
71
+
72
+ const segment = value.substring(valueIndex, valueIndex + comp.length);
73
+ formattedValue += segment;
74
+ valueIndex += segment.length;
75
+
76
+ if (segment.length === comp.length && comp.separator) {
77
+ formattedValue += comp.separator;
78
+ }
79
+ }
80
+
81
+ const prevLength = input.value.length;
82
+ input.value = formattedValue.substring(0, input.maxLength);
83
+ const newLength = input.value.length;
84
+
85
+ if (inputType.startsWith('deleteContent')) {
86
+ if (cursorPosition > 0) {
87
+ input.setSelectionRange(cursorPosition, cursorPosition);
88
+ }
89
+ }
90
+ else {
91
+ if (newLength > prevLength && newLength > cursorPosition) {
92
+ input.setSelectionRange(newLength, newLength);
93
+ }
94
+ else {
95
+ input.setSelectionRange(cursorPosition, cursorPosition);
96
+ }
97
+ }
98
+ };
99
+
47
100
  return {
48
101
  initialize: (input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void =>
49
102
  {
@@ -110,6 +163,9 @@ export default (() =>
110
163
  return;
111
164
  }
112
165
 
166
+ if (event.target instanceof HTMLInputElement && event.target.dataset.micldateformat) {
167
+ formatAsDate(event.target, (event as InputEvent).inputType);
168
+ }
113
169
  if (event.target.value) {
114
170
  event.target.dataset.miclvalue = '1';
115
171
  }