@dodlhuat/basix 1.0.0 → 1.1.1

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