@dodlhuat/basix 1.2.0 → 1.2.2

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 (93) hide show
  1. package/README.md +266 -6
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/button.scss +48 -0
  5. package/css/calendar.scss +957 -0
  6. package/css/card.scss +65 -65
  7. package/css/chart.scss +270 -157
  8. package/css/chat-bubbles.scss +134 -68
  9. package/css/chips.scss +109 -19
  10. package/css/colors.scss +32 -32
  11. package/css/datepicker.scss +336 -336
  12. package/css/defaults.scss +90 -90
  13. package/css/docs.scss +529 -0
  14. package/css/editor.scss +36 -0
  15. package/css/file-uploader.scss +1 -1
  16. package/css/flyout-menu.scss +361 -361
  17. package/css/form.scss +0 -15
  18. package/css/gallery.scss +65 -6
  19. package/css/grid.scss +41 -40
  20. package/css/group-picker.scss +345 -0
  21. package/css/guitar-chords.css +250 -250
  22. package/css/icons.scss +330 -330
  23. package/css/parameters.scss +3 -3
  24. package/css/placeholder.scss +33 -33
  25. package/css/popover.scss +206 -0
  26. package/css/progress.scss +76 -32
  27. package/css/properties.scss +51 -36
  28. package/css/push-menu.scss +302 -174
  29. package/css/reset.scss +39 -39
  30. package/css/scrollbar.scss +62 -5
  31. package/css/sidebar-nav.scss +92 -0
  32. package/css/spinner.scss +65 -65
  33. package/css/stepper.scss +48 -12
  34. package/css/style.css +3155 -254
  35. package/css/style.css.map +1 -1
  36. package/css/style.min.css +1 -1
  37. package/css/style.scss +51 -45
  38. package/css/table.scss +199 -199
  39. package/css/tabs.scss +154 -123
  40. package/css/timeline.scss +83 -38
  41. package/css/timepicker.scss +100 -5
  42. package/css/toast.scss +81 -81
  43. package/css/virtual-dropdown.scss +35 -29
  44. package/js/calendar.js +532 -0
  45. package/js/calendar.ts +706 -0
  46. package/js/chart.js +573 -257
  47. package/js/chart.ts +692 -0
  48. package/js/code-viewer.js +10 -10
  49. package/js/code-viewer.ts +188 -188
  50. package/js/datepicker.ts +627 -627
  51. package/js/docs-nav.js +204 -0
  52. package/js/dropdown.ts +179 -179
  53. package/js/editor.js +50 -6
  54. package/js/editor.ts +483 -444
  55. package/js/file-uploader.js +1 -0
  56. package/js/file-uploader.ts +1 -0
  57. package/js/flyout-menu.js +14 -14
  58. package/js/flyout-menu.ts +249 -249
  59. package/js/form-builder.js +106 -106
  60. package/js/gallery.js +14 -8
  61. package/js/gallery.ts +245 -236
  62. package/js/group-picker.js +342 -0
  63. package/js/group-picker.ts +447 -0
  64. package/js/guitar-chords.js +268 -268
  65. package/js/lazy-loader.js +121 -121
  66. package/js/modal.ts +166 -166
  67. package/js/popover.js +163 -0
  68. package/js/popover.ts +219 -0
  69. package/js/position.js +108 -0
  70. package/js/position.ts +111 -0
  71. package/js/push-menu.js +113 -0
  72. package/js/push-menu.ts +284 -145
  73. package/js/request.js +50 -50
  74. package/js/scroll.ts +47 -47
  75. package/js/scrollbar.js +13 -0
  76. package/js/scrollbar.ts +324 -307
  77. package/js/select.ts +216 -216
  78. package/js/sidebar-nav.js +41 -0
  79. package/js/sidebar-nav.ts +66 -0
  80. package/js/table.ts +452 -452
  81. package/js/tabs.ts +279 -279
  82. package/js/theme.js +17 -6
  83. package/js/theme.ts +234 -224
  84. package/js/toast.ts +137 -137
  85. package/js/tooltip.js +6 -60
  86. package/js/tooltip.ts +184 -251
  87. package/js/tsconfig.json +18 -18
  88. package/js/utils.ts +83 -83
  89. package/js/virtual-dropdown.js +25 -25
  90. package/js/virtual-dropdown.ts +365 -365
  91. package/package.json +37 -39
  92. package/js/index.js +0 -816
  93. package/js/index.ts +0 -987
