@brickclay-org/ui 0.0.9 → 0.0.13

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 (31) hide show
  1. package/README.md +1 -1
  2. package/package.json +16 -15
  3. package/ng-package.json +0 -8
  4. package/src/lib/assets/icons.ts +0 -8
  5. package/src/lib/brickclay-lib.spec.ts +0 -23
  6. package/src/lib/brickclay-lib.ts +0 -15
  7. package/src/lib/calender/calendar.module.ts +0 -35
  8. package/src/lib/calender/components/custom-calendar/custom-calendar.component.css +0 -698
  9. package/src/lib/calender/components/custom-calendar/custom-calendar.component.html +0 -230
  10. package/src/lib/calender/components/custom-calendar/custom-calendar.component.spec.ts +0 -23
  11. package/src/lib/calender/components/custom-calendar/custom-calendar.component.ts +0 -1534
  12. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.css +0 -373
  13. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.html +0 -210
  14. package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.ts +0 -360
  15. package/src/lib/calender/components/time-picker/time-picker.component.css +0 -174
  16. package/src/lib/calender/components/time-picker/time-picker.component.html +0 -60
  17. package/src/lib/calender/components/time-picker/time-picker.component.ts +0 -283
  18. package/src/lib/calender/services/calendar-manager.service.ts +0 -45
  19. package/src/lib/checkbox/checkbox.css +0 -26
  20. package/src/lib/checkbox/checkbox.html +0 -42
  21. package/src/lib/checkbox/checkbox.ts +0 -67
  22. package/src/lib/radio/radio.css +0 -39
  23. package/src/lib/radio/radio.html +0 -58
  24. package/src/lib/radio/radio.ts +0 -77
  25. package/src/lib/toggle/components/toggle.component.css +0 -74
  26. package/src/lib/toggle/components/toggle.component.html +0 -24
  27. package/src/lib/toggle/components/toggle.component.ts +0 -62
  28. package/src/public-api.ts +0 -20
  29. package/tsconfig.lib.json +0 -19
  30. package/tsconfig.lib.prod.json +0 -11
  31. package/tsconfig.spec.json +0 -15
