material-inspired-component-library 4.0.2 → 5.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.
@@ -23,8 +23,32 @@ export const timepickerSelector = 'dialog.micl-dialog.micl-timepicker';
23
23
 
24
24
  type ValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
25
25
 
26
+ interface TimeLimits {
27
+ max: number,
28
+ min: number
29
+ }
30
+
26
31
  export default (() =>
27
32
  {
33
+ const uses12HourFormat = (() =>
34
+ {
35
+ try {
36
+ const hourCycle = new Intl.DateTimeFormat(undefined, {
37
+ hour: 'numeric'
38
+ }).resolvedOptions().hourCycle;
39
+
40
+ return hourCycle === 'h11' || hourCycle === 'h12';
41
+ }
42
+ catch (error) {
43
+ return false;
44
+ }
45
+ })();
46
+
47
+ const getElement = <T extends Element>(parent: Element, selector: string): T | null =>
48
+ {
49
+ return parent.querySelector(selector) as T | null;
50
+ };
51
+
28
52
  const isValueElement = (element: Element): element is ValueElement =>
29
53
  {
30
54
  return element instanceof HTMLInputElement ||
@@ -32,81 +56,360 @@ export default (() =>
32
56
  element instanceof HTMLSelectElement;
33
57
  };
34
58
 
59
+ const isVisible = (element: Element | null): boolean =>
60
+ {
61
+ return !!element && !element.classList.contains('micl-hidden');
62
+ };
63
+
64
+ const toggleSelection = (element: Element, force: boolean): void =>
65
+ {
66
+ element.classList.toggle('micl-timepicker--selected', force);
67
+ };
68
+
69
+ const getTimeLimits = (name: string): TimeLimits =>
70
+ {
71
+ if (name === 'hour') {
72
+ return {
73
+ min: uses12HourFormat ? 1 : 0,
74
+ max: uses12HourFormat ? 12 : 23
75
+ };
76
+ }
77
+ return { min: 0, max: 59 };
78
+ };
79
+
80
+ const formatValue = (element: HTMLInputElement): void =>
81
+ {
82
+ const { max, min } = getTimeLimits(element.name);
83
+ let value = parseInt(element.value, 10);
84
+
85
+ if (isNaN(value)) value = min;
86
+ if (value > max) value = max;
87
+ if (value < 0) value = min;
88
+
89
+ element.value = String(value).padStart(2, '0');
90
+ };
91
+
92
+ const setInputAttributes = (input: HTMLInputElement): void =>
93
+ {
94
+ const { min, max } = getTimeLimits(input.name);
95
+
96
+ let pattern: string;
97
+ if (input.name === 'hour') {
98
+ pattern = uses12HourFormat ? '(0[1-9]|1[0-2])' : '(0[0-9]|1[0-9]|2[0-3])';
99
+ } else {
100
+ pattern = '(0[0-9]|[1-5][0-9])';
101
+ }
102
+
103
+ const attributes: Record<string, string> = {
104
+ maxlength: '2',
105
+ pattern: pattern,
106
+ inputmode: 'numeric',
107
+ autocomplete: 'off',
108
+ role: 'spinbutton',
109
+ min: String(min),
110
+ max: String(max)
111
+ };
112
+
113
+ for (const key in attributes) {
114
+ input.setAttribute(key, attributes[key]);
115
+ }
116
+ };
117
+
118
+ const setDial = (dial: HTMLElement, name: string, value: string): void =>
119
+ {
120
+ dial.querySelectorAll('data').forEach(
121
+ e => e.classList.remove('micl-timepicker__time--selected')
122
+ );
123
+
124
+ const mark = dial.querySelector(`data[data-${name}][value="${value}"]`);
125
+ let angle = '';
126
+
127
+ if (mark) {
128
+ angle = window.getComputedStyle(mark).getPropertyValue('--micl-angle');
129
+ mark.classList.add('micl-timepicker__time--selected');
130
+ }
131
+ else if (name === 'minute') {
132
+ angle = `${Math.round((parseInt(value, 10) * 360 / 60) - 90)}deg`;
133
+ }
134
+ !!angle && dial.style.setProperty('--micl-angle', angle);
135
+ };
136
+
137
+ const setInputValue = (
138
+ dialog : HTMLElement,
139
+ name : string,
140
+ value? : string,
141
+ setampm?: boolean,
142
+ setdial : boolean = true
143
+ ): void => {
144
+
145
+ let numeric = parseInt(value || '0', 10);
146
+ if (isNaN(numeric)) {
147
+ return;
148
+ }
149
+ const input = getElement<HTMLInputElement>(dialog, `input[name=${name}]`);
150
+ if (!input) {
151
+ return;
152
+ }
153
+
154
+ if (name === 'hour' && setampm && uses12HourFormat) {
155
+ const am = dialog.querySelector('.micl-timepicker__am') as HTMLInputElement;
156
+ const pm = dialog.querySelector('.micl-timepicker__pm') as HTMLInputElement;
157
+
158
+ if (numeric > 12) {
159
+ if (pm) {
160
+ pm.checked = true;
161
+ }
162
+ numeric -= 12;
163
+ }
164
+ else if (am) {
165
+ am.checked = true;
166
+ }
167
+ }
168
+ input.value = `${numeric}`.padStart(2, '0');
169
+
170
+ if (setdial) {
171
+ const dial = getElement<HTMLElement>(dialog, '.micl-timepicker__dial');
172
+ if (!dial) {
173
+ return;
174
+ }
175
+ setDial(dial, name, input.value);
176
+ }
177
+ }
178
+
179
+ const addMarks = (dialog: HTMLElement, dial: HTMLElement): void =>
180
+ {
181
+ let angle = uses12HourFormat ? 300 : 270;
182
+
183
+ for (let i = (uses12HourFormat ? 1 : 0); i <= (uses12HourFormat ? 12 : 23); i++) {
184
+ const mark = document.createElement('data') as HTMLDataElement;
185
+
186
+ mark.value = `${i}`.padStart(2, '0');
187
+ mark.textContent = `${i}`;
188
+ mark.dataset.hour = `${i}`;
189
+ mark.style.setProperty('--micl-angle', `${angle}deg`);
190
+ if (!uses12HourFormat && i >= 12) {
191
+ mark.classList.add('micl-timepicker__dial-inner');
192
+ }
193
+ else {
194
+ mark.dataset.minute = `${(i * 5) % 60}`;
195
+ }
196
+ dial.appendChild(mark);
197
+
198
+ angle = (angle + 30) % 360;
199
+ }
200
+
201
+ const track: HTMLSpanElement = document.createElement('span');
202
+ track.classList.add('micl-timepicker__track');
203
+ dial.appendChild(track);
204
+ };
205
+
206
+ const showDialMarks = (dial: HTMLElement, name: string): void =>
207
+ {
208
+ dial.querySelectorAll<HTMLDataElement>('data').forEach(mark =>
209
+ {
210
+ if (!!mark.dataset[name]) {
211
+ mark.textContent = mark.dataset[name];
212
+ mark.value = mark.dataset[name].padStart(2, '0');
213
+ }
214
+ if (mark.classList.contains('micl-timepicker__dial-inner')) {
215
+ mark.classList[name === 'hour' ? 'remove' : 'add']('micl-hidden');
216
+ }
217
+ });
218
+ };
219
+
220
+ const handleSpinning = (dialog: HTMLElement, input: HTMLInputElement, event: KeyboardEvent): void =>
221
+ {
222
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
223
+ return;
224
+ }
225
+ event.preventDefault();
226
+
227
+ const { max, min } = getTimeLimits(input.name);
228
+ let value = parseInt(input.value, 10) || 0;
229
+
230
+ value += event.key === 'ArrowUp' ? 1 : -1;
231
+ if (value < min || value > max) {
232
+ value = (value < min) ? max : min;
233
+ if (input.name === 'hour' && uses12HourFormat) {
234
+ const e = dialog.querySelector('input[name=period]:not(:checked)') as HTMLInputElement;
235
+ e?.click();
236
+ }
237
+ }
238
+
239
+ setInputValue(dialog, input.name, `${value}`);
240
+ };
241
+
35
242
  return {
36
243
  initialize: (dialog: HTMLDialogElement): void =>
37
244
  {
38
- const form = dialog.querySelector('form') as HTMLFormElement;
39
- const input = dialog.querySelector('input') as HTMLInputElement;
40
- if (!form || !input) {
245
+ if (dialog.dataset.miclinitialized) {
41
246
  return;
42
247
  }
43
- if (dialog.dataset.miclinitialized) {
248
+
249
+ const form = getElement<HTMLFormElement>(dialog, 'form');
250
+ const mode = getElement<HTMLElement>(dialog, '.micl-timepicker__inputmode');
251
+ const dial = getElement<HTMLElement>(dialog, '.micl-timepicker__dial');
252
+ const inputs = [
253
+ getElement<HTMLInputElement>(dialog, 'input[name=hour]'),
254
+ getElement<HTMLInputElement>(dialog, 'input[name=minute]')
255
+ ].filter((input): input is HTMLInputElement => input !== null);
256
+
257
+ if (!form || inputs.length < 2) {
44
258
  return;
45
259
  }
46
260
  dialog.dataset.miclinitialized = '1';
47
261
 
48
- input.addEventListener('keydown', (event: Event) =>
262
+ inputs.forEach((input, i) =>
49
263
  {
50
- if (!(event instanceof KeyboardEvent)) {
51
- return;
52
- }
53
- if (event.key === 'Enter' || event.key === ' ') {
54
- event.preventDefault();
264
+ setInputAttributes(input);
265
+ if (dial) {
266
+ input.toggleAttribute('readonly', isVisible(dial));
55
267
  }
268
+
269
+ input.addEventListener('keydown', handleSpinning.bind(null, dialog, input));
270
+ input.addEventListener('focus', () =>
271
+ {
272
+ toggleSelection(inputs[i === 0 ? 1 : 0], false);
273
+ toggleSelection(input, true);
274
+ if (dial) {
275
+ showDialMarks(dial, input.name);
276
+ setDial(dial, input.name, input.value);
277
+ }
278
+ });
279
+ input.addEventListener('blur', () =>
280
+ {
281
+ if (!isVisible(dial)) {
282
+ formatValue(input);
283
+ toggleSelection(input, false);
284
+ }
285
+ });
56
286
  });
57
287
 
58
- input.addEventListener('change', () =>
288
+ const period = dialog.querySelector('.micl-timepicker__period');
289
+ if (period && uses12HourFormat) {
290
+ ['am', 'pm'].forEach(ampm => {
291
+ let e = document.createElement('input') as HTMLInputElement;
292
+ e.type = 'radio';
293
+ e.name = 'period';
294
+ e.classList.add(`micl-timepicker__${ampm}`);
295
+ e.value = ampm;
296
+ e.ariaLabel = ampm.toUpperCase();
297
+ period.appendChild(e);
298
+ });
299
+ period.classList.toggle('micl-hidden', !uses12HourFormat);
300
+ }
301
+
302
+ mode?.addEventListener('click', () =>
59
303
  {
60
- const hour = parseInt(input.value.split(':')[0], 10);
61
- input.classList.toggle('micl-timepicker--pm', hour >= 12);
62
- console.log(`${input.value} - ${hour}`);
304
+ const icon = mode.textContent;
305
+ mode.textContent = mode.dataset.alticon || icon;
306
+ mode.dataset.alticon = icon;
307
+ dial?.classList.toggle('micl-hidden');
308
+ inputs.forEach(input =>
309
+ {
310
+ input.toggleAttribute('readonly', isVisible(dial));
311
+ });
312
+ if (isVisible(dial)) {
313
+ inputs[0].focus();
314
+ }
63
315
  });
64
316
 
65
- const dial = dialog.querySelector('.micl-timepicker__dial');
66
317
  if (dial) {
67
- dial.addEventListener('mousedown', () => {
318
+ addMarks(dialog, dial);
319
+
320
+ const handleSelection = (clientX: number, clientY: number) =>
321
+ {
322
+ const target = document.elementFromPoint(clientX, clientY);
323
+ if (target && target.tagName === 'DATA') {
324
+ setInputValue(dialog, !dialog.querySelector(
325
+ 'input[name=hour].micl-timepicker--selected'
326
+ ) ? 'minute' : 'hour', (target as HTMLDataElement).value);
327
+ }
328
+ };
329
+ dial.addEventListener('pointerdown', (event: PointerEvent) =>
330
+ {
68
331
  dial.classList.add('micl-timepicker__dial--dragging');
332
+ handleSelection(event.clientX, event.clientY);
333
+ dial.setPointerCapture(event.pointerId);
69
334
  });
70
- dial.addEventListener('mouseup', () => {
71
- dial.classList.remove('micl-timepicker__dial--dragging');
335
+ dial.addEventListener('pointermove', (event: PointerEvent) =>
336
+ {
337
+ if (dial.classList.contains('micl-timepicker__dial--dragging')) {
338
+ handleSelection(event.clientX, event.clientY);
339
+ }
72
340
  });
341
+ const stopDragging = (event: PointerEvent) =>
342
+ {
343
+ dial.classList.remove('micl-timepicker__dial--dragging');
344
+ dial.releasePointerCapture(event.pointerId);
345
+ };
346
+ dial.addEventListener('pointerup', stopDragging);
347
+ dial.addEventListener('pointercancel', stopDragging);
73
348
  }
74
349
 
75
- dialog.addEventListener('beforetoggle', event =>
350
+ dialog.addEventListener('beforetoggle', (event): void =>
76
351
  {
77
352
  if (event.oldState === 'open') {
78
353
  return;
79
354
  }
80
- const element = document.querySelector(
81
- `[data-timepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
82
- );
83
- if (!element) {
84
- return;
355
+
356
+ let invoker = document.activeElement as HTMLInputElement | HTMLButtonElement | null;
357
+ if (
358
+ !invoker
359
+ || (!invoker.dataset.timepicker && !invoker.popoverTargetElement)
360
+ ) {
361
+ invoker = document.querySelector(
362
+ `[data-timepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
363
+ );
85
364
  }
86
- if (isValueElement(element)) {
87
- input.value = element.value;
365
+ if (!invoker) {
366
+ return;
88
367
  }
89
- else if ('textContent' in element) {
90
- input.value = element.textContent;
368
+ (dialog as any)._miclInvoker = invoker;
369
+
370
+ const time = (isValueElement(invoker) ? invoker.value : invoker.textContent).split(':');
371
+ if (time.length === 2) {
372
+ setInputValue(dialog, 'hour', time[0], true);
373
+ setInputValue(dialog, 'minute', time[1], false, false);
91
374
  }
92
375
  });
93
376
 
94
- dialog.addEventListener('close', () =>
377
+ dialog.addEventListener('close', (): void =>
95
378
  {
96
379
  if (!dialog.returnValue) {
97
380
  return;
98
381
  }
99
- document.querySelectorAll(
100
- `[data-timepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
101
- ).forEach(element =>
102
- {
103
- if (isValueElement(element)) {
104
- element.value = input.value;
105
- }
106
- else if ('textContent' in element) {
107
- element.textContent = input.value;
108
- }
109
- });
382
+
383
+ let invoker = (dialog as any)._miclInvoker;
384
+ if (!invoker) {
385
+ invoker = document.querySelector(
386
+ `[data-timepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
387
+ );
388
+ }
389
+ if (!invoker) {
390
+ return;
391
+ }
392
+
393
+ const inputs = form.elements;
394
+ let h = parseInt((inputs.namedItem('hour') as HTMLInputElement)?.value || '0', 10);
395
+ if (isNaN(h)) {
396
+ return;
397
+ }
398
+ if (uses12HourFormat && (inputs.namedItem('period') as RadioNodeList)?.value === 'pm') {
399
+ h += 12;
400
+ }
401
+ const m = parseInt((inputs.namedItem('minute') as HTMLInputElement)?.value || '0', 10);
402
+ if (isNaN(m)) {
403
+ return;
404
+ }
405
+ const time = `${h}`.padStart(2, '0') + ':' + `${m}`.padStart(2, '0');
406
+
407
+ if (isValueElement(invoker)) {
408
+ invoker.value = time;
409
+ }
410
+ else {
411
+ invoker.textContent = time;
412
+ }
110
413
  });
111
414
  }
112
415
  };