package/js/datepicker.ts CHANGED
@@ -1,628 +1,628 @@
1
- interface DatePickerLocales {
2
- days: string[];
3
- months: string[];
4
- }
5
-
6
- interface DatePickerOptions {
7
- mode?: 'single' | 'range';
8
- startDay?: number;
9
- timePicker?: boolean;
10
- locales?: DatePickerLocales;
11
- format?: (date: Date) => string;
12
- onSelect?: (date: Date | DateRange) => void;
13
- }
14
-
15
- interface DateRange {
16
- start: Date | null;
17
- end: Date | null;
18
- }
19
-
20
- type ViewMode = 'days' | 'months' | 'years';
21
-
22
- class DatePicker {
23
- private input: HTMLInputElement | null;
24
- private options: DatePickerOptions;
25
- private currentDate: Date;
26
- private selectedDate: Date | null;
27
- private rangeStart: Date | null;
28
- private rangeEnd: Date | null;
29
- private viewYear: number;
30
- private viewMonth: number;
31
- private viewMode: ViewMode;
32
- private yearRangeStart: number;
33
- private selectedHours: number;
34
- private selectedMinutes: number;
35
- private calendar!: HTMLDivElement;
36
- private backdrop!: HTMLDivElement;
37
- private handleDocumentClick!: (e: Event) => void;
38
-
39
-
40
- constructor(elementOrSelector: string | HTMLInputElement, options: DatePickerOptions = {}) {
41
- this.input = typeof elementOrSelector === 'string'
42
- ? document.querySelector<HTMLInputElement>(elementOrSelector)
43
- : elementOrSelector;
44
-
45
- if (!this.input) {
46
- throw new Error(`DatePicker: Element not found for selector "${elementOrSelector}"`);
47
- }
48
-
49
- const timePicker = options.timePicker ?? false;
50
-
51
- this.options = {
52
- mode: 'single',
53
- startDay: 0,
54
- timePicker,
55
- locales: {
56
- days: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
57
- months: [
58
- 'January', 'February', 'March', 'April', 'May', 'June',
59
- 'July', 'August', 'September', 'October', 'November', 'December'
60
- ]
61
- },
62
- format: timePicker
63
- ? (date: Date) => {
64
- const hours = String(date.getHours()).padStart(2, '0');
65
- const minutes = String(date.getMinutes()).padStart(2, '0');
66
- return `${date.toDateString()} ${hours}:${minutes}`;
67
- }
68
- : (date: Date) => date.toDateString(),
69
- onSelect: () => {},
70
- ...options
71
- };
72
-
73
- this.currentDate = new Date();
74
- this.selectedDate = null;
75
- this.rangeStart = null;
76
- this.rangeEnd = null;
77
-
78
- this.viewYear = this.currentDate.getFullYear();
79
- this.viewMonth = this.currentDate.getMonth();
80
-
81
- this.viewMode = 'days';
82
- this.yearRangeStart = this.viewYear - (this.viewYear % 12);
83
-
84
- this.selectedHours = this.currentDate.getHours();
85
- this.selectedMinutes = this.currentDate.getMinutes();
86
-
87
- this.init();
88
- }
89
-
90
- private init(): void {
91
- this.createCalendarElement();
92
- this.attachEvents();
93
- this.render();
94
- }
95
-
96
- private createCalendarElement(): void {
97
- this.calendar = document.createElement('div');
98
- this.calendar.className = 'datepicker';
99
- document.body.appendChild(this.calendar);
100
-
101
- this.backdrop = document.createElement('div');
102
- this.backdrop.className = 'datepicker-backdrop';
103
- document.body.appendChild(this.backdrop);
104
-
105
- this.backdrop.addEventListener('click', () => this.hide());
106
- }
107
-
108
- private attachEvents(): void {
109
- const toggle = (e: Event): void => {
110
- e.preventDefault();
111
- e.stopPropagation();
112
-
113
- if (this.calendar.classList.contains('visible')) {
114
- this.hide();
115
- } else {
116
- this.show();
117
- }
118
- };
119
-
120
- this.input?.addEventListener('click', toggle);
121
-
122
- this.backdrop.addEventListener('click', (e: Event) => {
123
- e.preventDefault();
124
- e.stopPropagation();
125
- this.hide();
126
- });
127
-
128
- this.handleDocumentClick = (e: Event): void => {
129
- if (this.calendar.classList.contains('mobile')) return;
130
-
131
- const target = e.target as Node;
132
- if (!this.calendar.contains(target) && target !== this.input) {
133
- this.hide();
134
- }
135
- };
136
- }
137
-
138
- private show(): void {
139
- const isMobile = window.innerWidth <= 640;
140
-
141
- if (isMobile) {
142
- this.calendar.classList.add('mobile');
143
- this.backdrop.classList.add('visible');
144
- document.body.style.overflow = 'hidden';
145
-
146
- this.calendar.style.top = '';
147
- this.calendar.style.left = '';
148
- } else {
149
- this.calendar.classList.remove('mobile');
150
- this.backdrop.classList.remove('visible');
151
- document.body.style.overflow = '';
152
-
153
- if (this.input) {
154
- const rect = this.input.getBoundingClientRect();
155
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
156
- const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
157
-
158
- this.calendar.style.top = `${rect.bottom + scrollTop + 5}px`;
159
- this.calendar.style.left = `${rect.left + scrollLeft}px`;
160
-
161
- if (rect.left + 320 > window.innerWidth) {
162
- this.calendar.style.left = `${rect.right + scrollLeft - 320}px`;
163
- }
164
- }
165
-
166
- setTimeout(() => {
167
- document.addEventListener('click', this.handleDocumentClick);
168
- }, 0);
169
- }
170
-
171
- this.calendar.classList.add('visible');
172
- }
173
-
174
- private hide(): void {
175
- this.calendar.classList.remove('visible');
176
- this.backdrop.classList.remove('visible');
177
- document.body.style.overflow = '';
178
-
179
- document.removeEventListener('click', this.handleDocumentClick);
180
- }
181
-
182
- private render(): void {
183
- this.calendar.innerHTML = '';
184
-
185
- const header = this.createHeader();
186
- let content: HTMLDivElement;
187
-
188
- if (this.viewMode === 'days') {
189
- content = this.createGrid();
190
- } else if (this.viewMode === 'months') {
191
- content = this.createMonthGrid();
192
- } else {
193
- content = this.createYearGrid();
194
- }
195
-
196
- this.calendar.appendChild(header);
197
- this.calendar.appendChild(content);
198
-
199
- if (this.options.timePicker && this.viewMode === 'days') {
200
- const timeSection = this.createTimePicker();
201
- this.calendar.appendChild(timeSection);
202
-
203
- const setBtn = document.createElement('button');
204
- setBtn.className = 'datepicker-set-btn';
205
- setBtn.textContent = 'Set';
206
- setBtn.onclick = (e: MouseEvent) => {
207
- e.stopPropagation();
208
- this.hide();
209
- };
210
- this.calendar.appendChild(setBtn);
211
- }
212
- }
213
-
214
- private createHeader(): HTMLDivElement {
215
- const header = document.createElement('div');
216
- header.className = 'datepicker-header';
217
-
218
- const prevBtn = document.createElement('button');
219
- prevBtn.className = 'datepicker-nav';
220
- prevBtn.innerHTML = '&lt;';
221
- prevBtn.onclick = (e: MouseEvent) => {
222
- e.stopPropagation();
223
- this.navigate(-1);
224
- };
225
-
226
- const title = document.createElement('div');
227
- title.className = 'datepicker-title';
228
-
229
- if (this.viewMode === 'days') {
230
- const monthBtn = document.createElement('button');
231
- monthBtn.className = 'datepicker-title-btn';
232
-
233
- monthBtn.textContent = this.options?.locales?.months[this.viewMonth] ?? '';
234
- monthBtn.onclick = (e: MouseEvent) => {
235
- e.stopPropagation();
236
- this.viewMode = 'months';
237
- this.render();
238
- };
239
-
240
- const yearBtn = document.createElement('button');
241
- yearBtn.className = 'datepicker-title-btn';
242
- yearBtn.textContent = String(this.viewYear);
243
- yearBtn.onclick = (e: MouseEvent) => {
244
- e.stopPropagation();
245
- this.viewMode = 'years';
246
- this.yearRangeStart = this.viewYear - (this.viewYear % 12);
247
- this.render();
248
- };
249
-
250
- title.appendChild(monthBtn);
251
- title.appendChild(yearBtn);
252
- } else if (this.viewMode === 'months') {
253
- const yearBtn = document.createElement('button');
254
- yearBtn.className = 'datepicker-title-btn';
255
- yearBtn.textContent = String(this.viewYear);
256
- yearBtn.onclick = (e: MouseEvent) => {
257
- e.stopPropagation();
258
- this.viewMode = 'years';
259
- this.yearRangeStart = this.viewYear - (this.viewYear % 12);
260
- this.render();
261
- };
262
- title.appendChild(yearBtn);
263
- } else {
264
- const rangeText = document.createElement('span');
265
- rangeText.style.fontWeight = '600';
266
- rangeText.textContent = `${this.yearRangeStart} - ${this.yearRangeStart + 11}`;
267
- title.appendChild(rangeText);
268
- }
269
-
270
- const nextBtn = document.createElement('button');
271
- nextBtn.className = 'datepicker-nav';
272
- nextBtn.innerHTML = '&gt;';
273
- nextBtn.onclick = (e: MouseEvent) => {
274
- e.stopPropagation();
275
- this.navigate(1);
276
- };
277
-
278
- header.appendChild(prevBtn);
279
- header.appendChild(title);
280
- header.appendChild(nextBtn);
281
-
282
- return header;
283
- }
284
-
285
- private navigate(delta: number): void {
286
- if (this.viewMode === 'days') {
287
- this.changeMonth(delta);
288
- } else if (this.viewMode === 'months') {
289
- this.viewYear += delta;
290
- this.render();
291
- } else {
292
- this.yearRangeStart += delta * 12;
293
- this.render();
294
- }
295
- }
296
-
297
- private createMonthGrid(): HTMLDivElement {
298
- const grid = document.createElement('div');
299
- grid.className = 'datepicker-grid-months';
300
-
301
- this.options?.locales?.months.forEach((month, index) => {
302
- const el = document.createElement('div');
303
- el.className = 'datepicker-month';
304
- el.textContent = month.substring(0, 3);
305
-
306
- if (index === this.viewMonth) {
307
- el.classList.add('selected');
308
- }
309
- if (index === new Date().getMonth() && this.viewYear === new Date().getFullYear()) {
310
- el.classList.add('current');
311
- }
312
-
313
- el.onclick = (e: MouseEvent) => {
314
- e.stopPropagation();
315
- this.viewMonth = index;
316
- this.viewMode = 'days';
317
- this.render();
318
- };
319
- grid.appendChild(el);
320
- });
321
-
322
- return grid;
323
- }
324
-
325
- private createYearGrid(): HTMLDivElement {
326
- const grid = document.createElement('div');
327
- grid.className = 'datepicker-grid-years';
328
-
329
- for (let i = 0; i < 12; i++) {
330
- const year = this.yearRangeStart + i;
331
- const el = document.createElement('div');
332
- el.className = 'datepicker-year';
333
- el.textContent = String(year);
334
-
335
- if (year === this.viewYear) {
336
- el.classList.add('selected');
337
- }
338
- if (year === new Date().getFullYear()) {
339
- el.classList.add('current');
340
- }
341
-
342
- el.onclick = (e: MouseEvent) => {
343
- e.stopPropagation();
344
- this.viewYear = year;
345
- this.viewMode = 'months';
346
- this.render();
347
- };
348
- grid.appendChild(el);
349
- }
350
-
351
- return grid;
352
- }
353
-
354
- private createGrid(): HTMLDivElement {
355
- const grid = document.createElement('div');
356
- grid.className = 'datepicker-grid';
357
-
358
- const days = this.options?.locales?.days ?? ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
359
- const startDay = this.options.startDay ?? 0;
360
- const adjustedDays = [...days.slice(startDay), ...days.slice(0, startDay)];
361
-
362
- adjustedDays.forEach(day => {
363
- const el = document.createElement('div');
364
- el.className = 'datepicker-day-header';
365
- el.textContent = day;
366
- grid.appendChild(el);
367
- });
368
-
369
- const firstDayOfMonth = new Date(this.viewYear, this.viewMonth, 1).getDay();
370
- const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate();
371
-
372
- const offset = (firstDayOfMonth - startDay + 7) % 7;
373
-
374
- const prevMonthDays = new Date(this.viewYear, this.viewMonth, 0).getDate();
375
- for (let i = offset - 1; i >= 0; i--) {
376
- const day = document.createElement('div');
377
- day.className = 'datepicker-day other-month';
378
- day.textContent = String(prevMonthDays - i);
379
- grid.appendChild(day);
380
- }
381
-
382
- for (let i = 1; i <= daysInMonth; i++) {
383
- const day = document.createElement('div');
384
- day.className = 'datepicker-day';
385
- day.textContent = String(i);
386
-
387
- const date = new Date(this.viewYear, this.viewMonth, i);
388
- date.setHours(0, 0, 0, 0);
389
-
390
- const today = new Date();
391
- today.setHours(0, 0, 0, 0);
392
- if (date.getTime() === today.getTime()) {
393
- day.classList.add('today');
394
- }
395
-
396
- if (this.options.mode === 'single') {
397
- const selectedDay = this.selectedDate ? new Date(this.selectedDate) : null;
398
- if (selectedDay) selectedDay.setHours(0, 0, 0, 0);
399
- if (selectedDay && date.getTime() === selectedDay.getTime()) {
400
- day.classList.add('selected');
401
- }
402
- } else {
403
- const t = date.getTime();
404
- const start = this.rangeStart ? this.rangeStart.getTime() : null;
405
- const end = this.rangeEnd ? this.rangeEnd.getTime() : null;
406
-
407
- if (start && t === start) {
408
- day.classList.add('range-start');
409
- }
410
- if (end && t === end) {
411
- day.classList.add('range-end');
412
- }
413
- if (start && end && t > start && t < end) {
414
- day.classList.add('in-range');
415
- }
416
- if (start && !end && t === start) {
417
- day.classList.add('selected');
418
- }
419
- }
420
-
421
- day.onclick = (e: MouseEvent) => {
422
- e.stopPropagation();
423
- this.handleDateClick(date);
424
- };
425
- grid.appendChild(day);
426
- }
427
-
428
- return grid;
429
- }
430
-
431
- private createTimePicker(): HTMLDivElement {
432
- const wrapper = document.createElement('div');
433
- wrapper.className = 'datepicker-time';
434
-
435
- const label = document.createElement('div');
436
- label.className = 'datepicker-time-label';
437
- label.textContent = 'Time';
438
- wrapper.appendChild(label);
439
-
440
- const controls = document.createElement('div');
441
- controls.className = 'datepicker-time-controls';
442
-
443
- // Hours spinner
444
- const hoursSpinner = this.createSpinner(
445
- this.selectedHours,
446
- 0,
447
- 23,
448
- (value) => {
449
- this.selectedHours = value;
450
- this.applyTimeToSelection();
451
- }
452
- );
453
-
454
- const separator = document.createElement('span');
455
- separator.className = 'datepicker-time-separator';
456
- separator.textContent = ':';
457
-
458
- // Minutes spinner
459
- const minutesSpinner = this.createSpinner(
460
- this.selectedMinutes,
461
- 0,
462
- 59,
463
- (value) => {
464
- this.selectedMinutes = value;
465
- this.applyTimeToSelection();
466
- }
467
- );
468
-
469
- controls.appendChild(hoursSpinner);
470
- controls.appendChild(separator);
471
- controls.appendChild(minutesSpinner);
472
- wrapper.appendChild(controls);
473
-
474
- return wrapper;
475
- }
476
-
477
- private createSpinner(
478
- value: number,
479
- min: number,
480
- max: number,
481
- onChange: (value: number) => void
482
- ): HTMLDivElement {
483
- const spinner = document.createElement('div');
484
- spinner.className = 'datepicker-time-spinner';
485
-
486
- const upBtn = document.createElement('button');
487
- upBtn.className = 'datepicker-time-btn';
488
- upBtn.innerHTML = '&#9650;';
489
- upBtn.onclick = (e: MouseEvent) => {
490
- e.stopPropagation();
491
- const next = value + 1 > max ? min : value + 1;
492
- onChange(next);
493
- this.render();
494
- };
495
-
496
- const display = document.createElement('input');
497
- display.className = 'datepicker-time-display';
498
- display.type = 'text';
499
- display.inputMode = 'numeric';
500
- display.value = String(value).padStart(2, '0');
501
- display.maxLength = 2;
502
-
503
- display.addEventListener('click', (e: Event) => e.stopPropagation());
504
- display.addEventListener('focus', () => display.select());
505
- display.addEventListener('change', (e: Event) => {
506
- e.stopPropagation();
507
- let parsed = parseInt(display.value, 10);
508
- if (isNaN(parsed) || parsed < min || parsed > max) {
509
- display.value = String(value).padStart(2, '0');
510
- return;
511
- }
512
- onChange(parsed);
513
- this.render();
514
- });
515
- display.addEventListener('keydown', (e: KeyboardEvent) => {
516
- if (e.key === 'ArrowUp') {
517
- e.preventDefault();
518
- const next = value + 1 > max ? min : value + 1;
519
- onChange(next);
520
- this.render();
521
- } else if (e.key === 'ArrowDown') {
522
- e.preventDefault();
523
- const next = value - 1 < min ? max : value - 1;
524
- onChange(next);
525
- this.render();
526
- }
527
- });
528
-
529
- const downBtn = document.createElement('button');
530
- downBtn.className = 'datepicker-time-btn';
531
- downBtn.innerHTML = '&#9660;';
532
- downBtn.onclick = (e: MouseEvent) => {
533
- e.stopPropagation();
534
- const next = value - 1 < min ? max : value - 1;
535
- onChange(next);
536
- this.render();
537
- };
538
-
539
- spinner.appendChild(upBtn);
540
- spinner.appendChild(display);
541
- spinner.appendChild(downBtn);
542
-
543
- return spinner;
544
- }
545
-
546
- private applyTimeToSelection(): void {
547
- if (this.options.mode === 'single' && this.selectedDate) {
548
- this.selectedDate.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
549
- this.updateInput(this.options!.format!(this.selectedDate));
550
- this.options!.onSelect!(this.selectedDate);
551
- } else if (this.options.mode === 'range') {
552
- if (this.rangeStart) {
553
- this.rangeStart.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
554
- }
555
- if (this.rangeStart && this.rangeEnd) {
556
- const startDate = this.options!.format!(this.rangeStart);
557
- const endDate = this.options!.format!(this.rangeEnd);
558
- this.updateInput(`${startDate} - ${endDate}`);
559
- } else if (this.rangeStart) {
560
- this.updateInput(this.options!.format!(this.rangeStart) + ' - ...');
561
- }
562
- this.options!.onSelect!({ start: this.rangeStart, end: this.rangeEnd });
563
- }
564
- }
565
-
566
- private changeMonth(delta: number): void {
567
- this.viewMonth += delta;
568
- if (this.viewMonth > 11) {
569
- this.viewMonth = 0;
570
- this.viewYear++;
571
- } else if (this.viewMonth < 0) {
572
- this.viewMonth = 11;
573
- this.viewYear--;
574
- }
575
- this.render();
576
- }
577
-
578
- private handleDateClick(date: Date): void {
579
- if (this.options.timePicker) {
580
- date.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
581
- } else {
582
- date.setHours(0, 0, 0, 0);
583
- }
584
-
585
- if (this.options.mode === 'single') {
586
- this.selectedDate = date;
587
- this.updateInput(this.options!.format!(this.selectedDate));
588
- this.options!.onSelect!(this.selectedDate);
589
- if (!this.options.timePicker) {
590
- this.hide();
591
- }
592
- } else {
593
- if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) {
594
- this.rangeStart = date;
595
- this.rangeEnd = null;
596
- this.updateInput(this.options!.format!(this.rangeStart) + ' - ...');
597
- } else {
598
- if (date.getTime() < this.rangeStart.getTime()) {
599
- this.rangeEnd = this.rangeStart;
600
- this.rangeStart = date;
601
- } else {
602
- this.rangeEnd = date;
603
- }
604
- const startDate = this.options!.format!(this.rangeStart);
605
- const endDate = this.options!.format!(this.rangeEnd);
606
- if (startDate === endDate) {
607
- this.updateInput(startDate);
608
- } else {
609
- this.updateInput(`${startDate} - ${endDate}`);
610
- }
611
- if (!this.options.timePicker) {
612
- this.hide();
613
- }
614
- }
615
- this.options!.onSelect!({ start: this.rangeStart, end: this.rangeEnd });
616
- }
617
- this.render();
618
- }
619
-
620
- private updateInput(value: string): void {
621
- if (this.input) {
622
- this.input.value = value;
623
- }
624
- }
625
- }
626
-
627
- export { DatePicker };
1
+ interface DatePickerLocales {
2
+ days: string[];
3
+ months: string[];
4
+ }
5
+
6
+ interface DatePickerOptions {
7
+ mode?: 'single' | 'range';
8
+ startDay?: number;
9
+ timePicker?: boolean;
10
+ locales?: DatePickerLocales;
11
+ format?: (date: Date) => string;
12
+ onSelect?: (date: Date | DateRange) => void;
13
+ }
14
+
15
+ interface DateRange {
16
+ start: Date | null;
17
+ end: Date | null;
18
+ }
19
+
20
+ type ViewMode = 'days' | 'months' | 'years';
21
+
22
+ class DatePicker {
23
+ private input: HTMLInputElement | null;
24
+ private options: DatePickerOptions;
25
+ private currentDate: Date;
26
+ private selectedDate: Date | null;
27
+ private rangeStart: Date | null;
28
+ private rangeEnd: Date | null;
29
+ private viewYear: number;
30
+ private viewMonth: number;
31
+ private viewMode: ViewMode;
32
+ private yearRangeStart: number;
33
+ private selectedHours: number;
34
+ private selectedMinutes: number;
35
+ private calendar!: HTMLDivElement;
36
+ private backdrop!: HTMLDivElement;
37
+ private handleDocumentClick!: (e: Event) => void;
38
+
39
+
40
+ constructor(elementOrSelector: string | HTMLInputElement, options: DatePickerOptions = {}) {
41
+ this.input = typeof elementOrSelector === 'string'
42
+ ? document.querySelector<HTMLInputElement>(elementOrSelector)
43
+ : elementOrSelector;
44
+
45
+ if (!this.input) {
46
+ throw new Error(`DatePicker: Element not found for selector "${elementOrSelector}"`);
47
+ }
48
+
49
+ const timePicker = options.timePicker ?? false;
50
+
51
+ this.options = {
52
+ mode: 'single',
53
+ startDay: 0,
54
+ timePicker,
55
+ locales: {
56
+ days: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
57
+ months: [
58
+ 'January', 'February', 'March', 'April', 'May', 'June',
59
+ 'July', 'August', 'September', 'October', 'November', 'December'
60
+ ]
61
+ },
62
+ format: timePicker
63
+ ? (date: Date) => {
64
+ const hours = String(date.getHours()).padStart(2, '0');
65
+ const minutes = String(date.getMinutes()).padStart(2, '0');
66
+ return `${date.toDateString()} ${hours}:${minutes}`;
67
+ }
68
+ : (date: Date) => date.toDateString(),
69
+ onSelect: () => {},
70
+ ...options
71
+ };
72
+
73
+ this.currentDate = new Date();
74
+ this.selectedDate = null;
75
+ this.rangeStart = null;
76
+ this.rangeEnd = null;
77
+
78
+ this.viewYear = this.currentDate.getFullYear();
79
+ this.viewMonth = this.currentDate.getMonth();
80
+
81
+ this.viewMode = 'days';
82
+ this.yearRangeStart = this.viewYear - (this.viewYear % 12);
83
+
84
+ this.selectedHours = this.currentDate.getHours();
85
+ this.selectedMinutes = this.currentDate.getMinutes();
86
+
87
+ this.init();
88
+ }
89
+
90
+ private init(): void {
91
+ this.createCalendarElement();
92
+ this.attachEvents();
93
+ this.render();
94
+ }
95
+
96
+ private createCalendarElement(): void {
97
+ this.calendar = document.createElement('div');
98
+ this.calendar.className = 'datepicker';
99
+ document.body.appendChild(this.calendar);
100
+
101
+ this.backdrop = document.createElement('div');
102
+ this.backdrop.className = 'datepicker-backdrop';
103
+ document.body.appendChild(this.backdrop);
104
+
105
+ this.backdrop.addEventListener('click', () => this.hide());
106
+ }
107
+
108
+ private attachEvents(): void {
109
+ const toggle = (e: Event): void => {
110
+ e.preventDefault();
111
+ e.stopPropagation();
112
+
113
+ if (this.calendar.classList.contains('visible')) {
114
+ this.hide();
115
+ } else {
116
+ this.show();
117
+ }
118
+ };
119
+
120
+ this.input?.addEventListener('click', toggle);
121
+
122
+ this.backdrop.addEventListener('click', (e: Event) => {
123
+ e.preventDefault();
124
+ e.stopPropagation();
125
+ this.hide();
126
+ });
127
+
128
+ this.handleDocumentClick = (e: Event): void => {
129
+ if (this.calendar.classList.contains('mobile')) return;
130
+
131
+ const target = e.target as Node;
132
+ if (!this.calendar.contains(target) && target !== this.input) {
133
+ this.hide();
134
+ }
135
+ };
136
+ }
137
+
138
+ private show(): void {
139
+ const isMobile = window.innerWidth <= 640;
140
+
141
+ if (isMobile) {
142
+ this.calendar.classList.add('mobile');
143
+ this.backdrop.classList.add('visible');
144
+ document.body.style.overflow = 'hidden';
145
+
146
+ this.calendar.style.top = '';
147
+ this.calendar.style.left = '';
148
+ } else {
149
+ this.calendar.classList.remove('mobile');
150
+ this.backdrop.classList.remove('visible');
151
+ document.body.style.overflow = '';
152
+
153
+ if (this.input) {
154
+ const rect = this.input.getBoundingClientRect();
155
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
156
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
157
+
158
+ this.calendar.style.top = `${rect.bottom + scrollTop + 5}px`;
159
+ this.calendar.style.left = `${rect.left + scrollLeft}px`;
160
+
161
+ if (rect.left + 320 > window.innerWidth) {
162
+ this.calendar.style.left = `${rect.right + scrollLeft - 320}px`;
163
+ }
164
+ }
165
+
166
+ setTimeout(() => {
167
+ document.addEventListener('click', this.handleDocumentClick);
168
+ }, 0);
169
+ }
170
+
171
+ this.calendar.classList.add('visible');
172
+ }
173
+
174
+ private hide(): void {
175
+ this.calendar.classList.remove('visible');
176
+ this.backdrop.classList.remove('visible');
177
+ document.body.style.overflow = '';
178
+
179
+ document.removeEventListener('click', this.handleDocumentClick);
180
+ }
181
+
182
+ private render(): void {
183
+ this.calendar.innerHTML = '';
184
+
185
+ const header = this.createHeader();
186
+ let content: HTMLDivElement;
187
+
188
+ if (this.viewMode === 'days') {
189
+ content = this.createGrid();
190
+ } else if (this.viewMode === 'months') {
191
+ content = this.createMonthGrid();
192
+ } else {
193
+ content = this.createYearGrid();
194
+ }
195
+
196
+ this.calendar.appendChild(header);
197
+ this.calendar.appendChild(content);
198
+
199
+ if (this.options.timePicker && this.viewMode === 'days') {
200
+ const timeSection = this.createTimePicker();
201
+ this.calendar.appendChild(timeSection);
202
+
203
+ const setBtn = document.createElement('button');
204
+ setBtn.className = 'datepicker-set-btn';
205
+ setBtn.textContent = 'Set';
206
+ setBtn.onclick = (e: MouseEvent) => {
207
+ e.stopPropagation();
208
+ this.hide();
209
+ };
210
+ this.calendar.appendChild(setBtn);
211
+ }
212
+ }
213
+
214
+ private createHeader(): HTMLDivElement {
215
+ const header = document.createElement('div');
216
+ header.className = 'datepicker-header';
217
+
218
+ const prevBtn = document.createElement('button');
219
+ prevBtn.className = 'datepicker-nav';
220
+ prevBtn.innerHTML = '&lt;';
221
+ prevBtn.onclick = (e: MouseEvent) => {
222
+ e.stopPropagation();
223
+ this.navigate(-1);
224
+ };
225
+
226
+ const title = document.createElement('div');
227
+ title.className = 'datepicker-title';
228
+
229
+ if (this.viewMode === 'days') {
230
+ const monthBtn = document.createElement('button');
231
+ monthBtn.className = 'datepicker-title-btn';
232
+
233
+ monthBtn.textContent = this.options?.locales?.months[this.viewMonth] ?? '';
234
+ monthBtn.onclick = (e: MouseEvent) => {
235
+ e.stopPropagation();
236
+ this.viewMode = 'months';
237
+ this.render();
238
+ };
239
+
240
+ const yearBtn = document.createElement('button');
241
+ yearBtn.className = 'datepicker-title-btn';
242
+ yearBtn.textContent = String(this.viewYear);
243
+ yearBtn.onclick = (e: MouseEvent) => {
244
+ e.stopPropagation();
245
+ this.viewMode = 'years';
246
+ this.yearRangeStart = this.viewYear - (this.viewYear % 12);
247
+ this.render();
248
+ };
249
+
250
+ title.appendChild(monthBtn);
251
+ title.appendChild(yearBtn);
252
+ } else if (this.viewMode === 'months') {
253
+ const yearBtn = document.createElement('button');
254
+ yearBtn.className = 'datepicker-title-btn';
255
+ yearBtn.textContent = String(this.viewYear);
256
+ yearBtn.onclick = (e: MouseEvent) => {
257
+ e.stopPropagation();
258
+ this.viewMode = 'years';
259
+ this.yearRangeStart = this.viewYear - (this.viewYear % 12);
260
+ this.render();
261
+ };
262
+ title.appendChild(yearBtn);
263
+ } else {
264
+ const rangeText = document.createElement('span');
265
+ rangeText.style.fontWeight = '600';
266
+ rangeText.textContent = `${this.yearRangeStart} - ${this.yearRangeStart + 11}`;
267
+ title.appendChild(rangeText);
268
+ }
269
+
270
+ const nextBtn = document.createElement('button');
271
+ nextBtn.className = 'datepicker-nav';
272
+ nextBtn.innerHTML = '&gt;';
273
+ nextBtn.onclick = (e: MouseEvent) => {
274
+ e.stopPropagation();
275
+ this.navigate(1);
276
+ };
277
+
278
+ header.appendChild(prevBtn);
279
+ header.appendChild(title);
280
+ header.appendChild(nextBtn);
281
+
282
+ return header;
283
+ }
284
+
285
+ private navigate(delta: number): void {
286
+ if (this.viewMode === 'days') {
287
+ this.changeMonth(delta);
288
+ } else if (this.viewMode === 'months') {
289
+ this.viewYear += delta;
290
+ this.render();
291
+ } else {
292
+ this.yearRangeStart += delta * 12;
293
+ this.render();
294
+ }
295
+ }
296
+
297
+ private createMonthGrid(): HTMLDivElement {
298
+ const grid = document.createElement('div');
299
+ grid.className = 'datepicker-grid-months';
300
+
301
+ this.options?.locales?.months.forEach((month, index) => {
302
+ const el = document.createElement('div');
303
+ el.className = 'datepicker-month';
304
+ el.textContent = month.substring(0, 3);
305
+
306
+ if (index === this.viewMonth) {
307
+ el.classList.add('selected');
308
+ }
309
+ if (index === new Date().getMonth() && this.viewYear === new Date().getFullYear()) {
310
+ el.classList.add('current');
311
+ }
312
+
313
+ el.onclick = (e: MouseEvent) => {
314
+ e.stopPropagation();
315
+ this.viewMonth = index;
316
+ this.viewMode = 'days';
317
+ this.render();
318
+ };
319
+ grid.appendChild(el);
320
+ });
321
+
322
+ return grid;
323
+ }
324
+
325
+ private createYearGrid(): HTMLDivElement {
326
+ const grid = document.createElement('div');
327
+ grid.className = 'datepicker-grid-years';
328
+
329
+ for (let i = 0; i < 12; i++) {
330
+ const year = this.yearRangeStart + i;
331
+ const el = document.createElement('div');
332
+ el.className = 'datepicker-year';
333
+ el.textContent = String(year);
334
+
335
+ if (year === this.viewYear) {
336
+ el.classList.add('selected');
337
+ }
338
+ if (year === new Date().getFullYear()) {
339
+ el.classList.add('current');
340
+ }
341
+
342
+ el.onclick = (e: MouseEvent) => {
343
+ e.stopPropagation();
344
+ this.viewYear = year;
345
+ this.viewMode = 'months';
346
+ this.render();
347
+ };
348
+ grid.appendChild(el);
349
+ }
350
+
351
+ return grid;
352
+ }
353
+
354
+ private createGrid(): HTMLDivElement {
355
+ const grid = document.createElement('div');
356
+ grid.className = 'datepicker-grid';
357
+
358
+ const days = this.options?.locales?.days ?? ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
359
+ const startDay = this.options.startDay ?? 0;
360
+ const adjustedDays = [...days.slice(startDay), ...days.slice(0, startDay)];
361
+
362
+ adjustedDays.forEach(day => {
363
+ const el = document.createElement('div');
364
+ el.className = 'datepicker-day-header';
365
+ el.textContent = day;
366
+ grid.appendChild(el);
367
+ });
368
+
369
+ const firstDayOfMonth = new Date(this.viewYear, this.viewMonth, 1).getDay();
370
+ const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate();
371
+
372
+ const offset = (firstDayOfMonth - startDay + 7) % 7;
373
+
374
+ const prevMonthDays = new Date(this.viewYear, this.viewMonth, 0).getDate();
375
+ for (let i = offset - 1; i >= 0; i--) {
376
+ const day = document.createElement('div');
377
+ day.className = 'datepicker-day other-month';
378
+ day.textContent = String(prevMonthDays - i);
379
+ grid.appendChild(day);
380
+ }
381
+
382
+ for (let i = 1; i <= daysInMonth; i++) {
383
+ const day = document.createElement('div');
384
+ day.className = 'datepicker-day';
385
+ day.textContent = String(i);
386
+
387
+ const date = new Date(this.viewYear, this.viewMonth, i);
388
+ date.setHours(0, 0, 0, 0);
389
+
390
+ const today = new Date();
391
+ today.setHours(0, 0, 0, 0);
392
+ if (date.getTime() === today.getTime()) {
393
+ day.classList.add('today');
394
+ }
395
+
396
+ if (this.options.mode === 'single') {
397
+ const selectedDay = this.selectedDate ? new Date(this.selectedDate) : null;
398
+ if (selectedDay) selectedDay.setHours(0, 0, 0, 0);
399
+ if (selectedDay && date.getTime() === selectedDay.getTime()) {
400
+ day.classList.add('selected');
401
+ }
402
+ } else {
403
+ const t = date.getTime();
404
+ const start = this.rangeStart ? this.rangeStart.getTime() : null;
405
+ const end = this.rangeEnd ? this.rangeEnd.getTime() : null;
406
+
407
+ if (start && t === start) {
408
+ day.classList.add('range-start');
409
+ }
410
+ if (end && t === end) {
411
+ day.classList.add('range-end');
412
+ }
413
+ if (start && end && t > start && t < end) {
414
+ day.classList.add('in-range');
415
+ }
416
+ if (start && !end && t === start) {
417
+ day.classList.add('selected');
418
+ }
419
+ }
420
+
421
+ day.onclick = (e: MouseEvent) => {
422
+ e.stopPropagation();
423
+ this.handleDateClick(date);
424
+ };
425
+ grid.appendChild(day);
426
+ }
427
+
428
+ return grid;
429
+ }
430
+
431
+ private createTimePicker(): HTMLDivElement {
432
+ const wrapper = document.createElement('div');
433
+ wrapper.className = 'datepicker-time';
434
+
435
+ const label = document.createElement('div');
436
+ label.className = 'datepicker-time-label';
437
+ label.textContent = 'Time';
438
+ wrapper.appendChild(label);
439
+
440
+ const controls = document.createElement('div');
441
+ controls.className = 'datepicker-time-controls';
442
+
443
+ // Hours spinner
444
+ const hoursSpinner = this.createSpinner(
445
+ this.selectedHours,
446
+ 0,
447
+ 23,
448
+ (value) => {
449
+ this.selectedHours = value;
450
+ this.applyTimeToSelection();
451
+ }
452
+ );
453
+
454
+ const separator = document.createElement('span');
455
+ separator.className = 'datepicker-time-separator';
456
+ separator.textContent = ':';
457
+
458
+ // Minutes spinner
459
+ const minutesSpinner = this.createSpinner(
460
+ this.selectedMinutes,
461
+ 0,
462
+ 59,
463
+ (value) => {
464
+ this.selectedMinutes = value;
465
+ this.applyTimeToSelection();
466
+ }
467
+ );
468
+
469
+ controls.appendChild(hoursSpinner);
470
+ controls.appendChild(separator);
471
+ controls.appendChild(minutesSpinner);
472
+ wrapper.appendChild(controls);
473
+
474
+ return wrapper;
475
+ }
476
+
477
+ private createSpinner(
478
+ value: number,
479
+ min: number,
480
+ max: number,
481
+ onChange: (value: number) => void
482
+ ): HTMLDivElement {
483
+ const spinner = document.createElement('div');
484
+ spinner.className = 'datepicker-time-spinner';
485
+
486
+ const upBtn = document.createElement('button');
487
+ upBtn.className = 'datepicker-time-btn';
488
+ upBtn.innerHTML = '&#9650;';
489
+ upBtn.onclick = (e: MouseEvent) => {
490
+ e.stopPropagation();
491
+ const next = value + 1 > max ? min : value + 1;
492
+ onChange(next);
493
+ this.render();
494
+ };
495
+
496
+ const display = document.createElement('input');
497
+ display.className = 'datepicker-time-display';
498
+ display.type = 'text';
499
+ display.inputMode = 'numeric';
500
+ display.value = String(value).padStart(2, '0');
501
+ display.maxLength = 2;
502
+
503
+ display.addEventListener('click', (e: Event) => e.stopPropagation());
504
+ display.addEventListener('focus', () => display.select());
505
+ display.addEventListener('change', (e: Event) => {
506
+ e.stopPropagation();
507
+ let parsed = parseInt(display.value, 10);
508
+ if (isNaN(parsed) || parsed < min || parsed > max) {
509
+ display.value = String(value).padStart(2, '0');
510
+ return;
511
+ }
512
+ onChange(parsed);
513
+ this.render();
514
+ });
515
+ display.addEventListener('keydown', (e: KeyboardEvent) => {
516
+ if (e.key === 'ArrowUp') {
517
+ e.preventDefault();
518
+ const next = value + 1 > max ? min : value + 1;
519
+ onChange(next);
520
+ this.render();
521
+ } else if (e.key === 'ArrowDown') {
522
+ e.preventDefault();
523
+ const next = value - 1 < min ? max : value - 1;
524
+ onChange(next);
525
+ this.render();
526
+ }
527
+ });
528
+
529
+ const downBtn = document.createElement('button');
530
+ downBtn.className = 'datepicker-time-btn';
531
+ downBtn.innerHTML = '&#9660;';
532
+ downBtn.onclick = (e: MouseEvent) => {
533
+ e.stopPropagation();
534
+ const next = value - 1 < min ? max : value - 1;
535
+ onChange(next);
536
+ this.render();
537
+ };
538
+
539
+ spinner.appendChild(upBtn);
540
+ spinner.appendChild(display);
541
+ spinner.appendChild(downBtn);
542
+
543
+ return spinner;
544
+ }
545
+
546
+ private applyTimeToSelection(): void {
547
+ if (this.options.mode === 'single' && this.selectedDate) {
548
+ this.selectedDate.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
549
+ this.updateInput(this.options!.format!(this.selectedDate));
550
+ this.options!.onSelect!(this.selectedDate);
551
+ } else if (this.options.mode === 'range') {
552
+ if (this.rangeStart) {
553
+ this.rangeStart.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
554
+ }
555
+ if (this.rangeStart && this.rangeEnd) {
556
+ const startDate = this.options!.format!(this.rangeStart);
557
+ const endDate = this.options!.format!(this.rangeEnd);
558
+ this.updateInput(`${startDate} - ${endDate}`);
559
+ } else if (this.rangeStart) {
560
+ this.updateInput(this.options!.format!(this.rangeStart) + ' - ...');
561
+ }
562
+ this.options!.onSelect!({ start: this.rangeStart, end: this.rangeEnd });
563
+ }
564
+ }
565
+
566
+ private changeMonth(delta: number): void {
567
+ this.viewMonth += delta;
568
+ if (this.viewMonth > 11) {
569
+ this.viewMonth = 0;
570
+ this.viewYear++;
571
+ } else if (this.viewMonth < 0) {
572
+ this.viewMonth = 11;
573
+ this.viewYear--;
574
+ }
575
+ this.render();
576
+ }
577
+
578
+ private handleDateClick(date: Date): void {
579
+ if (this.options.timePicker) {
580
+ date.setHours(this.selectedHours, this.selectedMinutes, 0, 0);
581
+ } else {
582
+ date.setHours(0, 0, 0, 0);
583
+ }
584
+
585
+ if (this.options.mode === 'single') {
586
+ this.selectedDate = date;
587
+ this.updateInput(this.options!.format!(this.selectedDate));
588
+ this.options!.onSelect!(this.selectedDate);
589
+ if (!this.options.timePicker) {
590
+ this.hide();
591
+ }
592
+ } else {
593
+ if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) {
594
+ this.rangeStart = date;
595
+ this.rangeEnd = null;
596
+ this.updateInput(this.options!.format!(this.rangeStart) + ' - ...');
597
+ } else {
598
+ if (date.getTime() < this.rangeStart.getTime()) {
599
+ this.rangeEnd = this.rangeStart;
600
+ this.rangeStart = date;
601
+ } else {
602
+ this.rangeEnd = date;
603
+ }
604
+ const startDate = this.options!.format!(this.rangeStart);
605
+ const endDate = this.options!.format!(this.rangeEnd);
606
+ if (startDate === endDate) {
607
+ this.updateInput(startDate);
608
+ } else {
609
+ this.updateInput(`${startDate} - ${endDate}`);
610
+ }
611
+ if (!this.options.timePicker) {
612
+ this.hide();
613
+ }
614
+ }
615
+ this.options!.onSelect!({ start: this.rangeStart, end: this.rangeEnd });
616
+ }
617
+ this.render();
618
+ }
619
+
620
+ private updateInput(value: string): void {
621
+ if (this.input) {
622
+ this.input.value = value;
623
+ }
624
+ }
625
+ }
626
+
627
+ export { DatePicker };
628
628
  export type { DatePickerOptions, DatePickerLocales, DateRange };