@@ -1,1534 +0,0 @@
1
- import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, HostListener, OnChanges, SimpleChanges } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { FormsModule } from '@angular/forms';
4
- import { CalendarManagerService } from '../../services/calendar-manager.service';
5
- import { Subscription } from 'rxjs';
6
- import { TimePickerComponent } from '../time-picker/time-picker.component';
7
- import moment from 'moment';
8
- import { BrickclayIcons } from '../../../assets/icons';
9
- export interface CalendarRange {
10
- start: Date;
11
- end: Date;
12
- }
13
-
14
- export interface CalendarSelection {
15
- startDate: Date | null;
16
- endDate: Date | null;
17
- selectedDates?: Date[]; // For multi-date selection
18
- }
19
-
20
- @Component({
21
- selector: 'brickclay-custom-calendar',
22
- standalone: true,
23
- imports: [CommonModule, FormsModule, TimePickerComponent],
24
- templateUrl: './custom-calendar.component.html',
25
- styleUrls: ['./custom-calendar.component.css']
26
- })
27
- export class CustomCalendarComponent implements OnInit, OnDestroy, OnChanges {
28
-
29
- // Basic Options
30
- @Input() enableTimepicker = false;
31
- @Input() autoApply = false;
32
- @Input() closeOnAutoApply = false;
33
- @Input() showCancel = true;
34
- @Input() linkedCalendars = false;
35
- @Input() singleDatePicker = false;
36
- @Input() showWeekNumbers = false;
37
- @Input() showISOWeekNumbers = false;
38
- @Input() customRangeDirection = false;
39
- @Input() lockStartDate = false;
40
- @Input() position: 'center' | 'left' | 'right' = 'left';
41
- @Input() drop: 'up' | 'down' = 'down';
42
- @Input() dualCalendar = false;
43
- @Input() showRanges = true;
44
- @Input() timeFormat: 12 | 24 = 24;
45
- @Input() enableSeconds = false;
46
- @Input() customRanges?: Record<string, CalendarRange>;
47
- @Input() multiDateSelection = false; // NEW: Enable multi-date selection
48
- @Input() maxDate?: Date; // NEW: Maximum selectable date
49
- @Input() minDate?: Date; // NEW: Minimum selectable date
50
- @Input() placeholder = 'Select date range'; // NEW: Custom placeholder
51
- @Input() opens: 'left' | 'right' | 'center' = 'left'; // NEW: Popup position
52
- @Input() inline = false; // NEW: Always show calendar inline (no popup)
53
- @Input() isDisplayCrossIcon = true; // NEW: Show/Hide clear (X) icon
54
-
55
- @Output() selected = new EventEmitter<CalendarSelection>();
56
- @Output() opened = new EventEmitter<void>();
57
- @Output() closed = new EventEmitter<void>();
58
-
59
- /**
60
- * External value passed from parent. If provided, component will select these dates on init / change.
61
- * Accepts { startDate: Date|null, endDate: Date|null, selectedDates?: Date[] }
62
- */
63
- @Input() selectedValue: CalendarSelection | null = null;
64
- /** Optional display format for the input value. Uses moment formatting tokens. */
65
- @Input() displayFormat = 'MM/DD/YYYY';
66
-
67
- brickclayIcons = BrickclayIcons;
68
-
69
- show = false;
70
- today = new Date();
71
- month = this.today.getMonth();
72
- year = this.today.getFullYear();
73
- calendar: { day: number, currentMonth: boolean }[][] = [];
74
- leftMonth!: number;
75
- leftYear!: number;
76
- rightMonth!: number;
77
- rightYear!: number;
78
- leftCalendar: { day: number, currentMonth: boolean }[][] = [];
79
- rightCalendar: { day: number, currentMonth: boolean }[][] = [];
80
- startDate: Date | null = null;
81
- endDate: Date | null = null;
82
- selectedDates: Date[] = []; // NEW: For multi-date selection
83
- disableHighlight = false;
84
- hoveredDate: Date | null = null; // For hover preview
85
-
86
- // Track raw input values for minutes to allow free typing
87
- minuteInputValues: { [key: string]: string } = {};
88
-
89
- // Time picker for single calendar (12-hour format: 1-12)
90
- selectedHour = 1;
91
- selectedMinute = 0;
92
- selectedSecond = 0;
93
- selectedAMPM: 'AM' | 'PM' = 'AM';
94
-
95
- // NEW: Separate time pickers for dual calendar (12-hour format: 1-12)
96
- startHour = 1;
97
- startMinute = 0;
98
- startSecond = 0;
99
- startAMPM: 'AM' | 'PM' = 'AM';
100
- endHour = 2;
101
- endMinute = 0;
102
- endSecond = 0;
103
- endAMPM: 'AM' | 'PM' = 'AM';
104
-
105
- // Track open time-picker within this calendar (for single-open behavior)
106
- openTimePickerId: string | null = null;
107
- closePickerCounter: { [key: string]: number } = {};
108
-
109
- defaultRanges: Record<string, CalendarRange> = {};
110
- activeRange: string | null = null; // Track which range is currently active
111
- rangeOrder: string[] = []; // Maintain order of ranges
112
-
113
- private unregisterFn?: () => void;
114
- private closeAllSubscription?: Subscription;
115
- private closeFn?: () => void;
116
-
117
- constructor(private calendarManager: CalendarManagerService) {}
118
-
119
- @HostListener('document:click', ['$event'])
120
- onClickOutside(event: MouseEvent) {
121
- // Don't handle click outside if inline mode is enabled
122
- if (this.inline) {
123
- return;
124
- }
125
- const target = event.target as HTMLElement;
126
- if (this.show && !target.closest('.calendar-container')) {
127
- this.close();
128
- }
129
- }
130
-
131
- ngOnInit() {
132
- if (!this.customRanges) {
133
- this.initializeDefaultRanges();
134
- } else {
135
- // If customRanges is provided via @Input, set the order based on the keys
136
- // Maintain the desired order if keys match, otherwise use provided order
137
- const desiredOrder = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'This Month', 'Last Month', 'Custom Range'];
138
- const providedKeys = Object.keys(this.customRanges);
139
- // Check if Custom Range exists, if not add it
140
- if (!this.customRanges['Custom Range']) {
141
- this.customRanges['Custom Range'] = { start: new Date(), end: new Date() };
142
- }
143
- // Build order: first add desired order items that exist, then add any remaining
144
- this.rangeOrder = desiredOrder.filter(key => providedKeys.includes(key) || key === 'Custom Range');
145
- const remaining = providedKeys.filter(key => !this.rangeOrder.includes(key));
146
- this.rangeOrder = [...this.rangeOrder, ...remaining];
147
- }
148
- if (this.dualCalendar) this.initializeDual();
149
- else this.generateCalendar();
150
-
151
- // Initialize time from existing dates if available
152
- if (this.startDate) {
153
- this.initializeTimeFromDate(this.startDate, true);
154
- }
155
- if (this.endDate) {
156
- this.initializeTimeFromDate(this.endDate, false);
157
- }
158
-
159
- // Check if current dates match any predefined range
160
- if (this.startDate && this.endDate) {
161
- this.checkAndSetActiveRange();
162
- }
163
-
164
- // If inline mode, always show calendar
165
- if (this.inline) {
166
- this.show = true;
167
- }
168
-
169
- // Register this calendar instance with the manager service
170
- this.closeFn = () => {
171
- if (this.show && !this.inline) {
172
- this.close();
173
- }
174
- };
175
- this.unregisterFn = this.calendarManager.register(this.closeFn);
176
-
177
- // Subscribe to close all events (skip if inline)
178
- this.closeAllSubscription = this.calendarManager.closeAll$.subscribe(() => {
179
- if (this.show && !this.inline) {
180
- this.close();
181
- }
182
- });
183
- }
184
-
185
- ngOnChanges(changes: SimpleChanges) {
186
- if (changes['selectedValue'] && this.selectedValue) {
187
- // Normalize incoming values to Date or null
188
- const s = this.selectedValue;
189
- this.startDate = s.startDate ? new Date(s.startDate) : null;
190
- this.endDate = s.endDate ? new Date(s.endDate) : null;
191
- this.selectedDates = (s.selectedDates || []).map((d) => new Date(d));
192
-
193
- // Update calendar month/year to show the start date (or end date if start missing)
194
- const focusDate = this.startDate ?? this.endDate ?? new Date();
195
- this.month = focusDate.getMonth();
196
- this.year = focusDate.getFullYear();
197
- if (this.dualCalendar) {
198
- this.initializeDual();
199
- } else {
200
- this.generateCalendar();
201
- }
202
-
203
- // Re-evaluate active range if any
204
- this.checkAndSetActiveRange();
205
- }
206
- }
207
-
208
- ngOnDestroy() {
209
- // Unregister this calendar instance
210
- if (this.unregisterFn) {
211
- this.unregisterFn();
212
- }
213
-
214
- // Unsubscribe from close all events
215
- if (this.closeAllSubscription) {
216
- this.closeAllSubscription.unsubscribe();
217
- }
218
- }
219
-
220
- checkAndSetActiveRange() {
221
- if (!this.customRanges || !this.startDate || !this.endDate) return;
222
-
223
- // Normalize dates for comparison (ignore time)
224
- const normalizeDate = (date: Date) => {
225
- const d = new Date(date);
226
- d.setHours(0, 0, 0, 0);
227
- return d;
228
- };
229
-
230
- const start = normalizeDate(this.startDate);
231
- const end = normalizeDate(this.endDate);
232
-
233
- // Check each range (except Custom Range)
234
- for (const key of this.rangeOrder) {
235
- if (key === 'Custom Range') continue;
236
- const range = this.customRanges![key];
237
- if (range) {
238
- const rangeStart = normalizeDate(range.start);
239
- const rangeEnd = normalizeDate(range.end);
240
- if (start.getTime() === rangeStart.getTime() && end.getTime() === rangeEnd.getTime()) {
241
- this.activeRange = key;
242
- return;
243
- }
244
- }
245
- }
246
-
247
- // If no match found, it's a custom range
248
- this.activeRange = 'Custom Range';
249
- }
250
-
251
- initializeDefaultRanges() {
252
- const today = new Date();
253
- this.customRanges = {
254
- 'Today': { start: new Date(today.getFullYear(), today.getMonth(), today.getDate()), end: new Date(today.getFullYear(), today.getMonth(), today.getDate()) },
255
- 'Yesterday': { start: this.addDays(today, -1), end: this.addDays(today, -1) },
256
- 'Last 7 Days': { start: this.addDays(today, -6), end: today },
257
- 'Last 30 Days': { start: this.addDays(today, -29), end: today },
258
- 'This Month': { start: new Date(today.getFullYear(), today.getMonth(), 1), end: today },
259
- 'Last Month': { start: new Date(today.getFullYear(), today.getMonth() - 1, 1), end: new Date(today.getFullYear(), today.getMonth(), 0) },
260
- 'Custom Range': { start: new Date(), end: new Date() }, // Placeholder, won't be used for selection
261
- };
262
- // Set the order of ranges
263
- this.rangeOrder = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'This Month', 'Last Month', 'Custom Range'];
264
- }
265
-
266
- initializeTimeFromDate(date: Date, isStart: boolean) {
267
- // Always use 12-hour format
268
- const hours24 = date.getHours();
269
- const minutes = date.getMinutes();
270
- const seconds = date.getSeconds();
271
-
272
- if (isStart) {
273
- this.startMinute = minutes;
274
- this.startSecond = seconds;
275
- if (hours24 >= 12) {
276
- this.startAMPM = 'PM';
277
- this.startHour = hours24 > 12 ? hours24 - 12 : 12;
278
- } else {
279
- this.startAMPM = 'AM';
280
- this.startHour = hours24 === 0 ? 12 : hours24;
281
- }
282
- } else {
283
- this.endMinute = minutes;
284
- this.endSecond = seconds;
285
- if (hours24 >= 12) {
286
- this.endAMPM = 'PM';
287
- this.endHour = hours24 > 12 ? hours24 - 12 : 12;
288
- } else {
289
- this.endAMPM = 'AM';
290
- this.endHour = hours24 === 0 ? 12 : hours24;
291
- }
292
- }
293
- }
294
-
295
- toggle() {
296
- // Don't toggle if inline mode is enabled
297
- if (this.inline) {
298
- return;
299
- }
300
- const wasOpen = this.show;
301
- this.show = !this.show;
302
-
303
- if (this.show) {
304
- // If opening, close all other calendars first
305
- if (!wasOpen && this.closeFn) {
306
- this.calendarManager.closeAllExcept(this.closeFn);
307
- }
308
- this.disableHighlight = false;
309
- this.opened.emit();
310
- } else {
311
- this.closed.emit();
312
- }
313
- }
314
-
315
- close() {
316
- // Don't close if inline mode is enabled
317
- if (this.inline) {
318
- return;
319
- }
320
- this.show = false;
321
- this.closed.emit();
322
- }
323
-
324
- onDateHover(day: number | null, fromRight = false) {
325
- if (!day || this.singleDatePicker || this.multiDateSelection) {
326
- this.hoveredDate = null;
327
- return;
328
- }
329
-
330
- // Only show hover preview if start date is selected but end date is not
331
- if (!this.startDate || this.endDate) {
332
- this.hoveredDate = null;
333
- return;
334
- }
335
-
336
- if (!this.dualCalendar) {
337
- this.hoveredDate = new Date(this.year, this.month, day);
338
- } else {
339
- this.hoveredDate = fromRight
340
- ? new Date(this.rightYear, this.rightMonth, day)
341
- : new Date(this.leftYear, this.leftMonth, day);
342
- }
343
- }
344
-
345
- onDateLeave() {
346
- this.hoveredDate = null;
347
- }
348
-
349
- selectDate(day: number | null, fromRight = false) {
350
- if (!day) return;
351
-
352
- let selected: Date;
353
- if (!this.dualCalendar) {
354
- selected = new Date(this.year, this.month, day);
355
- } else {
356
- selected = fromRight
357
- ? new Date(this.rightYear, this.rightMonth, day)
358
- : new Date(this.leftYear, this.leftMonth, day);
359
- }
360
-
361
- // Clear hover on selection
362
- this.hoveredDate = null;
363
-
364
- // Check min/max date constraints
365
- if (this.minDate && selected < this.minDate) return;
366
- if (this.maxDate && selected > this.maxDate) return;
367
-
368
- // Multi-date selection mode
369
- if (this.multiDateSelection) {
370
- this.handleMultiDateSelection(selected);
371
- return;
372
- }
373
-
374
- // Apply time if timepicker is enabled (convert 12-hour to 24-hour)
375
- if (this.enableTimepicker) {
376
- if (this.dualCalendar) {
377
- // For dual calendar, use separate start/end times
378
- // If no startDate OR endDate exists, we're selecting start date
379
- const isStart = !this.startDate || !!this.endDate;
380
- this.applyTimeToDate(selected, isStart);
381
- } else {
382
- // For single calendar, always use selected time for start
383
- this.applyTimeToDate(selected, true);
384
- }
385
- }
386
-
387
- // Single date picker mode
388
- if (this.singleDatePicker) {
389
- this.startDate = selected;
390
- this.endDate = null;
391
- // Activate Custom Range when manually selecting dates
392
- this.activeRange = 'Custom Range';
393
- // Apply time immediately if timepicker is enabled
394
- if (this.enableTimepicker) {
395
- this.applyTimeToDate(this.startDate, true);
396
- }
397
- if (this.autoApply) {
398
- this.apply();
399
- if (this.closeOnAutoApply && !this.inline) this.close();
400
- } else {
401
- // Always emit selection event even if autoApply is false (especially for inline calendars)
402
- this.emitSelection();
403
- }
404
- return;
405
- }
406
-
407
- // Range selection mode
408
- if (!this.startDate || this.endDate) {
409
- this.startDate = selected;
410
- this.endDate = null;
411
- // Activate Custom Range when manually selecting dates
412
- this.activeRange = 'Custom Range';
413
- // Keep left calendar on the selected month for better UX
414
- if (this.dualCalendar) {
415
- this.leftMonth = selected.getMonth();
416
- this.leftYear = selected.getFullYear();
417
- // Reset right calendar to original position (next month after left) when end date is cleared
418
- this.rightMonth = this.leftMonth + 1;
419
- this.rightYear = this.leftYear;
420
- if (this.rightMonth > 11) {
421
- this.rightMonth = 0;
422
- this.rightYear++;
423
- }
424
- this.generateDualCalendars();
425
- }
426
- // Don't overwrite time picker values - keep current values and apply them to the date
427
- // Time picker values are already set by user, we just apply them to the selected date
428
- } else {
429
- if (selected < this.startDate && !this.customRangeDirection) {
430
- this.endDate = this.startDate;
431
- this.startDate = selected;
432
- // Activate Custom Range when manually selecting dates
433
- this.activeRange = 'Custom Range';
434
- // Swap times if needed
435
- if (this.dualCalendar && this.enableTimepicker) {
436
- [this.startHour, this.endHour] = [this.endHour, this.startHour];
437
- [this.startMinute, this.endMinute] = [this.endMinute, this.startMinute];
438
- [this.startSecond, this.endSecond] = [this.endSecond, this.startSecond];
439
- [this.startAMPM, this.endAMPM] = [this.endAMPM, this.startAMPM];
440
- }
441
- // Keep left calendar on the selected month
442
- if (this.dualCalendar) {
443
- this.leftMonth = selected.getMonth();
444
- this.leftYear = selected.getFullYear();
445
- this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
446
- }
447
- } else {
448
- this.endDate = selected;
449
- // Activate Custom Range when manually selecting dates
450
- this.activeRange = 'Custom Range';
451
- // Only move right calendar if end date is in a different month than start date
452
- if (this.dualCalendar) {
453
- if (this.startDate) {
454
- const startMonth = this.startDate.getMonth();
455
- const startYear = this.startDate.getFullYear();
456
- const endMonth = selected.getMonth();
457
- const endYear = selected.getFullYear();
458
-
459
- // Only move right calendar if end date is in a different month
460
- if (endMonth !== startMonth || endYear !== startYear) {
461
- this.rightMonth = endMonth;
462
- this.rightYear = endYear;
463
- this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
464
- }
465
- // If both dates are in same month, keep right calendar in its current position
466
- } else {
467
- // If no start date, move right calendar to end date month
468
- this.rightMonth = selected.getMonth();
469
- this.rightYear = selected.getFullYear();
470
- this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
471
- }
472
- }
473
- // Don't overwrite time picker values - keep current end time values
474
- // Time picker values are already set by user
475
- }
476
- if (this.autoApply) {
477
- this.apply();
478
- if (this.closeOnAutoApply && !this.inline) this.close();
479
- } else {
480
- // Check if the selection matches a predefined range
481
- this.checkAndSetActiveRange();
482
- // Always emit selection event for inline calendars
483
- if (this.inline) {
484
- this.emitSelection();
485
- }
486
- }
487
- }
488
- }
489
-
490
- handleMultiDateSelection(selected: Date) {
491
- const dateStr = this.getDateString(selected);
492
- const existingIndex = this.selectedDates.findIndex(d => this.getDateString(d) === dateStr);
493
-
494
- if (existingIndex >= 0) {
495
- // Deselect if already selected
496
- this.selectedDates.splice(existingIndex, 1);
497
- } else {
498
- // Add to selection
499
- this.selectedDates.push(new Date(selected));
500
- this.selectedDates.sort((a, b) => a.getTime() - b.getTime());
501
- }
502
-
503
- // Update startDate and endDate for compatibility
504
- if (this.selectedDates.length > 0) {
505
- this.startDate = new Date(this.selectedDates[0]);
506
- this.endDate = new Date(this.selectedDates[this.selectedDates.length - 1]);
507
- // Activate Custom Range when manually selecting dates
508
- this.activeRange = 'Custom Range';
509
- } else {
510
- this.startDate = null;
511
- this.endDate = null;
512
- this.activeRange = null;
513
- }
514
-
515
- // Always emit selection event for inline calendars or when autoApply is true
516
- if (this.autoApply || this.inline) {
517
- this.emitSelection();
518
- if (this.closeOnAutoApply && !this.inline) this.close();
519
- }
520
- }
521
-
522
- getDateString(date: Date): string {
523
- return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
524
- }
525
-
526
- isDateInMultiSelection(year: number, month: number, day: number): boolean {
527
- if (!this.multiDateSelection || this.selectedDates.length === 0) return false;
528
- const cellDate = new Date(year, month, day);
529
- return this.selectedDates.some(d => this.getDateString(d) === this.getDateString(cellDate));
530
- }
531
-
532
- apply() {
533
- // Format minute inputs to 2 digits before applying
534
- this.formatAllMinuteInputs();
535
-
536
- // Apply time to dates
537
- if (this.enableTimepicker) {
538
- if (this.dualCalendar) {
539
- // Dual calendar with separate start/end times (always 12-hour format)
540
- if (this.startDate) {
541
- this.applyTimeToDate(this.startDate, true);
542
- }
543
- if (this.endDate) {
544
- this.applyTimeToDate(this.endDate, false);
545
- }
546
- } else {
547
- // Single calendar with time (always 12-hour format)
548
- if (this.startDate) {
549
- this.applyTimeToDate(this.startDate, true);
550
- }
551
- if (this.endDate && !this.singleDatePicker) {
552
- this.applyTimeToDate(this.endDate, true);
553
- }
554
- }
555
- }
556
-
557
- // Check if the selection matches a predefined range
558
- this.checkAndSetActiveRange();
559
-
560
- this.emitSelection();
561
- this.disableHighlight = true;
562
- this.close();
563
- }
564
-
565
- cancel() {
566
- this.startDate = null;
567
- this.endDate = null;
568
- this.selectedDates = [];
569
- this.close();
570
- }
571
-
572
- clear() {
573
- this.startDate = null;
574
- this.endDate = null;
575
- this.selectedDates = [];
576
- this.activeRange = null; // Clear active range
577
-
578
- // Reset right calendar to original position (next month after left) when end date is cleared
579
- if (this.dualCalendar && !this.endDate) {
580
- this.rightMonth = this.leftMonth + 1;
581
- this.rightYear = this.leftYear;
582
- if (this.rightMonth > 11) {
583
- this.rightMonth = 0;
584
- this.rightYear++;
585
- }
586
- this.generateDualCalendars();
587
- }
588
-
589
- this.emitSelection();
590
- }
591
-
592
- chooseRange(key: string) {
593
- if (!this.customRanges) return;
594
- // Don't allow selecting "Custom Range" directly - it's only activated when manually selecting dates
595
- if (key === 'Custom Range') return;
596
- const r = this.customRanges[key];
597
- if (!r) return;
598
- this.startDate = new Date(r.start);
599
- this.endDate = new Date(r.end);
600
- this.selectedDates = [];
601
- this.activeRange = key; // Set active range
602
-
603
- // Navigate calendars to show the selected date range
604
- if (this.dualCalendar) {
605
- // For dual calendar: left always shows start date month
606
- if (this.startDate) {
607
- this.leftMonth = this.startDate.getMonth();
608
- this.leftYear = this.startDate.getFullYear();
609
- }
610
-
611
- // Right calendar logic
612
- if (this.endDate && this.startDate) {
613
- const startMonth = this.startDate.getMonth();
614
- const startYear = this.startDate.getFullYear();
615
- const endMonth = this.endDate.getMonth();
616
- const endYear = this.endDate.getFullYear();
617
-
618
- // Only move right calendar if end date is in a different month than start date
619
- if (endMonth !== startMonth || endYear !== startYear) {
620
- this.rightMonth = endMonth;
621
- this.rightYear = endYear;
622
- } else {
623
- // If both dates are in same month, reset right calendar to default position (next month after left)
624
- this.rightMonth = this.leftMonth + 1;
625
- this.rightYear = this.leftYear;
626
- if (this.rightMonth > 11) {
627
- this.rightMonth = 0;
628
- this.rightYear++;
629
- }
630
- }
631
- } else if (this.endDate && !this.startDate) {
632
- // If only end date exists, show it in right calendar
633
- this.rightMonth = this.endDate.getMonth();
634
- this.rightYear = this.endDate.getFullYear();
635
- } else {
636
- // If no end date, reset right calendar to default position
637
- this.rightMonth = this.leftMonth + 1;
638
- this.rightYear = this.leftYear;
639
- if (this.rightMonth > 11) {
640
- this.rightMonth = 0;
641
- this.rightYear++;
642
- }
643
- }
644
-
645
- this.generateDualCalendars();
646
- } else {
647
- // For single calendar: show the start date month (or end date if only end date exists)
648
- if (this.startDate) {
649
- this.month = this.startDate.getMonth();
650
- this.year = this.startDate.getFullYear();
651
- } else if (this.endDate) {
652
- this.month = this.endDate.getMonth();
653
- this.year = this.endDate.getFullYear();
654
- }
655
- this.generateCalendar();
656
- }
657
-
658
- this.emitSelection();
659
- if (this.autoApply || this.closeOnAutoApply) {
660
- this.close();
661
- }
662
- }
663
-
664
- emitSelection() {
665
- const selection: CalendarSelection = {
666
- startDate: this.startDate,
667
- endDate: this.endDate
668
- };
669
- if (this.multiDateSelection) {
670
- selection.selectedDates = [...this.selectedDates];
671
- }
672
- this.selected.emit(selection);
673
- }
674
-
675
- addDays(date: Date, days: number): Date {
676
- const d = new Date(date);
677
- d.setDate(d.getDate() + days);
678
- return d;
679
- }
680
-
681
- generateCalendar() {
682
- this.calendar = this.buildCalendar(this.year, this.month);
683
- }
684
-
685
- nextMonth() {
686
- if (!this.dualCalendar) {
687
- this.month++;
688
- if (this.month > 11) { this.month = 0; this.year++; }
689
- this.generateCalendar();
690
- return;
691
- }
692
- // For dual calendar, this should not be used - use nextLeftMonth or nextRightMonth instead
693
- this.nextLeftMonth();
694
- }
695
-
696
- prevMonth() {
697
- if (!this.dualCalendar) {
698
- this.month--;
699
- if (this.month < 0) { this.month = 11; this.year--; }
700
- this.generateCalendar();
701
- return;
702
- }
703
- // For dual calendar, this should not be used - use prevLeftMonth or prevRightMonth instead
704
- this.prevLeftMonth();
705
- }
706
-
707
- // Independent navigation for left calendar
708
- nextLeftMonth() {
709
- this.leftMonth++;
710
- if (this.leftMonth > 11) { this.leftMonth = 0; this.leftYear++; }
711
- this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
712
- }
713
-
714
- prevLeftMonth() {
715
- this.leftMonth--;
716
- if (this.leftMonth < 0) { this.leftMonth = 11; this.leftYear--; }
717
- this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
718
- }
719
-
720
- // Independent navigation for right calendar
721
- nextRightMonth() {
722
- this.rightMonth++;
723
- if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
724
- this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
725
- }
726
-
727
- prevRightMonth() {
728
- this.rightMonth--;
729
- if (this.rightMonth < 0) { this.rightMonth = 11; this.rightYear--; }
730
- this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
731
- }
732
-
733
- initializeDual() {
734
- this.leftMonth = this.today.getMonth();
735
- this.leftYear = this.today.getFullYear();
736
- // Initialize right calendar to next month, but they can move independently
737
- this.rightMonth = this.leftMonth + 1;
738
- this.rightYear = this.leftYear;
739
- if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
740
- this.generateDualCalendars();
741
- }
742
-
743
- generateDualCalendars() {
744
- this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
745
- this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
746
- }
747
-
748
- buildCalendar(year: number, month: number): { day: number, currentMonth: boolean }[][] {
749
- const firstDay = new Date(year, month, 1).getDay();
750
- const daysInMonth = new Date(year, month + 1, 0).getDate();
751
- const prevMonthDays = new Date(year, month, 0).getDate();
752
-
753
- const grid: { day: number, currentMonth: boolean }[][] = [];
754
- let row: { day: number, currentMonth: boolean }[] = [];
755
-
756
- // Adjust first day (0 = Sunday, 1 = Monday, etc.)
757
- const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1; // Make Monday = 0
758
-
759
- for (let i = adjustedFirstDay - 1; i >= 0; i--) {
760
- row.push({ day: prevMonthDays - i, currentMonth: false });
761
- }
762
- for (let d = 1; d <= daysInMonth; d++) {
763
- row.push({ day: d, currentMonth: true });
764
- if (row.length === 7) { grid.push(row); row = []; }
765
- }
766
- let nextMonthDay = 1;
767
- while (row.length > 0 && row.length < 7) {
768
- row.push({ day: nextMonthDay++, currentMonth: false });
769
- }
770
- if (row.length) grid.push(row);
771
-
772
- // Ensure we always have 6 rows (42 cells total) for consistent layout
773
- while (grid.length < 6) {
774
- const newRow: { day: number, currentMonth: boolean }[] = [];
775
- for (let i = 0; i < 7; i++) {
776
- newRow.push({ day: nextMonthDay++, currentMonth: false });
777
- }
778
- grid.push(newRow);
779
- }
780
-
781
- return grid;
782
- }
783
-
784
- isDateSelected(year: number, month: number, day: number): boolean {
785
- if (this.disableHighlight) return false;
786
- if (!day) return false;
787
-
788
- // Multi-date selection
789
- if (this.multiDateSelection) {
790
- return this.isDateInMultiSelection(year, month, day);
791
- }
792
-
793
- const cellDate = new Date(year, month, day);
794
-
795
- // Check if it's today (highlight today by default if no date selected)
796
- const today = new Date();
797
- const isToday = cellDate.getFullYear() === today.getFullYear() &&
798
- cellDate.getMonth() === today.getMonth() &&
799
- cellDate.getDate() === today.getDate();
800
-
801
- // If no startDate is set and it's today, highlight it
802
- if (!this.startDate && isToday) {
803
- return true;
804
- }
805
-
806
- if (!this.startDate) return false;
807
-
808
- // Check if date is disabled
809
- if (this.minDate && cellDate < this.minDate) return false;
810
- if (this.maxDate && cellDate > this.maxDate) return false;
811
-
812
- const sameDay =
813
- cellDate.getFullYear() === this.startDate.getFullYear() &&
814
- cellDate.getMonth() === this.startDate.getMonth() &&
815
- cellDate.getDate() === this.startDate.getDate();
816
-
817
- if (this.singleDatePicker) return sameDay;
818
-
819
- // For range selection: only highlight start and end dates (not in-between)
820
- if (this.startDate && this.endDate) {
821
- const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
822
- const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
823
- return cellDate.getTime() === start.getTime() || cellDate.getTime() === end.getTime();
824
- }
825
-
826
- // If only start date is selected and hovering, check if this is start or hovered end
827
- if (this.startDate && !this.endDate && this.hoveredDate) {
828
- const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
829
- const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
830
- // Show both start and hovered date as selected (circular black)
831
- return cellDate.getTime() === start.getTime() || cellDate.getTime() === hovered.getTime();
832
- }
833
-
834
- return sameDay;
835
- }
836
-
837
- isDateInRange(year: number, month: number, day: number): boolean {
838
- if (this.disableHighlight || !day) return false;
839
- if (this.singleDatePicker) return false;
840
- if (this.multiDateSelection) return false;
841
-
842
- const cellDate = new Date(year, month, day);
843
-
844
- // If both start and end are selected, show gray background for dates in between
845
- if (this.startDate && this.endDate) {
846
- const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
847
- const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
848
- return cellDate > start && cellDate < end;
849
- }
850
-
851
- // If only start is selected and hovering, show preview range
852
- if (this.startDate && !this.endDate && this.hoveredDate) {
853
- const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
854
- const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
855
-
856
- // Determine which is earlier - show gray background for dates between them
857
- const minDate = hovered < start ? hovered : start;
858
- const maxDate = hovered >= start ? hovered : start;
859
- return cellDate > minDate && cellDate < maxDate;
860
- }
861
-
862
- return false;
863
- }
864
-
865
- isDateDisabled(year: number, month: number, day: number): boolean {
866
- if (!day) return false;
867
- const cellDate = new Date(year, month, day);
868
- if (this.minDate && cellDate < this.minDate) return true;
869
- if (this.maxDate && cellDate > this.maxDate) return true;
870
- return false;
871
- }
872
-
873
- isToday(year: number, month: number, day: number): boolean {
874
- if (!day) return false;
875
- const today = new Date();
876
- const cellDate = new Date(year, month, day);
877
- return cellDate.getFullYear() === today.getFullYear() &&
878
- cellDate.getMonth() === today.getMonth() &&
879
- cellDate.getDate() === today.getDate();
880
- }
881
-
882
- getDisplayValue(): string {
883
- if (this.multiDateSelection && this.selectedDates.length > 0) {
884
- if (this.selectedDates.length === 1) {
885
- return moment(this.selectedDates[0]).format(this.displayFormat);
886
- }
887
- return `${this.selectedDates.length} dates selected`;
888
- }
889
-
890
- if (!this.startDate) return '';
891
-
892
- // Prefer moment formatting for consistent display
893
- let dateStr = moment(this.startDate).format(this.displayFormat);
894
-
895
- if (this.enableTimepicker && !this.dualCalendar) {
896
- const hr = this.startDate.getHours().toString().padStart(2, '0');
897
- const min = this.startDate.getMinutes().toString().padStart(2, '0');
898
- dateStr += ` ${hr}:${min}`;
899
- if (this.enableSeconds) {
900
- const sec = this.startDate.getSeconds().toString().padStart(2, '0');
901
- dateStr += `:${sec}`;
902
- }
903
- }
904
-
905
- if (this.endDate && !this.singleDatePicker) {
906
- let endStr = moment(this.endDate).format(this.displayFormat);
907
- if (this.enableTimepicker) {
908
- if (this.dualCalendar) {
909
- const startHr = this.startDate.getHours().toString().padStart(2, '0');
910
- const startMin = this.startDate.getMinutes().toString().padStart(2, '0');
911
- dateStr += ` ${startHr}:${startMin}`;
912
- if (this.enableSeconds) {
913
- const startSec = this.startDate.getSeconds().toString().padStart(2, '0');
914
- dateStr += `:${startSec}`;
915
- }
916
- }
917
- const endHr = this.endDate.getHours().toString().padStart(2, '0');
918
- const endMin = this.endDate.getMinutes().toString().padStart(2, '0');
919
- endStr += ` ${endHr}:${endMin}`;
920
- if (this.enableSeconds) {
921
- const endSec = this.endDate.getSeconds().toString().padStart(2, '0');
922
- endStr += `:${endSec}`;
923
- }
924
- }
925
- return `${dateStr} - ${endStr}`;
926
- }
927
- return dateStr;
928
- }
929
-
930
- // Time picker helpers
931
- getTimeInputValue(isStart = true): string {
932
- const h = isStart ? this.startHour : this.endHour;
933
- const m = isStart ? this.startMinute : this.endMinute;
934
- return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
935
- }
936
-
937
- getSingleTimeInputValue(): string {
938
- return `${this.selectedHour.toString().padStart(2, '0')}:${this.selectedMinute.toString().padStart(2, '0')}`;
939
- }
940
-
941
- // NEW: Helper to build display value for TimePickerComponent (single calendar)
942
- getSingleTimePickerDisplay(): string {
943
- const hour = this.selectedHour || 12;
944
- const minuteStr = this.selectedMinute.toString().padStart(2, '0');
945
- const ampm = this.selectedAMPM || 'AM';
946
- return `${hour}:${minuteStr} ${ampm}`;
947
- }
948
-
949
- // NEW: Helper to build display value for TimePickerComponent (dual calendar)
950
- getDualTimePickerDisplay(isStart = true): string {
951
- const hour = isStart ? (this.startHour || 12) : (this.endHour || 12);
952
- const minute = isStart ? this.startMinute : this.endMinute;
953
- const ampm = isStart ? (this.startAMPM || 'AM') : (this.endAMPM || 'AM');
954
- const minuteStr = minute.toString().padStart(2, '0');
955
- return `${hour}:${minuteStr} ${ampm}`;
956
- }
957
-
958
- // Coordination helpers for embedded TimePickerComponent instances
959
- onTimePickerOpened(pickerId: string) {
960
- // Close previously open picker inside this calendar
961
- if (this.openTimePickerId && this.openTimePickerId !== pickerId) {
962
- if (!this.closePickerCounter[this.openTimePickerId]) {
963
- this.closePickerCounter[this.openTimePickerId] = 0;
964
- }
965
- this.closePickerCounter[this.openTimePickerId]++;
966
- }
967
- this.openTimePickerId = pickerId;
968
- }
969
-
970
- onTimePickerClosed(pickerId: string) {
971
- if (this.openTimePickerId === pickerId) {
972
- this.openTimePickerId = null;
973
- }
974
- }
975
-
976
- shouldClosePicker(pickerId: string): number {
977
- return this.closePickerCounter[pickerId] || 0;
978
- }
979
-
980
- // NEW: Parse "H:MM AM/PM" (or "HH:MM" 24h) from TimePickerComponent
981
- private parsePickerTimeString(timeStr: string): { hour12: number; minute: number; ampm: 'AM' | 'PM' } {
982
- if (!timeStr) {
983
- return { hour12: 12, minute: 0, ampm: 'AM' };
984
- }
985
-
986
- const parts = timeStr.trim().split(' ');
987
- const timePart = parts[0] || '12:00';
988
- let ampmPart = (parts[1] || '').toUpperCase();
989
-
990
- const [hourStr, minuteStr] = timePart.split(':');
991
- let hour = parseInt(hourStr || '12', 10);
992
- const minute = parseInt(minuteStr || '0', 10);
993
-
994
- if (ampmPart !== 'AM' && ampmPart !== 'PM') {
995
- // Interpret as 24-hour input and convert
996
- if (hour >= 12) {
997
- ampmPart = 'PM';
998
- if (hour > 12) hour -= 12;
999
- } else {
1000
- ampmPart = 'AM';
1001
- if (hour === 0) hour = 12;
1002
- }
1003
- }
1004
-
1005
- // Clamp to 1-12 range just in case
1006
- if (hour < 1) hour = 1;
1007
- if (hour > 12) hour = 12;
1008
-
1009
- return { hour12: hour, minute, ampm: ampmPart as 'AM' | 'PM' };
1010
- }
1011
-
1012
- // NEW: Handle TimePickerComponent change for single calendar
1013
- onSingleTimePickerChange(time: string) {
1014
- const { hour12, minute, ampm } = this.parsePickerTimeString(time);
1015
-
1016
- this.selectedHour = hour12;
1017
- this.selectedMinute = minute;
1018
- this.selectedAMPM = ampm;
1019
-
1020
- if (this.startDate) {
1021
- let h24 = hour12;
1022
- if (ampm === 'PM' && h24 < 12) h24 += 12;
1023
- if (ampm === 'AM' && h24 === 12) h24 = 0;
1024
- this.startDate.setHours(h24, minute, this.selectedSecond);
1025
- this.emitSelection();
1026
- }
1027
- }
1028
-
1029
- // NEW: Handle TimePickerComponent change for dual calendar
1030
- onDualTimePickerChange(time: string, isStart = true) {
1031
- const { hour12, minute, ampm } = this.parsePickerTimeString(time);
1032
-
1033
- if (isStart) {
1034
- this.startHour = hour12;
1035
- this.startMinute = minute;
1036
- this.startAMPM = ampm;
1037
-
1038
- if (this.startDate) {
1039
- let h24 = hour12;
1040
- if (ampm === 'PM' && h24 < 12) h24 += 12;
1041
- if (ampm === 'AM' && h24 === 12) h24 = 0;
1042
- this.startDate.setHours(h24, minute, this.startSecond);
1043
- }
1044
- } else {
1045
- this.endHour = hour12;
1046
- this.endMinute = minute;
1047
- this.endAMPM = ampm;
1048
-
1049
- if (this.endDate) {
1050
- let h24 = hour12;
1051
- if (ampm === 'PM' && h24 < 12) h24 += 12;
1052
- if (ampm === 'AM' && h24 === 12) h24 = 0;
1053
- this.endDate.setHours(h24, minute, this.endSecond);
1054
- }
1055
- }
1056
-
1057
- this.emitSelection();
1058
- }
1059
-
1060
- onTimeChange(event: any, isStart = true) {
1061
- const [h, m] = event.target.value.split(':').map(Number);
1062
- if (isStart) {
1063
- this.startHour = h;
1064
- this.startMinute = m;
1065
- if (this.startDate) {
1066
- this.startDate.setHours(h, m, this.startSecond);
1067
- this.emitSelection();
1068
- }
1069
- } else {
1070
- this.endHour = h;
1071
- this.endMinute = m;
1072
- if (this.endDate) {
1073
- this.endDate.setHours(h, m, this.endSecond);
1074
- this.emitSelection();
1075
- }
1076
- }
1077
- }
1078
-
1079
- onSingleTimeChange(event: any) {
1080
- const [h, m] = event.target.value.split(':').map(Number);
1081
- this.selectedHour = h;
1082
- this.selectedMinute = m;
1083
- if (this.startDate) {
1084
- this.startDate.setHours(h, m, this.selectedSecond);
1085
- this.emitSelection();
1086
- }
1087
- }
1088
-
1089
- // Custom time picker controls
1090
- incrementHour(isStart = true) {
1091
- // 12-hour format: 1-12
1092
- if (isStart) {
1093
- this.startHour = this.startHour >= 12 ? 1 : this.startHour + 1;
1094
- // Toggle AM/PM at 12
1095
- if (this.startHour === 12) {
1096
- this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1097
- }
1098
- if (this.startDate) {
1099
- let h = this.startHour;
1100
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1101
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1102
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1103
- }
1104
- } else {
1105
- this.endHour = this.endHour >= 12 ? 1 : this.endHour + 1;
1106
- // Toggle AM/PM at 12
1107
- if (this.endHour === 12) {
1108
- this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1109
- }
1110
- if (this.endDate) {
1111
- let h = this.endHour;
1112
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1113
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1114
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1115
- }
1116
- }
1117
- this.emitSelection();
1118
- }
1119
-
1120
- decrementHour(isStart = true) {
1121
- // 12-hour format: 1-12
1122
- if (isStart) {
1123
- this.startHour = this.startHour <= 1 ? 12 : this.startHour - 1;
1124
- // Toggle AM/PM at 12
1125
- if (this.startHour === 12) {
1126
- this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1127
- }
1128
- if (this.startDate) {
1129
- let h = this.startHour;
1130
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1131
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1132
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1133
- }
1134
- } else {
1135
- this.endHour = this.endHour <= 1 ? 12 : this.endHour - 1;
1136
- // Toggle AM/PM at 12
1137
- if (this.endHour === 12) {
1138
- this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1139
- }
1140
- if (this.endDate) {
1141
- let h = this.endHour;
1142
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1143
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1144
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1145
- }
1146
- }
1147
- this.emitSelection();
1148
- }
1149
-
1150
- incrementMinute(isStart = true) {
1151
- if (isStart) {
1152
- this.startMinute = (this.startMinute + 1) % 60;
1153
- if (this.startDate) {
1154
- let h = this.startHour;
1155
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1156
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1157
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1158
- }
1159
- } else {
1160
- this.endMinute = (this.endMinute + 1) % 60;
1161
- if (this.endDate) {
1162
- let h = this.endHour;
1163
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1164
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1165
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1166
- }
1167
- }
1168
- this.emitSelection();
1169
- }
1170
-
1171
- decrementMinute(isStart = true) {
1172
- if (isStart) {
1173
- this.startMinute = this.startMinute <= 0 ? 59 : this.startMinute - 1;
1174
- if (this.startDate) {
1175
- let h = this.startHour;
1176
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1177
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1178
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1179
- }
1180
- } else {
1181
- this.endMinute = this.endMinute <= 0 ? 59 : this.endMinute - 1;
1182
- if (this.endDate) {
1183
- let h = this.endHour;
1184
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1185
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1186
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1187
- }
1188
- }
1189
- this.emitSelection();
1190
- }
1191
-
1192
- toggleAMPM(isStart = true) {
1193
- if (isStart) {
1194
- this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
1195
- if (this.startDate) {
1196
- let h = this.startHour;
1197
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1198
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1199
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1200
- }
1201
- } else {
1202
- this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
1203
- if (this.endDate) {
1204
- let h = this.endHour;
1205
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1206
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1207
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1208
- }
1209
- }
1210
- this.emitSelection();
1211
- }
1212
-
1213
- // Single calendar time picker controls (12-hour format: 1-12)
1214
- incrementSingleHour() {
1215
- this.selectedHour = this.selectedHour >= 12 ? 1 : this.selectedHour + 1;
1216
- // Toggle AM/PM at 12
1217
- if (this.selectedHour === 12) {
1218
- this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1219
- }
1220
- if (this.startDate) {
1221
- let h = this.selectedHour;
1222
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1223
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1224
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1225
- this.emitSelection();
1226
- }
1227
- }
1228
-
1229
- decrementSingleHour() {
1230
- this.selectedHour = this.selectedHour <= 1 ? 12 : this.selectedHour - 1;
1231
- // Toggle AM/PM at 12
1232
- if (this.selectedHour === 12) {
1233
- this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1234
- }
1235
- if (this.startDate) {
1236
- let h = this.selectedHour;
1237
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1238
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1239
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1240
- this.emitSelection();
1241
- }
1242
- }
1243
-
1244
- incrementSingleMinute() {
1245
- this.selectedMinute = (this.selectedMinute + 1) % 60;
1246
- if (this.startDate) {
1247
- let h = this.selectedHour;
1248
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1249
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1250
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1251
- this.emitSelection();
1252
- }
1253
- }
1254
-
1255
- decrementSingleMinute() {
1256
- this.selectedMinute = this.selectedMinute <= 0 ? 59 : this.selectedMinute - 1;
1257
- if (this.startDate) {
1258
- let h = this.selectedHour;
1259
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1260
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1261
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1262
- this.emitSelection();
1263
- }
1264
- }
1265
-
1266
- toggleSingleAMPM() {
1267
- this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
1268
- if (this.startDate) {
1269
- let h = this.selectedHour;
1270
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1271
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1272
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1273
- this.emitSelection();
1274
- }
1275
- }
1276
-
1277
- getMonthName(month: number): string {
1278
- const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
1279
- return months[month];
1280
- }
1281
-
1282
- // Input handlers for direct hour/minute input (12-hour format only)
1283
- onHourInput(event: any, isStart = true, isSingle = false): void {
1284
- const inputValue = event.target.value;
1285
-
1286
- // Allow empty input while typing
1287
- if (inputValue === '' || inputValue === null || inputValue === undefined) {
1288
- return;
1289
- }
1290
-
1291
- let value = parseInt(inputValue) || 0;
1292
-
1293
- // Validate: 1-12 for 12-hour format
1294
- if (value < 1) value = 1;
1295
- if (value > 12) value = 12;
1296
-
1297
- if (isSingle) {
1298
- this.selectedHour = value;
1299
- if (this.startDate) {
1300
- let h = value;
1301
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1302
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1303
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1304
- this.emitSelection();
1305
- }
1306
- } else if (isStart) {
1307
- this.startHour = value;
1308
- if (this.startDate) {
1309
- let h = value;
1310
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1311
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1312
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1313
- this.emitSelection();
1314
- }
1315
- } else {
1316
- this.endHour = value;
1317
- if (this.endDate) {
1318
- let h = value;
1319
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1320
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1321
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1322
- this.emitSelection();
1323
- }
1324
- }
1325
-
1326
- // Don't format during input, only on blur
1327
- event.target.value = value.toString();
1328
- }
1329
-
1330
- onHourBlur(event: any, isStart = true, isSingle = false): void {
1331
- const inputValue = event.target.value;
1332
- if (inputValue === '' || inputValue === null || inputValue === undefined) {
1333
- // If empty, set to current value
1334
- const currentValue = isSingle ? this.selectedHour : (isStart ? this.startHour : this.endHour);
1335
- event.target.value = currentValue.toString();
1336
- return;
1337
- }
1338
-
1339
- let value = parseInt(inputValue) || 0;
1340
- if (value < 1) value = 1;
1341
- if (value > 12) value = 12;
1342
-
1343
- // Format to single digit (no padding for hours in 12-hour format)
1344
- event.target.value = value.toString();
1345
-
1346
- // Update the value
1347
- if (isSingle) {
1348
- this.selectedHour = value;
1349
- if (this.startDate) {
1350
- let h = value;
1351
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1352
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1353
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1354
- this.emitSelection();
1355
- }
1356
- } else if (isStart) {
1357
- this.startHour = value;
1358
- if (this.startDate) {
1359
- let h = value;
1360
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1361
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1362
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1363
- this.emitSelection();
1364
- }
1365
- } else {
1366
- this.endHour = value;
1367
- if (this.endDate) {
1368
- let h = value;
1369
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1370
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1371
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1372
- this.emitSelection();
1373
- }
1374
- }
1375
- }
1376
-
1377
- onMinuteInput(event: any, isStart = true, isSingle = false): void {
1378
- const inputValue = event.target.value;
1379
- const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1380
-
1381
- // Remove any non-digit characters
1382
- const digitsOnly = inputValue.replace(/\D/g, '');
1383
-
1384
- // Store raw input value for display
1385
- this.minuteInputValues[key] = digitsOnly;
1386
-
1387
- // Allow empty input while typing
1388
- if (digitsOnly === '' || digitsOnly === null || digitsOnly === undefined) {
1389
- return; // Don't modify the input, let user clear it
1390
- }
1391
-
1392
- // Allow typing up to 2 digits without formatting
1393
- let value = parseInt(digitsOnly) || 0;
1394
-
1395
- // If user types more than 2 digits, take only first 2
1396
- if (digitsOnly.length > 2) {
1397
- value = parseInt(digitsOnly.substring(0, 2));
1398
- this.minuteInputValues[key] = digitsOnly.substring(0, 2);
1399
- event.target.value = digitsOnly.substring(0, 2);
1400
- }
1401
-
1402
- // If value exceeds 59, clamp it to 59
1403
- if (value > 59) {
1404
- value = 59;
1405
- this.minuteInputValues[key] = '59';
1406
- event.target.value = '59';
1407
- }
1408
-
1409
- // Update the internal value silently (don't emit during typing to avoid re-rendering)
1410
- if (isSingle) {
1411
- this.selectedMinute = value;
1412
- } else if (isStart) {
1413
- this.startMinute = value;
1414
- } else {
1415
- this.endMinute = value;
1416
- }
1417
-
1418
- // Don't update dates or emit during typing - wait for blur or apply
1419
- }
1420
-
1421
- onMinuteBlur(event: any, isStart = true, isSingle = false): void {
1422
- const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1423
- const inputValue = event.target.value;
1424
-
1425
- if (inputValue === '' || inputValue === null || inputValue === undefined) {
1426
- // If empty, set to current value
1427
- const currentValue = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
1428
- event.target.value = currentValue.toString().padStart(2, '0');
1429
- delete this.minuteInputValues[key];
1430
- return;
1431
- }
1432
-
1433
- const digitsOnly = inputValue.replace(/\D/g, '');
1434
- let value = parseInt(digitsOnly) || 0;
1435
- if (value < 0) value = 0;
1436
- if (value > 59) value = 59;
1437
-
1438
- // Format to 2 digits on blur (01-09 becomes 01-09, 10-59 stays as is)
1439
- event.target.value = value.toString().padStart(2, '0');
1440
- delete this.minuteInputValues[key]; // Clear raw input, use formatted value
1441
-
1442
- // Update the value
1443
- if (isSingle) {
1444
- this.selectedMinute = value;
1445
- if (this.startDate) {
1446
- let h = this.selectedHour;
1447
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1448
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1449
- this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
1450
- this.emitSelection();
1451
- }
1452
- } else if (isStart) {
1453
- this.startMinute = value;
1454
- if (this.startDate) {
1455
- let h = this.startHour;
1456
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1457
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1458
- this.startDate.setHours(h, this.startMinute, this.startSecond);
1459
- this.emitSelection();
1460
- }
1461
- } else {
1462
- this.endMinute = value;
1463
- if (this.endDate) {
1464
- let h = this.endHour;
1465
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1466
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1467
- this.endDate.setHours(h, this.endMinute, this.endSecond);
1468
- this.emitSelection();
1469
- }
1470
- }
1471
- }
1472
-
1473
- // Get display value for hour (always 12-hour format)
1474
- getDisplayHour(hour: number): number {
1475
- if (hour === 0) return 12;
1476
- if (hour > 12) return hour - 12;
1477
- return hour;
1478
- }
1479
-
1480
- // Get display value for minute (formatted only when not actively typing)
1481
- getMinuteDisplayValue(isStart: boolean, isSingle: boolean): string {
1482
- const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
1483
- // If user is typing (has raw input), show that, otherwise show formatted value
1484
- if (this.minuteInputValues[key] !== undefined) {
1485
- return this.minuteInputValues[key];
1486
- }
1487
- // Otherwise return formatted value
1488
- const value = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
1489
- return value.toString().padStart(2, '0');
1490
- }
1491
-
1492
- // Helper method to apply time picker values to a date
1493
- applyTimeToDate(date: Date, isStart: boolean): void {
1494
- if (this.dualCalendar) {
1495
- if (isStart) {
1496
- let h = this.startHour;
1497
- if (this.startAMPM === 'PM' && h < 12) h += 12;
1498
- if (this.startAMPM === 'AM' && h === 12) h = 0;
1499
- date.setHours(h, this.startMinute, this.startSecond);
1500
- } else {
1501
- let h = this.endHour;
1502
- if (this.endAMPM === 'PM' && h < 12) h += 12;
1503
- if (this.endAMPM === 'AM' && h === 12) h = 0;
1504
- date.setHours(h, this.endMinute, this.endSecond);
1505
- }
1506
- } else {
1507
- let h = this.selectedHour;
1508
- if (this.selectedAMPM === 'PM' && h < 12) h += 12;
1509
- if (this.selectedAMPM === 'AM' && h === 12) h = 0;
1510
- date.setHours(h, this.selectedMinute, this.selectedSecond);
1511
- }
1512
- }
1513
-
1514
- // Select all text on focus for easy replacement
1515
- onTimeInputFocus(event: any): void {
1516
- event.target.select();
1517
- }
1518
-
1519
- // Format all minute inputs to 2 digits (called before Apply)
1520
- formatAllMinuteInputs(): void {
1521
- // Format minute inputs in the DOM - find all minute inputs and format single digits
1522
- const inputs = document.querySelectorAll('.time-input');
1523
- inputs.forEach((input: any) => {
1524
- const value = parseInt(input.value);
1525
- // If it's a valid minute value (0-59) and not already formatted (single digit or 2 digits without leading zero)
1526
- if (!isNaN(value) && value >= 0 && value <= 59) {
1527
- // Format if it's a single digit (1-9) or if it's 2 digits but the first is not 0
1528
- if (input.value.length === 1 || (input.value.length === 2 && !input.value.startsWith('0') && value < 10)) {
1529
- input.value = value.toString().padStart(2, '0');
1530
- }
1531
- }
1532
- });
1533
- }
1534
- }