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