@brickclay-org/ui 0.0.39 → 0.0.40
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/ASSETS_SETUP.md +59 -0
- package/ng-package.json +29 -0
- package/package.json +15 -26
- package/src/lib/assets/icons.ts +8 -0
- package/src/lib/badge/badge.html +24 -0
- package/src/lib/badge/badge.ts +42 -0
- package/src/lib/brickclay-lib.spec.ts +23 -0
- package/src/lib/brickclay-lib.ts +15 -0
- package/src/lib/button-group/button-group.html +12 -0
- package/src/lib/button-group/button-group.ts +73 -0
- package/src/lib/calender/calendar.module.ts +35 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.css +698 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.html +230 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.spec.ts +23 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.ts +1554 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.css +373 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.html +210 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.ts +361 -0
- package/src/lib/calender/components/time-picker/time-picker.component.css +174 -0
- package/src/lib/calender/components/time-picker/time-picker.component.html +60 -0
- package/src/lib/calender/components/time-picker/time-picker.component.ts +283 -0
- package/src/lib/calender/services/calendar-manager.service.ts +45 -0
- package/src/lib/checkbox/checkbox.html +42 -0
- package/src/lib/checkbox/checkbox.ts +67 -0
- package/src/lib/chips/chips.html +74 -0
- package/src/lib/chips/chips.ts +222 -0
- package/src/lib/grid/components/grid/grid.html +97 -0
- package/src/lib/grid/components/grid/grid.ts +139 -0
- package/src/lib/grid/models/grid.model.ts +20 -0
- package/src/lib/input/input.html +127 -0
- package/src/lib/input/input.ts +394 -0
- package/src/lib/pill/pill.html +24 -0
- package/src/lib/pill/pill.ts +39 -0
- package/src/lib/radio/radio.html +58 -0
- package/src/lib/radio/radio.ts +72 -0
- package/src/lib/select/select.html +111 -0
- package/src/lib/select/select.ts +401 -0
- package/src/lib/spinner/spinner.html +5 -0
- package/src/lib/spinner/spinner.ts +22 -0
- package/src/lib/tabs/tabs.html +28 -0
- package/src/lib/tabs/tabs.ts +48 -0
- package/src/lib/textarea/textarea.html +80 -0
- package/src/lib/textarea/textarea.ts +172 -0
- package/src/lib/toggle/toggle.html +24 -0
- package/src/lib/toggle/toggle.ts +62 -0
- package/src/lib/ui-button/ui-button.html +25 -0
- package/src/lib/ui-button/ui-button.ts +55 -0
- package/src/lib/ui-icon-button/ui-icon-button.html +7 -0
- package/src/lib/ui-icon-button/ui-icon-button.ts +38 -0
- package/src/public-api.ts +43 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +15 -0
- package/fesm2022/brickclay-org-ui.mjs +0 -4035
- package/fesm2022/brickclay-org-ui.mjs.map +0 -1
- package/index.d.ts +0 -857
|
@@ -0,0 +1,1554 @@
|
|
|
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 { BkTimePicker } 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: string | null;
|
|
16
|
+
endDate: string | null;
|
|
17
|
+
selectedDates?: string[]; // For multi-date selection
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'bk-custom-calendar',
|
|
22
|
+
standalone: true,
|
|
23
|
+
imports: [CommonModule, FormsModule, BkTimePicker],
|
|
24
|
+
templateUrl: './custom-calendar.component.html',
|
|
25
|
+
styleUrls: ['./custom-calendar.component.css']
|
|
26
|
+
})
|
|
27
|
+
export class BkCustomCalendar 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
|
+
@Input() startDate: Date | null = null;
|
|
81
|
+
@Input() 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
|
+
emitSelection() {
|
|
676
|
+
const selection: CalendarSelection = {
|
|
677
|
+
startDate: this.startDate ? this.formatDateToString(this.startDate) : null,
|
|
678
|
+
endDate: this.endDate ? this.formatDateToString(this.endDate) : null,
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
if (this.multiDateSelection && this.selectedDates.length > 0) {
|
|
682
|
+
selection.selectedDates = this.selectedDates.map(d => this.formatDateToString(d));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
this.selected.emit(selection);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
addDays(date: Date, days: number): Date {
|
|
689
|
+
const d = new Date(date);
|
|
690
|
+
d.setDate(d.getDate() + days);
|
|
691
|
+
return d;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
generateCalendar() {
|
|
695
|
+
this.calendar = this.buildCalendar(this.year, this.month);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
nextMonth() {
|
|
699
|
+
if (!this.dualCalendar) {
|
|
700
|
+
this.month++;
|
|
701
|
+
if (this.month > 11) { this.month = 0; this.year++; }
|
|
702
|
+
this.generateCalendar();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// For dual calendar, this should not be used - use nextLeftMonth or nextRightMonth instead
|
|
706
|
+
this.nextLeftMonth();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
prevMonth() {
|
|
710
|
+
if (!this.dualCalendar) {
|
|
711
|
+
this.month--;
|
|
712
|
+
if (this.month < 0) { this.month = 11; this.year--; }
|
|
713
|
+
this.generateCalendar();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// For dual calendar, this should not be used - use prevLeftMonth or prevRightMonth instead
|
|
717
|
+
this.prevLeftMonth();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Independent navigation for left calendar
|
|
721
|
+
nextLeftMonth() {
|
|
722
|
+
this.leftMonth++;
|
|
723
|
+
if (this.leftMonth > 11) { this.leftMonth = 0; this.leftYear++; }
|
|
724
|
+
this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
prevLeftMonth() {
|
|
728
|
+
this.leftMonth--;
|
|
729
|
+
if (this.leftMonth < 0) { this.leftMonth = 11; this.leftYear--; }
|
|
730
|
+
this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Independent navigation for right calendar
|
|
734
|
+
nextRightMonth() {
|
|
735
|
+
this.rightMonth++;
|
|
736
|
+
if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
|
|
737
|
+
this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
prevRightMonth() {
|
|
741
|
+
this.rightMonth--;
|
|
742
|
+
if (this.rightMonth < 0) { this.rightMonth = 11; this.rightYear--; }
|
|
743
|
+
this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
initializeDual() {
|
|
747
|
+
this.leftMonth = this.today.getMonth();
|
|
748
|
+
this.leftYear = this.today.getFullYear();
|
|
749
|
+
// Initialize right calendar to next month, but they can move independently
|
|
750
|
+
this.rightMonth = this.leftMonth + 1;
|
|
751
|
+
this.rightYear = this.leftYear;
|
|
752
|
+
if (this.rightMonth > 11) { this.rightMonth = 0; this.rightYear++; }
|
|
753
|
+
this.generateDualCalendars();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
generateDualCalendars() {
|
|
757
|
+
this.leftCalendar = this.buildCalendar(this.leftYear, this.leftMonth);
|
|
758
|
+
this.rightCalendar = this.buildCalendar(this.rightYear, this.rightMonth);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
buildCalendar(year: number, month: number): { day: number, currentMonth: boolean }[][] {
|
|
762
|
+
const firstDay = new Date(year, month, 1).getDay();
|
|
763
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
764
|
+
const prevMonthDays = new Date(year, month, 0).getDate();
|
|
765
|
+
|
|
766
|
+
const grid: { day: number, currentMonth: boolean }[][] = [];
|
|
767
|
+
let row: { day: number, currentMonth: boolean }[] = [];
|
|
768
|
+
|
|
769
|
+
// Adjust first day (0 = Sunday, 1 = Monday, etc.)
|
|
770
|
+
const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1; // Make Monday = 0
|
|
771
|
+
|
|
772
|
+
for (let i = adjustedFirstDay - 1; i >= 0; i--) {
|
|
773
|
+
row.push({ day: prevMonthDays - i, currentMonth: false });
|
|
774
|
+
}
|
|
775
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
776
|
+
row.push({ day: d, currentMonth: true });
|
|
777
|
+
if (row.length === 7) { grid.push(row); row = []; }
|
|
778
|
+
}
|
|
779
|
+
let nextMonthDay = 1;
|
|
780
|
+
while (row.length > 0 && row.length < 7) {
|
|
781
|
+
row.push({ day: nextMonthDay++, currentMonth: false });
|
|
782
|
+
}
|
|
783
|
+
if (row.length) grid.push(row);
|
|
784
|
+
|
|
785
|
+
// Ensure we always have 6 rows (42 cells total) for consistent layout
|
|
786
|
+
while (grid.length < 6) {
|
|
787
|
+
const newRow: { day: number, currentMonth: boolean }[] = [];
|
|
788
|
+
for (let i = 0; i < 7; i++) {
|
|
789
|
+
newRow.push({ day: nextMonthDay++, currentMonth: false });
|
|
790
|
+
}
|
|
791
|
+
grid.push(newRow);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return grid;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
isDateSelected(year: number, month: number, day: number): boolean {
|
|
798
|
+
if (this.disableHighlight) return false;
|
|
799
|
+
if (!day) return false;
|
|
800
|
+
|
|
801
|
+
// Multi-date selection
|
|
802
|
+
if (this.multiDateSelection) {
|
|
803
|
+
return this.isDateInMultiSelection(year, month, day);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const cellDate = new Date(year, month, day);
|
|
807
|
+
|
|
808
|
+
// Check if it's today (highlight today by default if no date selected)
|
|
809
|
+
const today = new Date();
|
|
810
|
+
const isToday = cellDate.getFullYear() === today.getFullYear() &&
|
|
811
|
+
cellDate.getMonth() === today.getMonth() &&
|
|
812
|
+
cellDate.getDate() === today.getDate();
|
|
813
|
+
|
|
814
|
+
// If no startDate is set and it's today, highlight it
|
|
815
|
+
if (!this.startDate && isToday) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (!this.startDate) return false;
|
|
820
|
+
|
|
821
|
+
// Check if date is disabled
|
|
822
|
+
if (this.minDate && cellDate < this.minDate) return false;
|
|
823
|
+
if (this.maxDate && cellDate > this.maxDate) return false;
|
|
824
|
+
|
|
825
|
+
const sameDay =
|
|
826
|
+
cellDate.getFullYear() === this.startDate.getFullYear() &&
|
|
827
|
+
cellDate.getMonth() === this.startDate.getMonth() &&
|
|
828
|
+
cellDate.getDate() === this.startDate.getDate();
|
|
829
|
+
|
|
830
|
+
if (this.singleDatePicker) return sameDay;
|
|
831
|
+
|
|
832
|
+
// For range selection: only highlight start and end dates (not in-between)
|
|
833
|
+
if (this.startDate && this.endDate) {
|
|
834
|
+
const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
|
|
835
|
+
const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
|
|
836
|
+
return cellDate.getTime() === start.getTime() || cellDate.getTime() === end.getTime();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// If only start date is selected and hovering, check if this is start or hovered end
|
|
840
|
+
if (this.startDate && !this.endDate && this.hoveredDate) {
|
|
841
|
+
const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
|
|
842
|
+
const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
|
|
843
|
+
// Show both start and hovered date as selected (circular black)
|
|
844
|
+
return cellDate.getTime() === start.getTime() || cellDate.getTime() === hovered.getTime();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return sameDay;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
isDateInRange(year: number, month: number, day: number): boolean {
|
|
851
|
+
if (this.disableHighlight || !day) return false;
|
|
852
|
+
if (this.singleDatePicker) return false;
|
|
853
|
+
if (this.multiDateSelection) return false;
|
|
854
|
+
|
|
855
|
+
const cellDate = new Date(year, month, day);
|
|
856
|
+
|
|
857
|
+
// If both start and end are selected, show gray background for dates in between
|
|
858
|
+
if (this.startDate && this.endDate) {
|
|
859
|
+
const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
|
|
860
|
+
const end = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate());
|
|
861
|
+
return cellDate > start && cellDate < end;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// If only start is selected and hovering, show preview range
|
|
865
|
+
if (this.startDate && !this.endDate && this.hoveredDate) {
|
|
866
|
+
const start = new Date(this.startDate.getFullYear(), this.startDate.getMonth(), this.startDate.getDate());
|
|
867
|
+
const hovered = new Date(this.hoveredDate.getFullYear(), this.hoveredDate.getMonth(), this.hoveredDate.getDate());
|
|
868
|
+
|
|
869
|
+
// Determine which is earlier - show gray background for dates between them
|
|
870
|
+
const minDate = hovered < start ? hovered : start;
|
|
871
|
+
const maxDate = hovered >= start ? hovered : start;
|
|
872
|
+
return cellDate > minDate && cellDate < maxDate;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
isDateDisabled(year: number, month: number, day: number): boolean {
|
|
879
|
+
if (!day) return false;
|
|
880
|
+
const cellDate = new Date(year, month, day);
|
|
881
|
+
if (this.minDate && cellDate < this.minDate) return true;
|
|
882
|
+
if (this.maxDate && cellDate > this.maxDate) return true;
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
isToday(year: number, month: number, day: number): boolean {
|
|
887
|
+
if (!day) return false;
|
|
888
|
+
const today = new Date();
|
|
889
|
+
const cellDate = new Date(year, month, day);
|
|
890
|
+
return cellDate.getFullYear() === today.getFullYear() &&
|
|
891
|
+
cellDate.getMonth() === today.getMonth() &&
|
|
892
|
+
cellDate.getDate() === today.getDate();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
getDisplayValue(): string {
|
|
896
|
+
if (this.multiDateSelection && this.selectedDates.length > 0) {
|
|
897
|
+
if (this.selectedDates.length === 1) {
|
|
898
|
+
return moment(this.selectedDates[0]).format(this.displayFormat);
|
|
899
|
+
}
|
|
900
|
+
return `${this.selectedDates.length} dates selected`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!this.startDate) return '';
|
|
904
|
+
|
|
905
|
+
// Prefer moment formatting for consistent display
|
|
906
|
+
let dateStr = moment(this.startDate).format(this.displayFormat);
|
|
907
|
+
|
|
908
|
+
if (this.enableTimepicker && !this.dualCalendar) {
|
|
909
|
+
const hr = this.startDate.getHours().toString().padStart(2, '0');
|
|
910
|
+
const min = this.startDate.getMinutes().toString().padStart(2, '0');
|
|
911
|
+
dateStr += ` ${hr}:${min}`;
|
|
912
|
+
if (this.enableSeconds) {
|
|
913
|
+
const sec = this.startDate.getSeconds().toString().padStart(2, '0');
|
|
914
|
+
dateStr += `:${sec}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (this.endDate && !this.singleDatePicker) {
|
|
919
|
+
let endStr = moment(this.endDate).format(this.displayFormat);
|
|
920
|
+
if (this.enableTimepicker) {
|
|
921
|
+
if (this.dualCalendar) {
|
|
922
|
+
const startHr = this.startDate.getHours().toString().padStart(2, '0');
|
|
923
|
+
const startMin = this.startDate.getMinutes().toString().padStart(2, '0');
|
|
924
|
+
dateStr += ` ${startHr}:${startMin}`;
|
|
925
|
+
if (this.enableSeconds) {
|
|
926
|
+
const startSec = this.startDate.getSeconds().toString().padStart(2, '0');
|
|
927
|
+
dateStr += `:${startSec}`;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const endHr = this.endDate.getHours().toString().padStart(2, '0');
|
|
931
|
+
const endMin = this.endDate.getMinutes().toString().padStart(2, '0');
|
|
932
|
+
endStr += ` ${endHr}:${endMin}`;
|
|
933
|
+
if (this.enableSeconds) {
|
|
934
|
+
const endSec = this.endDate.getSeconds().toString().padStart(2, '0');
|
|
935
|
+
endStr += `:${endSec}`;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return `${dateStr} - ${endStr}`;
|
|
939
|
+
}
|
|
940
|
+
return dateStr;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Time picker helpers
|
|
944
|
+
getTimeInputValue(isStart = true): string {
|
|
945
|
+
const h = isStart ? this.startHour : this.endHour;
|
|
946
|
+
const m = isStart ? this.startMinute : this.endMinute;
|
|
947
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
getSingleTimeInputValue(): string {
|
|
951
|
+
return `${this.selectedHour.toString().padStart(2, '0')}:${this.selectedMinute.toString().padStart(2, '0')}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// NEW: Helper to build display value for BkTimePicker (single calendar)
|
|
955
|
+
getSingleTimePickerDisplay(): string {
|
|
956
|
+
const hour = this.selectedHour || 12;
|
|
957
|
+
const minuteStr = this.selectedMinute.toString().padStart(2, '0');
|
|
958
|
+
const ampm = this.selectedAMPM || 'AM';
|
|
959
|
+
return `${hour}:${minuteStr} ${ampm}`;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// NEW: Helper to build display value for BkTimePicker (dual calendar)
|
|
963
|
+
getDualTimePickerDisplay(isStart = true): string {
|
|
964
|
+
const hour = isStart ? (this.startHour || 12) : (this.endHour || 12);
|
|
965
|
+
const minute = isStart ? this.startMinute : this.endMinute;
|
|
966
|
+
const ampm = isStart ? (this.startAMPM || 'AM') : (this.endAMPM || 'AM');
|
|
967
|
+
const minuteStr = minute.toString().padStart(2, '0');
|
|
968
|
+
return `${hour}:${minuteStr} ${ampm}`;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Coordination helpers for embedded BkTimePicker instances
|
|
972
|
+
onTimePickerOpened(pickerId: string) {
|
|
973
|
+
// Close previously open picker inside this calendar
|
|
974
|
+
if (this.openTimePickerId && this.openTimePickerId !== pickerId) {
|
|
975
|
+
if (!this.closePickerCounter[this.openTimePickerId]) {
|
|
976
|
+
this.closePickerCounter[this.openTimePickerId] = 0;
|
|
977
|
+
}
|
|
978
|
+
this.closePickerCounter[this.openTimePickerId]++;
|
|
979
|
+
}
|
|
980
|
+
this.openTimePickerId = pickerId;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
onTimePickerClosed(pickerId: string) {
|
|
984
|
+
if (this.openTimePickerId === pickerId) {
|
|
985
|
+
this.openTimePickerId = null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
shouldClosePicker(pickerId: string): number {
|
|
990
|
+
return this.closePickerCounter[pickerId] || 0;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// NEW: Parse "H:MM AM/PM" (or "HH:MM" 24h) from BkTimePicker
|
|
994
|
+
private parsePickerTimeString(timeStr: string): { hour12: number; minute: number; ampm: 'AM' | 'PM' } {
|
|
995
|
+
if (!timeStr) {
|
|
996
|
+
return { hour12: 12, minute: 0, ampm: 'AM' };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const parts = timeStr.trim().split(' ');
|
|
1000
|
+
const timePart = parts[0] || '12:00';
|
|
1001
|
+
let ampmPart = (parts[1] || '').toUpperCase();
|
|
1002
|
+
|
|
1003
|
+
const [hourStr, minuteStr] = timePart.split(':');
|
|
1004
|
+
let hour = parseInt(hourStr || '12', 10);
|
|
1005
|
+
const minute = parseInt(minuteStr || '0', 10);
|
|
1006
|
+
|
|
1007
|
+
if (ampmPart !== 'AM' && ampmPart !== 'PM') {
|
|
1008
|
+
// Interpret as 24-hour input and convert
|
|
1009
|
+
if (hour >= 12) {
|
|
1010
|
+
ampmPart = 'PM';
|
|
1011
|
+
if (hour > 12) hour -= 12;
|
|
1012
|
+
} else {
|
|
1013
|
+
ampmPart = 'AM';
|
|
1014
|
+
if (hour === 0) hour = 12;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Clamp to 1-12 range just in case
|
|
1019
|
+
if (hour < 1) hour = 1;
|
|
1020
|
+
if (hour > 12) hour = 12;
|
|
1021
|
+
|
|
1022
|
+
return { hour12: hour, minute, ampm: ampmPart as 'AM' | 'PM' };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// NEW: Handle BkTimePicker change for single calendar
|
|
1026
|
+
onSingleTimePickerChange(time: string) {
|
|
1027
|
+
const { hour12, minute, ampm } = this.parsePickerTimeString(time);
|
|
1028
|
+
|
|
1029
|
+
this.selectedHour = hour12;
|
|
1030
|
+
this.selectedMinute = minute;
|
|
1031
|
+
this.selectedAMPM = ampm;
|
|
1032
|
+
|
|
1033
|
+
if (this.startDate) {
|
|
1034
|
+
let h24 = hour12;
|
|
1035
|
+
if (ampm === 'PM' && h24 < 12) h24 += 12;
|
|
1036
|
+
if (ampm === 'AM' && h24 === 12) h24 = 0;
|
|
1037
|
+
this.startDate.setHours(h24, minute, this.selectedSecond);
|
|
1038
|
+
this.emitSelection();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// NEW: Handle BkTimePicker change for dual calendar
|
|
1043
|
+
onDualTimePickerChange(time: string, isStart = true) {
|
|
1044
|
+
const { hour12, minute, ampm } = this.parsePickerTimeString(time);
|
|
1045
|
+
|
|
1046
|
+
if (isStart) {
|
|
1047
|
+
this.startHour = hour12;
|
|
1048
|
+
this.startMinute = minute;
|
|
1049
|
+
this.startAMPM = ampm;
|
|
1050
|
+
|
|
1051
|
+
if (this.startDate) {
|
|
1052
|
+
let h24 = hour12;
|
|
1053
|
+
if (ampm === 'PM' && h24 < 12) h24 += 12;
|
|
1054
|
+
if (ampm === 'AM' && h24 === 12) h24 = 0;
|
|
1055
|
+
this.startDate.setHours(h24, minute, this.startSecond);
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
this.endHour = hour12;
|
|
1059
|
+
this.endMinute = minute;
|
|
1060
|
+
this.endAMPM = ampm;
|
|
1061
|
+
|
|
1062
|
+
if (this.endDate) {
|
|
1063
|
+
let h24 = hour12;
|
|
1064
|
+
if (ampm === 'PM' && h24 < 12) h24 += 12;
|
|
1065
|
+
if (ampm === 'AM' && h24 === 12) h24 = 0;
|
|
1066
|
+
this.endDate.setHours(h24, minute, this.endSecond);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
this.emitSelection();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
onTimeChange(event: any, isStart = true) {
|
|
1074
|
+
const [h, m] = event.target.value.split(':').map(Number);
|
|
1075
|
+
if (isStart) {
|
|
1076
|
+
this.startHour = h;
|
|
1077
|
+
this.startMinute = m;
|
|
1078
|
+
if (this.startDate) {
|
|
1079
|
+
this.startDate.setHours(h, m, this.startSecond);
|
|
1080
|
+
this.emitSelection();
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
this.endHour = h;
|
|
1084
|
+
this.endMinute = m;
|
|
1085
|
+
if (this.endDate) {
|
|
1086
|
+
this.endDate.setHours(h, m, this.endSecond);
|
|
1087
|
+
this.emitSelection();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
onSingleTimeChange(event: any) {
|
|
1093
|
+
const [h, m] = event.target.value.split(':').map(Number);
|
|
1094
|
+
this.selectedHour = h;
|
|
1095
|
+
this.selectedMinute = m;
|
|
1096
|
+
if (this.startDate) {
|
|
1097
|
+
this.startDate.setHours(h, m, this.selectedSecond);
|
|
1098
|
+
this.emitSelection();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Custom time picker controls
|
|
1103
|
+
incrementHour(isStart = true) {
|
|
1104
|
+
// 12-hour format: 1-12
|
|
1105
|
+
if (isStart) {
|
|
1106
|
+
this.startHour = this.startHour >= 12 ? 1 : this.startHour + 1;
|
|
1107
|
+
// Toggle AM/PM at 12
|
|
1108
|
+
if (this.startHour === 12) {
|
|
1109
|
+
this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
|
|
1110
|
+
}
|
|
1111
|
+
if (this.startDate) {
|
|
1112
|
+
let h = this.startHour;
|
|
1113
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1114
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1115
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
this.endHour = this.endHour >= 12 ? 1 : this.endHour + 1;
|
|
1119
|
+
// Toggle AM/PM at 12
|
|
1120
|
+
if (this.endHour === 12) {
|
|
1121
|
+
this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
|
|
1122
|
+
}
|
|
1123
|
+
if (this.endDate) {
|
|
1124
|
+
let h = this.endHour;
|
|
1125
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1126
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1127
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
this.emitSelection();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
decrementHour(isStart = true) {
|
|
1134
|
+
// 12-hour format: 1-12
|
|
1135
|
+
if (isStart) {
|
|
1136
|
+
this.startHour = this.startHour <= 1 ? 12 : this.startHour - 1;
|
|
1137
|
+
// Toggle AM/PM at 12
|
|
1138
|
+
if (this.startHour === 12) {
|
|
1139
|
+
this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
|
|
1140
|
+
}
|
|
1141
|
+
if (this.startDate) {
|
|
1142
|
+
let h = this.startHour;
|
|
1143
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1144
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1145
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
this.endHour = this.endHour <= 1 ? 12 : this.endHour - 1;
|
|
1149
|
+
// Toggle AM/PM at 12
|
|
1150
|
+
if (this.endHour === 12) {
|
|
1151
|
+
this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
|
|
1152
|
+
}
|
|
1153
|
+
if (this.endDate) {
|
|
1154
|
+
let h = this.endHour;
|
|
1155
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1156
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1157
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
this.emitSelection();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
incrementMinute(isStart = true) {
|
|
1164
|
+
if (isStart) {
|
|
1165
|
+
this.startMinute = (this.startMinute + 1) % 60;
|
|
1166
|
+
if (this.startDate) {
|
|
1167
|
+
let h = this.startHour;
|
|
1168
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1169
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1170
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
this.endMinute = (this.endMinute + 1) % 60;
|
|
1174
|
+
if (this.endDate) {
|
|
1175
|
+
let h = this.endHour;
|
|
1176
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1177
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1178
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
this.emitSelection();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
decrementMinute(isStart = true) {
|
|
1185
|
+
if (isStart) {
|
|
1186
|
+
this.startMinute = this.startMinute <= 0 ? 59 : this.startMinute - 1;
|
|
1187
|
+
if (this.startDate) {
|
|
1188
|
+
let h = this.startHour;
|
|
1189
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1190
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1191
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
this.endMinute = this.endMinute <= 0 ? 59 : this.endMinute - 1;
|
|
1195
|
+
if (this.endDate) {
|
|
1196
|
+
let h = this.endHour;
|
|
1197
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1198
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1199
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
this.emitSelection();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
toggleAMPM(isStart = true) {
|
|
1206
|
+
if (isStart) {
|
|
1207
|
+
this.startAMPM = this.startAMPM === 'AM' ? 'PM' : 'AM';
|
|
1208
|
+
if (this.startDate) {
|
|
1209
|
+
let h = this.startHour;
|
|
1210
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1211
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1212
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
this.endAMPM = this.endAMPM === 'AM' ? 'PM' : 'AM';
|
|
1216
|
+
if (this.endDate) {
|
|
1217
|
+
let h = this.endHour;
|
|
1218
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1219
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1220
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
this.emitSelection();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Single calendar time picker controls (12-hour format: 1-12)
|
|
1227
|
+
incrementSingleHour() {
|
|
1228
|
+
this.selectedHour = this.selectedHour >= 12 ? 1 : this.selectedHour + 1;
|
|
1229
|
+
// Toggle AM/PM at 12
|
|
1230
|
+
if (this.selectedHour === 12) {
|
|
1231
|
+
this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
|
|
1232
|
+
}
|
|
1233
|
+
if (this.startDate) {
|
|
1234
|
+
let h = this.selectedHour;
|
|
1235
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1236
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1237
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1238
|
+
this.emitSelection();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
decrementSingleHour() {
|
|
1243
|
+
this.selectedHour = this.selectedHour <= 1 ? 12 : this.selectedHour - 1;
|
|
1244
|
+
// Toggle AM/PM at 12
|
|
1245
|
+
if (this.selectedHour === 12) {
|
|
1246
|
+
this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
|
|
1247
|
+
}
|
|
1248
|
+
if (this.startDate) {
|
|
1249
|
+
let h = this.selectedHour;
|
|
1250
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1251
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1252
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1253
|
+
this.emitSelection();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
incrementSingleMinute() {
|
|
1258
|
+
this.selectedMinute = (this.selectedMinute + 1) % 60;
|
|
1259
|
+
if (this.startDate) {
|
|
1260
|
+
let h = this.selectedHour;
|
|
1261
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1262
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1263
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1264
|
+
this.emitSelection();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
decrementSingleMinute() {
|
|
1269
|
+
this.selectedMinute = this.selectedMinute <= 0 ? 59 : this.selectedMinute - 1;
|
|
1270
|
+
if (this.startDate) {
|
|
1271
|
+
let h = this.selectedHour;
|
|
1272
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1273
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1274
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1275
|
+
this.emitSelection();
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
toggleSingleAMPM() {
|
|
1280
|
+
this.selectedAMPM = this.selectedAMPM === 'AM' ? 'PM' : 'AM';
|
|
1281
|
+
if (this.startDate) {
|
|
1282
|
+
let h = this.selectedHour;
|
|
1283
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1284
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1285
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1286
|
+
this.emitSelection();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
getMonthName(month: number): string {
|
|
1291
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
1292
|
+
return months[month];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Input handlers for direct hour/minute input (12-hour format only)
|
|
1296
|
+
onHourInput(event: any, isStart = true, isSingle = false): void {
|
|
1297
|
+
const inputValue = event.target.value;
|
|
1298
|
+
|
|
1299
|
+
// Allow empty input while typing
|
|
1300
|
+
if (inputValue === '' || inputValue === null || inputValue === undefined) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
let value = parseInt(inputValue) || 0;
|
|
1305
|
+
|
|
1306
|
+
// Validate: 1-12 for 12-hour format
|
|
1307
|
+
if (value < 1) value = 1;
|
|
1308
|
+
if (value > 12) value = 12;
|
|
1309
|
+
|
|
1310
|
+
if (isSingle) {
|
|
1311
|
+
this.selectedHour = value;
|
|
1312
|
+
if (this.startDate) {
|
|
1313
|
+
let h = value;
|
|
1314
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1315
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1316
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1317
|
+
this.emitSelection();
|
|
1318
|
+
}
|
|
1319
|
+
} else if (isStart) {
|
|
1320
|
+
this.startHour = value;
|
|
1321
|
+
if (this.startDate) {
|
|
1322
|
+
let h = value;
|
|
1323
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1324
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1325
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1326
|
+
this.emitSelection();
|
|
1327
|
+
}
|
|
1328
|
+
} else {
|
|
1329
|
+
this.endHour = value;
|
|
1330
|
+
if (this.endDate) {
|
|
1331
|
+
let h = value;
|
|
1332
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1333
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1334
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1335
|
+
this.emitSelection();
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Don't format during input, only on blur
|
|
1340
|
+
event.target.value = value.toString();
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
onHourBlur(event: any, isStart = true, isSingle = false): void {
|
|
1344
|
+
const inputValue = event.target.value;
|
|
1345
|
+
if (inputValue === '' || inputValue === null || inputValue === undefined) {
|
|
1346
|
+
// If empty, set to current value
|
|
1347
|
+
const currentValue = isSingle ? this.selectedHour : (isStart ? this.startHour : this.endHour);
|
|
1348
|
+
event.target.value = currentValue.toString();
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
let value = parseInt(inputValue) || 0;
|
|
1353
|
+
if (value < 1) value = 1;
|
|
1354
|
+
if (value > 12) value = 12;
|
|
1355
|
+
|
|
1356
|
+
// Format to single digit (no padding for hours in 12-hour format)
|
|
1357
|
+
event.target.value = value.toString();
|
|
1358
|
+
|
|
1359
|
+
// Update the value
|
|
1360
|
+
if (isSingle) {
|
|
1361
|
+
this.selectedHour = value;
|
|
1362
|
+
if (this.startDate) {
|
|
1363
|
+
let h = value;
|
|
1364
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1365
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1366
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1367
|
+
this.emitSelection();
|
|
1368
|
+
}
|
|
1369
|
+
} else if (isStart) {
|
|
1370
|
+
this.startHour = value;
|
|
1371
|
+
if (this.startDate) {
|
|
1372
|
+
let h = value;
|
|
1373
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1374
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1375
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1376
|
+
this.emitSelection();
|
|
1377
|
+
}
|
|
1378
|
+
} else {
|
|
1379
|
+
this.endHour = value;
|
|
1380
|
+
if (this.endDate) {
|
|
1381
|
+
let h = value;
|
|
1382
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1383
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1384
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1385
|
+
this.emitSelection();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
onMinuteInput(event: any, isStart = true, isSingle = false): void {
|
|
1391
|
+
const inputValue = event.target.value;
|
|
1392
|
+
const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
|
|
1393
|
+
|
|
1394
|
+
// Remove any non-digit characters
|
|
1395
|
+
const digitsOnly = inputValue.replace(/\D/g, '');
|
|
1396
|
+
|
|
1397
|
+
// Store raw input value for display
|
|
1398
|
+
this.minuteInputValues[key] = digitsOnly;
|
|
1399
|
+
|
|
1400
|
+
// Allow empty input while typing
|
|
1401
|
+
if (digitsOnly === '' || digitsOnly === null || digitsOnly === undefined) {
|
|
1402
|
+
return; // Don't modify the input, let user clear it
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Allow typing up to 2 digits without formatting
|
|
1406
|
+
let value = parseInt(digitsOnly) || 0;
|
|
1407
|
+
|
|
1408
|
+
// If user types more than 2 digits, take only first 2
|
|
1409
|
+
if (digitsOnly.length > 2) {
|
|
1410
|
+
value = parseInt(digitsOnly.substring(0, 2));
|
|
1411
|
+
this.minuteInputValues[key] = digitsOnly.substring(0, 2);
|
|
1412
|
+
event.target.value = digitsOnly.substring(0, 2);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// If value exceeds 59, clamp it to 59
|
|
1416
|
+
if (value > 59) {
|
|
1417
|
+
value = 59;
|
|
1418
|
+
this.minuteInputValues[key] = '59';
|
|
1419
|
+
event.target.value = '59';
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Update the internal value silently (don't emit during typing to avoid re-rendering)
|
|
1423
|
+
if (isSingle) {
|
|
1424
|
+
this.selectedMinute = value;
|
|
1425
|
+
} else if (isStart) {
|
|
1426
|
+
this.startMinute = value;
|
|
1427
|
+
} else {
|
|
1428
|
+
this.endMinute = value;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Don't update dates or emit during typing - wait for blur or apply
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
onMinuteBlur(event: any, isStart = true, isSingle = false): void {
|
|
1435
|
+
const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
|
|
1436
|
+
const inputValue = event.target.value;
|
|
1437
|
+
|
|
1438
|
+
if (inputValue === '' || inputValue === null || inputValue === undefined) {
|
|
1439
|
+
// If empty, set to current value
|
|
1440
|
+
const currentValue = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
|
|
1441
|
+
event.target.value = currentValue.toString().padStart(2, '0');
|
|
1442
|
+
delete this.minuteInputValues[key];
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const digitsOnly = inputValue.replace(/\D/g, '');
|
|
1447
|
+
let value = parseInt(digitsOnly) || 0;
|
|
1448
|
+
if (value < 0) value = 0;
|
|
1449
|
+
if (value > 59) value = 59;
|
|
1450
|
+
|
|
1451
|
+
// Format to 2 digits on blur (01-09 becomes 01-09, 10-59 stays as is)
|
|
1452
|
+
event.target.value = value.toString().padStart(2, '0');
|
|
1453
|
+
delete this.minuteInputValues[key]; // Clear raw input, use formatted value
|
|
1454
|
+
|
|
1455
|
+
// Update the value
|
|
1456
|
+
if (isSingle) {
|
|
1457
|
+
this.selectedMinute = value;
|
|
1458
|
+
if (this.startDate) {
|
|
1459
|
+
let h = this.selectedHour;
|
|
1460
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1461
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1462
|
+
this.startDate.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1463
|
+
this.emitSelection();
|
|
1464
|
+
}
|
|
1465
|
+
} else if (isStart) {
|
|
1466
|
+
this.startMinute = value;
|
|
1467
|
+
if (this.startDate) {
|
|
1468
|
+
let h = this.startHour;
|
|
1469
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1470
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1471
|
+
this.startDate.setHours(h, this.startMinute, this.startSecond);
|
|
1472
|
+
this.emitSelection();
|
|
1473
|
+
}
|
|
1474
|
+
} else {
|
|
1475
|
+
this.endMinute = value;
|
|
1476
|
+
if (this.endDate) {
|
|
1477
|
+
let h = this.endHour;
|
|
1478
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1479
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1480
|
+
this.endDate.setHours(h, this.endMinute, this.endSecond);
|
|
1481
|
+
this.emitSelection();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Get display value for hour (always 12-hour format)
|
|
1487
|
+
getDisplayHour(hour: number): number {
|
|
1488
|
+
if (hour === 0) return 12;
|
|
1489
|
+
if (hour > 12) return hour - 12;
|
|
1490
|
+
return hour;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Get display value for minute (formatted only when not actively typing)
|
|
1494
|
+
getMinuteDisplayValue(isStart: boolean, isSingle: boolean): string {
|
|
1495
|
+
const key = `${isStart ? 'start' : 'end'}_${isSingle ? 'single' : 'dual'}`;
|
|
1496
|
+
// If user is typing (has raw input), show that, otherwise show formatted value
|
|
1497
|
+
if (this.minuteInputValues[key] !== undefined) {
|
|
1498
|
+
return this.minuteInputValues[key];
|
|
1499
|
+
}
|
|
1500
|
+
// Otherwise return formatted value
|
|
1501
|
+
const value = isSingle ? this.selectedMinute : (isStart ? this.startMinute : this.endMinute);
|
|
1502
|
+
return value.toString().padStart(2, '0');
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Helper method to apply time picker values to a date
|
|
1506
|
+
applyTimeToDate(date: Date, isStart: boolean): void {
|
|
1507
|
+
if (this.dualCalendar) {
|
|
1508
|
+
if (isStart) {
|
|
1509
|
+
let h = this.startHour;
|
|
1510
|
+
if (this.startAMPM === 'PM' && h < 12) h += 12;
|
|
1511
|
+
if (this.startAMPM === 'AM' && h === 12) h = 0;
|
|
1512
|
+
date.setHours(h, this.startMinute, this.startSecond);
|
|
1513
|
+
} else {
|
|
1514
|
+
let h = this.endHour;
|
|
1515
|
+
if (this.endAMPM === 'PM' && h < 12) h += 12;
|
|
1516
|
+
if (this.endAMPM === 'AM' && h === 12) h = 0;
|
|
1517
|
+
date.setHours(h, this.endMinute, this.endSecond);
|
|
1518
|
+
}
|
|
1519
|
+
} else {
|
|
1520
|
+
let h = this.selectedHour;
|
|
1521
|
+
if (this.selectedAMPM === 'PM' && h < 12) h += 12;
|
|
1522
|
+
if (this.selectedAMPM === 'AM' && h === 12) h = 0;
|
|
1523
|
+
date.setHours(h, this.selectedMinute, this.selectedSecond);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Select all text on focus for easy replacement
|
|
1528
|
+
onTimeInputFocus(event: any): void {
|
|
1529
|
+
event.target.select();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Format all minute inputs to 2 digits (called before Apply)
|
|
1533
|
+
formatAllMinuteInputs(): void {
|
|
1534
|
+
// Format minute inputs in the DOM - find all minute inputs and format single digits
|
|
1535
|
+
const inputs = document.querySelectorAll('.time-input');
|
|
1536
|
+
inputs.forEach((input: any) => {
|
|
1537
|
+
const value = parseInt(input.value);
|
|
1538
|
+
// If it's a valid minute value (0-59) and not already formatted (single digit or 2 digits without leading zero)
|
|
1539
|
+
if (!isNaN(value) && value >= 0 && value <= 59) {
|
|
1540
|
+
// Format if it's a single digit (1-9) or if it's 2 digits but the first is not 0
|
|
1541
|
+
if (input.value.length === 1 || (input.value.length === 2 && !input.value.startsWith('0') && value < 10)) {
|
|
1542
|
+
input.value = value.toString().padStart(2, '0');
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
formatDateToString(date: Date): string {
|
|
1549
|
+
const yyyy = date.getFullYear();
|
|
1550
|
+
const mm = (date.getMonth() + 1).toString().padStart(2, '0'); // month is 0-based
|
|
1551
|
+
const dd = date.getDate().toString().padStart(2, '0');
|
|
1552
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
1553
|
+
}
|
|
1554
|
+
}
|