nexheal-lib 0.0.2 → 0.0.3

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 (62) hide show
  1. package/.editorconfig +17 -0
  2. package/.vscode/extensions.json +4 -0
  3. package/.vscode/launch.json +20 -0
  4. package/.vscode/tasks.json +42 -0
  5. package/README.md +15 -19
  6. package/angular.json +36 -0
  7. package/package.json +47 -23
  8. package/projects/nexheal-lib/README.md +63 -0
  9. package/projects/nexheal-lib/ng-package.json +9 -0
  10. package/projects/nexheal-lib/package.json +12 -0
  11. package/projects/nexheal-lib/src/directives/clickoutside.directive.ts +34 -0
  12. package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.html +52 -0
  13. package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.scss +22 -0
  14. package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.spec.ts +22 -0
  15. package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.ts +367 -0
  16. package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.html +152 -0
  17. package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.scss +194 -0
  18. package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.spec.ts +22 -0
  19. package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.ts +759 -0
  20. package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.html +4 -0
  21. package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.spec.ts +22 -0
  22. package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.ts +94 -0
  23. package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.html +61 -0
  24. package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.scss +132 -0
  25. package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.spec.ts +22 -0
  26. package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.ts +202 -0
  27. package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.html +72 -0
  28. package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.scss +90 -0
  29. package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.spec.ts +22 -0
  30. package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.ts +482 -0
  31. package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.html +53 -0
  32. package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.scss +19 -0
  33. package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.spec.ts +22 -0
  34. package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.ts +375 -0
  35. package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.html +4 -0
  36. package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.scss +53 -0
  37. package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.spec.ts +22 -0
  38. package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.ts +93 -0
  39. package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.html +88 -0
  40. package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.scss +122 -0
  41. package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.spec.ts +22 -0
  42. package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.ts +314 -0
  43. package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.html +19 -0
  44. package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.scss +15 -0
  45. package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.spec.ts +22 -0
  46. package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.ts +83 -0
  47. package/projects/nexheal-lib/src/public-api.ts +13 -0
  48. package/projects/nexheal-lib/src/styles/nexheal.scss +1 -0
  49. package/projects/nexheal-lib/tsconfig.lib.json +18 -0
  50. package/projects/nexheal-lib/tsconfig.lib.prod.json +11 -0
  51. package/projects/nexheal-lib/tsconfig.spec.json +14 -0
  52. package/tsconfig.json +39 -0
  53. package/fesm2022/nexheal-lib.mjs +0 -2837
  54. package/fesm2022/nexheal-lib.mjs.map +0 -1
  55. package/index.d.ts +0 -498
  56. package/src/styles/fonts/icomoon.eot +0 -0
  57. package/src/styles/fonts/icomoon.svg +0 -46
  58. package/src/styles/fonts/icomoon.ttf +0 -0
  59. package/src/styles/fonts/icomoon.woff +0 -0
  60. package/src/styles/icon.css +0 -133
  61. package/src/styles/nexheal.scss +0 -2
  62. /package/{src → projects/nexheal-lib/src}/styles/_formcontrols.scss +0 -0
@@ -0,0 +1,759 @@
1
+ import { CommonModule, DatePipe } from "@angular/common";
2
+ import { Component, OnInit, forwardRef, Input, HostListener, ViewChild, ElementRef, EventEmitter, Output, OnDestroy } from "@angular/core";
3
+ import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from "@angular/forms";
4
+ import { createPopper, Instance as PopperInstance } from "@popperjs/core";
5
+ type ViewMode = "day" | "month" | "yearRange";
6
+
7
+ @Component({
8
+ selector: "calendar-control",
9
+ standalone: true,
10
+ imports: [
11
+ CommonModule,
12
+ ReactiveFormsModule,
13
+ FormsModule
14
+ ],
15
+ templateUrl: "./calendar-control.component.html",
16
+ styleUrl: "./calendar-control.component.scss",
17
+ providers: [
18
+ DatePipe,
19
+ {
20
+ provide: NG_VALUE_ACCESSOR,
21
+ useExisting: forwardRef(() => CalendarControl),
22
+ multi: true,
23
+ },
24
+ ],
25
+ })
26
+ export class CalendarControl implements ControlValueAccessor, OnInit, OnDestroy {
27
+
28
+ @Input() title!: string;
29
+ @Input() required: boolean = false;
30
+ @Input() customClass: string | object = "";
31
+ @Input() clearVal: boolean = true;
32
+ @Input() deFocus: boolean = true;
33
+ @Input() error: boolean = false;
34
+ @Input() errorMessage: string = "";
35
+ @Input() inputLoader: boolean = false;
36
+ @Input() hourFormat: string = "24";
37
+ @Input() selectionMode: string = "single";
38
+ @Input() timeOnly = false;
39
+ @Input() dateFormat: string = "dd/MM/yyyy";
40
+ @Input() placeholder: string = "dd-mm-yyyy";
41
+ @Input() disabled = false;
42
+ @Input() readonly = false;
43
+ @Input() submitted = false;
44
+ @Input() inputPlaceholder: boolean = false;
45
+ @Input("close-val") closeVal: boolean = false;
46
+ @Input() showTime = false;
47
+ @Output() selectionCleared = new EventEmitter<void>();
48
+ @Output() blurEvent = new EventEmitter<void>();
49
+ @Output() dateSelected = new EventEmitter<Date | null>();
50
+
51
+ private _minDate?: string;
52
+ private _maxDate?: string;
53
+ @Input() get minDate(): string | undefined {
54
+ return this._minDate;
55
+ }
56
+ set minDate(value: string | undefined) {
57
+ this._minDate = value;
58
+ }
59
+ @Input() get maxDate(): string | undefined {
60
+ return this._maxDate;
61
+ }
62
+ set maxDate(value: string | undefined) {
63
+ this._maxDate = value;
64
+ }
65
+ get meridian(): 'AM' | 'PM' {
66
+ if (this.hourFormat !== '12') return 'AM';
67
+ return this.selectedHour >= 12 ? 'PM' : 'AM';
68
+ }
69
+ private static _currentlyOpen?: CalendarControl;
70
+ private onChangeFn: (val: string | null) => void = () => { };
71
+ private onTouchedFn: () => void = () => { };
72
+ private popperInstance: PopperInstance | null = null;
73
+
74
+ @ViewChild("root", { static: true }) rootElement!: ElementRef;
75
+ @ViewChild("inputEl", { static: true }) inputEl!: ElementRef<HTMLElement>;
76
+ @ViewChild("datePicker", { static: false }) datePickerEl!: ElementRef<HTMLElement>;
77
+
78
+ isOpen: boolean = false;
79
+ focus: boolean = false;
80
+ displayMonth: number;
81
+ displayYear: number;
82
+ selectedDate: Date | null = null;
83
+ today = new Date();
84
+ todayYear = this.today.getFullYear();
85
+ todayMonth = this.today.getMonth();
86
+ todayDate = this.today.getDate();
87
+ selectedHour = this.today.getHours();
88
+ selectedMinute = this.today.getMinutes();
89
+ dayClassMap: {
90
+ [day: number]: { disabled: boolean; selected: boolean; today: boolean };
91
+ } = {};
92
+
93
+ inputVal: string | null = null; // Replace with the appropriate type and default value
94
+ yearRangeSize = 15; // The year range view shows a chunk of years, for example 15 years at a time.
95
+ currentView: ViewMode = "day";
96
+ inputControl: FormControl = new FormControl({ value: "", disabled: false });
97
+
98
+ constructor(
99
+ public datePipe: DatePipe
100
+ ) {
101
+ const today = new Date();
102
+ this.displayMonth = today.getMonth();
103
+ this.displayYear = today.getFullYear();
104
+ }
105
+
106
+ ngOnInit(): void {
107
+ const now = new Date();
108
+ this.displayYear = now.getFullYear();
109
+ this.displayMonth = now.getMonth();
110
+ }
111
+
112
+ ngAfterViewChecked() {
113
+ if (this.isOpen) {
114
+ this.initializePopper();
115
+ }
116
+ }
117
+
118
+ ngAfterViewInit(): void {
119
+ if (this.isOpen) {
120
+ this.initializePopper();
121
+ }
122
+ }
123
+
124
+ ngOnDestroy(): void {
125
+ this.destroyPopper();
126
+ }
127
+
128
+ writeValue(value: any): void {
129
+ // 1) Clear out if no value
130
+ if (!value) {
131
+ this.selectedDate = null;
132
+ this.inputControl.setValue("", { emitEvent: false });
133
+ return;
134
+ }
135
+ if (this.timeOnly) {
136
+ let hours: number, mins: number;
137
+
138
+ if (typeof value === "string") {
139
+ // “HH:mm” or “hh:mm AM/PM”
140
+ const m24 = value.match(/^(\d{1,2}):(\d{2})$/);
141
+ const m12 = value.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
142
+ if (m24) {
143
+ hours = +m24[1];
144
+ mins = +m24[2];
145
+ } else if (m12) {
146
+ let h = +m12[1];
147
+ if (/PM/i.test(m12[3]) && h < 12) h += 12;
148
+ if (/AM/i.test(m12[3]) && h === 12) h = 0;
149
+ hours = h;
150
+ mins = +m12[2];
151
+ } else {
152
+ // fallback parse
153
+ const dt = new Date(value);
154
+ hours = dt.getHours();
155
+ mins = dt.getMinutes();
156
+ }
157
+ } else if (value instanceof Date) {
158
+ hours = value.getHours();
159
+ mins = value.getMinutes();
160
+ } else {
161
+ // maybe a timestamp or something else
162
+ const dt = new Date(value);
163
+ hours = dt.getHours();
164
+ mins = dt.getMinutes();
165
+ }
166
+
167
+ // 3) Apply
168
+ this.selectedHour = hours;
169
+ this.selectedMinute = mins;
170
+ // 4) Write back into the formControl & notify forms
171
+ const out = this.formatOutputTimeOnly();
172
+ this.inputControl.setValue(out, { emitEvent: false });
173
+ this.onChangeFn(out);
174
+ this.onTouchedFn();
175
+
176
+ return;
177
+ }
178
+ // 2) Parse the incoming string (dd/MM/yyyy or dd/MM/yyyy HH:mm)
179
+ const parsed = this.parseDate(value);
180
+ this.selectedDate = parsed;
181
+ if (this.showTime) {
182
+ const raw = value as string;
183
+
184
+ // split into date vs time
185
+ let datePart = raw,
186
+ timePart: string | null = null;
187
+ const parts = raw.match(/^(.+?)\s+(.+)$/);
188
+ if (parts) {
189
+ datePart = parts[1];
190
+ timePart = parts[2];
191
+ }
192
+
193
+ // parse the date
194
+ const parsedDate = this.parseDate(datePart);
195
+ if (!parsedDate) {
196
+ return this.clearDate(new Event("manual"));
197
+ }
198
+
199
+ // parse the time portion (reuse your timeOnly logic)
200
+ if (timePart) {
201
+ let h: number, m: number;
202
+ const m24 = timePart.match(/^(\d{1,2}):(\d{2})$/);
203
+ const m12 = timePart.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
204
+
205
+ if (m24) {
206
+ h = +m24[1];
207
+ m = +m24[2];
208
+ } else if (m12) {
209
+ h = +m12[1];
210
+ m = +m12[2];
211
+ if (/PM/i.test(m12[3]) && h < 12) h += 12;
212
+ if (/AM/i.test(m12[3]) && h === 12) h = 0;
213
+ } else {
214
+ const dt = new Date(`1970-01-01T${timePart}`);
215
+ h = dt.getHours();
216
+ m = dt.getMinutes();
217
+ }
218
+
219
+ parsedDate.setHours(h, m);
220
+ this.selectedHour = h;
221
+ this.selectedMinute = m;
222
+
223
+ }
224
+
225
+ // update calendar view & control value
226
+ this.selectedDate = parsedDate;
227
+ this.displayMonth = parsedDate.getMonth();
228
+ this.displayYear = parsedDate.getFullYear();
229
+ const out = this.formatOutput(parsedDate);
230
+
231
+ this.inputControl.setValue(out, { emitEvent: false });
232
+ this.onChangeFn(out);
233
+ this.onTouchedFn();
234
+ return;
235
+ }
236
+ if (parsed) {
237
+ // 3) Always update the calendar view
238
+ this.displayMonth = parsed.getMonth();
239
+ this.displayYear = parsed.getFullYear();
240
+
241
+ // 4) ONLY if showTime is on, pull hours/minutes
242
+ if (this.showTime) {
243
+ this.selectedHour = parsed.getHours();
244
+ this.selectedMinute = parsed.getMinutes();
245
+
246
+ }
247
+
248
+ // 5) Finally, set the input text via your formatter
249
+ this.inputControl.setValue(this.formatOutput(parsed), {
250
+ emitEvent: false,
251
+ });
252
+ }
253
+ }
254
+
255
+ registerOnChange(fn: (value: string | null) => void): void {
256
+ this.onChangeFn = fn;
257
+ }
258
+ registerOnTouched(fn: () => void): void {
259
+ this.onTouchedFn = fn;
260
+ }
261
+ setData(config: any) {
262
+ if (config.hasOwnProperty("placeholder")) {
263
+ this.placeholder = config.placeholder;
264
+ }
265
+ }
266
+
267
+ // events
268
+ onBlur() {
269
+ this.onTouchedFn();
270
+ const raw = this.inputControl.value as string;
271
+ if (this.showTime && !this.timeOnly) {
272
+ // split off the date vs. time
273
+ const match = raw.match(/^(.+?)\s+(.+)$/);
274
+ const datePart = match ? match[1] : raw;
275
+ const timePart = match ? match[2] : "";
276
+ // parse the date
277
+ const pd = this.parseDate(datePart);
278
+ if (!pd) return this.clearDate(new Event("manual"));
279
+ // parse the time portion (reuse your timeOnly logic)
280
+ let h: number, m: number;
281
+ const m24 = timePart.match(/^(\d{1,2}):(\d{2})$/);
282
+ const m12 = timePart.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
283
+ if (m24) {
284
+ h = +m24[1];
285
+ m = +m24[2];
286
+ } else if (m12) {
287
+ h = +m12[1];
288
+ m = +m12[2];
289
+ if (/PM/i.test(m12[3]) && h < 12) h += 12;
290
+ if (/AM/i.test(m12[3]) && h === 12) h = 0;
291
+ } else {
292
+ const tmp = new Date(`1970-01-01T${timePart}`);
293
+ h = tmp.getHours();
294
+ m = tmp.getMinutes();
295
+ }
296
+ // apply
297
+ pd.setHours(h, m);
298
+ this.selectedDate = pd;
299
+ this.selectedHour = h;
300
+ this.selectedMinute = m;
301
+
302
+ // update calendar view
303
+ this.displayMonth = pd.getMonth();
304
+ this.displayYear = pd.getFullYear();
305
+ // write back into the input & notify
306
+ const out = this.formatOutput(pd);
307
+ this.inputControl.setValue(out, { emitEvent: false });
308
+ this.onChangeFn(out);
309
+ this.dateSelected.emit(pd);
310
+ this.blurEvent.emit();
311
+ return;
312
+ }
313
+ if (this.timeOnly) {
314
+ // try 24-hour HH:mm
315
+ let m = raw.match(/^(\d{1,2}):(\d{2})$/);
316
+ // or 12-hour h:mm AM/PM
317
+ const mAmPm = !m && raw.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
318
+
319
+ if (m) {
320
+ // 24h
321
+ const h = +m[1],
322
+ min = +m[2];
323
+ if (h < 24 && min < 60) {
324
+ this.selectedHour = h;
325
+ this.selectedMinute = min;
326
+
327
+ } else {
328
+ return this.clearDate(new Event("manual"));
329
+ }
330
+ } else if (mAmPm) {
331
+ // 12h
332
+ let h = +mAmPm[1],
333
+ min = +mAmPm[2];
334
+ const ap = mAmPm[3].toUpperCase();
335
+ if (h >= 1 && h <= 12 && min < 60) {
336
+ if (ap === "PM" && h < 12) h += 12;
337
+ if (ap === "AM" && h === 12) h = 0;
338
+ this.selectedHour = h;
339
+ this.selectedMinute = min;
340
+
341
+ } else {
342
+ return this.clearDate(new Event("manual"));
343
+ }
344
+ } else {
345
+ // not a valid time string
346
+ return this.clearDate(new Event("manual"));
347
+ }
348
+ const out = this.formatOutputTimeOnly();
349
+ this.inputControl.setValue(out, { emitEvent: false });
350
+ this.onChangeFn(out);
351
+ this.dateSelected.emit(this.selectedDate);
352
+ this.blurEvent.emit();
353
+ return;
354
+ }
355
+ if (this.showTime) {
356
+ this.blurEvent.emit();
357
+ return;
358
+ }
359
+ const parsed = this.parseDate(raw);
360
+
361
+ if (!parsed) {
362
+ this.clearDate(new Event("manual"));
363
+ } else {
364
+ this.selectedDate = parsed;
365
+ this.displayMonth = parsed.getMonth();
366
+ this.displayYear = parsed.getFullYear();
367
+ this.selectedHour = parsed.getHours();
368
+ this.selectedMinute = parsed.getMinutes();
369
+
370
+ const formatted = this.formatOutput(parsed);
371
+ this.inputControl.setValue(formatted, { emitEvent: false });
372
+ this.onChangeFn(formatted);
373
+ }
374
+ this.blurEvent.emit();
375
+ }
376
+ onFocus() {
377
+ this.focus = true;
378
+ }
379
+ setDisabledState(isDisabled: boolean) {
380
+ this.disabled = isDisabled;
381
+ }
382
+ onInputKeydown(event: KeyboardEvent) {
383
+ switch (event.key) {
384
+ case "ArrowDown":
385
+ event.preventDefault();
386
+ this.toggleCalendar();
387
+ break;
388
+ case "Escape":
389
+ this.isOpen = false;
390
+ break;
391
+ case " ":
392
+ case "Spacebar":
393
+ event.preventDefault();
394
+ this.toggleCalendar();
395
+ break;
396
+ case "Tab":
397
+ this.isOpen = false;
398
+ break;
399
+ }
400
+ }
401
+
402
+ // day
403
+ selectDay(day: number | null) {
404
+ if (!day) return;
405
+ const picked = this.showTime
406
+ ? this.buildDateWithTime(day)
407
+ : new Date(this.displayYear, this.displayMonth, day);
408
+
409
+ if (this.isDateDisabled(picked)) return;
410
+
411
+ this.selectedDate = picked;
412
+
413
+ const out = this.formatOutput(picked);
414
+
415
+ this.inputControl.setValue(out, { emitEvent: false });
416
+
417
+ this.onChangeFn(out);
418
+ this.onTouchedFn();
419
+ this.dateSelected.emit(picked);
420
+
421
+ this.isOpen = false;
422
+ }
423
+ get daysInMonth(): (number | null)[] {
424
+ const firstDay = new Date(this.displayYear, this.displayMonth, 1).getDay();
425
+ const totalDays = new Date(
426
+ this.displayYear,
427
+ this.displayMonth + 1,
428
+ 0
429
+ ).getDate();
430
+ const offset = firstDay;
431
+
432
+ const daysArray: (number | null)[] = Array(offset).fill(null);
433
+ this.dayClassMap = {}; // Reset
434
+
435
+ for (let i = 1; i <= totalDays; i++) {
436
+ const date = new Date(this.displayYear, this.displayMonth, i);
437
+
438
+ this.dayClassMap[i] = {
439
+ disabled: this.isDateDisabled(date),
440
+ selected:
441
+ this.selectedDate?.getFullYear() === this.displayYear &&
442
+ this.selectedDate?.getMonth() === this.displayMonth &&
443
+ this.selectedDate?.getDate() === i,
444
+ today:
445
+ this.todayYear === this.displayYear &&
446
+ this.todayMonth === this.displayMonth &&
447
+ this.todayDate === i,
448
+ };
449
+
450
+ daysArray.push(i);
451
+ }
452
+
453
+ return daysArray;
454
+ }
455
+ parseDate(val: any): Date | null {
456
+ if (typeof val === "string") {
457
+ const m = val.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
458
+ if (m) {
459
+ const d = +m[1],
460
+ mo = +m[2] - 1,
461
+ y = +m[3];
462
+ return new Date(y, mo, d);
463
+ }
464
+ }
465
+ // fallback
466
+ const date = new Date(val);
467
+ return isNaN(date.getTime()) ? null : date;
468
+ }
469
+ formatOutput(date: Date): string {
470
+ const datePart = this.datePipe.transform(date, this.dateFormat)!;
471
+ if (this.showTime) {
472
+ const timeFormat = this.hourFormat === "12" ? "hh:mm a" : "HH:mm";
473
+ const timePart = this.datePipe.transform(date, timeFormat)!;
474
+ return `${datePart} ${timePart}`;
475
+ }
476
+ return datePart;
477
+ }
478
+ isSelectedDay(day: number | null): boolean {
479
+ if (!this.selectedDate || !day) return false;
480
+ return (
481
+ this.selectedDate.getFullYear() === this.displayYear &&
482
+ this.selectedDate.getMonth() === this.displayMonth &&
483
+ this.selectedDate.getDate() === day
484
+ );
485
+ }
486
+ isDateDisabled(d: Date): boolean {
487
+ if (this._minDate && d < new Date(this._minDate)) return true;
488
+ if (this._maxDate && d > new Date(this._maxDate)) return true;
489
+ return false;
490
+ }
491
+ isDayDisabled(day: number | null): boolean {
492
+ return (
493
+ day !== null &&
494
+ this.isDateDisabled(new Date(this.displayYear, this.displayMonth, day))
495
+ );
496
+ }
497
+
498
+ // month
499
+ goToMonthView() {
500
+ this.currentView = "month";
501
+ }
502
+ months = [
503
+ "Jan",
504
+ "Feb",
505
+ "Mar",
506
+ "Apr",
507
+ "May",
508
+ "Jun",
509
+ "Jul",
510
+ "Aug",
511
+ "Sep",
512
+ "Oct",
513
+ "Nov",
514
+ "Dec",
515
+ ];
516
+ selectMonth(index: number) {
517
+ this.displayMonth = index;
518
+ this.currentView = "day";
519
+ }
520
+ get displayMonthName(): string {
521
+ return this.months[this.displayMonth];
522
+ }
523
+ prevMonth() {
524
+ if (this.displayMonth === 0) {
525
+ this.displayMonth = 11;
526
+ this.displayYear--;
527
+ } else {
528
+ this.displayMonth--;
529
+ }
530
+ }
531
+ nextMonth() {
532
+ if (this.displayMonth === 11) {
533
+ this.displayMonth = 0;
534
+ this.displayYear++;
535
+ } else {
536
+ this.displayMonth++;
537
+ }
538
+ }
539
+
540
+ // year
541
+ goToYearRangeView() {
542
+ this.currentView = "yearRange";
543
+ }
544
+ get yearRange(): number[] {
545
+ const startYear = this.getStartYearForRange();
546
+ const years: number[] = [];
547
+ for (let i = 0; i < this.yearRangeSize; i++) {
548
+ years.push(startYear + i);
549
+ }
550
+ return years;
551
+ }
552
+ getStartYearForRange(): number {
553
+ const remainder = this.displayYear % this.yearRangeSize;
554
+ return this.displayYear - remainder;
555
+ }
556
+ selectYear(year: number) {
557
+ this.displayYear = year;
558
+ this.currentView = "month";
559
+ }
560
+ prevYearRange() {
561
+ this.displayYear -= this.yearRangeSize;
562
+ }
563
+ nextYearRange() {
564
+ this.displayYear += this.yearRangeSize;
565
+ }
566
+
567
+ // time picker
568
+ private formatOutputTimeOnly(): string {
569
+ // you could choose 12h vs 24h based on hourFormat
570
+ return this.datePipe.transform(
571
+ new Date(0, 0, 0, this.selectedHour, this.selectedMinute),
572
+ this.hourFormat === "12" ? "hh:mm a" : "HH:mm"
573
+ )!;
574
+ }
575
+ private patchDateWithTime() {
576
+ if (!this.selectedDate) {
577
+ const now = new Date();
578
+ this.selectedDate = new Date(
579
+ now.getFullYear(),
580
+ now.getMonth(),
581
+ now.getDate(),
582
+ this.selectedHour,
583
+ this.selectedMinute
584
+ );
585
+ } else {
586
+ this.selectedDate.setHours(this.selectedHour, this.selectedMinute);
587
+ }
588
+
589
+ const output = this.timeOnly
590
+ ? this.formatOutputTimeOnly()
591
+ : this.formatOutput(this.selectedDate);
592
+
593
+ this.inputControl.setValue(output, { emitEvent: false });
594
+ this.onChangeFn(output);
595
+ this.onTouchedFn();
596
+ this.dateSelected.emit(this.selectedDate);
597
+ }
598
+ setMeridian(value: "AM" | "PM") {
599
+ if (this.meridian === value) return;
600
+ if (value === "AM" && this.selectedHour >= 12) {
601
+ this.selectedHour -= 12;
602
+ } else if (value === "PM" && this.selectedHour < 12) {
603
+ this.selectedHour += 12;
604
+ }
605
+ this.patchDateWithTime();
606
+ }
607
+ private buildDateWithTime(day: number): Date {
608
+ return new Date(
609
+ this.displayYear,
610
+ this.displayMonth,
611
+ day,
612
+ this.selectedHour,
613
+ this.selectedMinute
614
+ );
615
+ }
616
+ incrementHour() {
617
+ this.selectedHour = (this.selectedHour + 1) % 24;
618
+ if (this.showTime || this.timeOnly) this.patchDateWithTime();
619
+ }
620
+ decrementHour() {
621
+ this.selectedHour = (this.selectedHour + 23) % 24;
622
+ if (this.showTime || this.timeOnly) this.patchDateWithTime();
623
+ }
624
+ incrementMinute() {
625
+ this.selectedMinute++;
626
+ if (this.selectedMinute >= 60) {
627
+ this.selectedMinute = 0;
628
+ this.selectedHour = (this.selectedHour + 1) % 24;
629
+ }
630
+ if (this.showTime || this.timeOnly) {
631
+ this.patchDateWithTime();
632
+ }
633
+ }
634
+ decrementMinute() {
635
+ this.selectedMinute--;
636
+ if (this.selectedMinute < 0) {
637
+ this.selectedMinute = 59;
638
+ // go back an hour, wrapping 0 → 23
639
+ this.selectedHour = (this.selectedHour + 23) % 24;
640
+ }
641
+ if (this.showTime || this.timeOnly) {
642
+ this.patchDateWithTime();
643
+ }
644
+ }
645
+
646
+ // popper
647
+ initializePopper() {
648
+ if (
649
+ !this.popperInstance &&
650
+ this.inputEl?.nativeElement &&
651
+ this.datePickerEl?.nativeElement
652
+ ) {
653
+ this.popperInstance = createPopper(
654
+ this.inputEl.nativeElement,
655
+ this.datePickerEl.nativeElement,
656
+ {
657
+ placement: "bottom-start",
658
+ modifiers: [
659
+ { name: "offset", options: { offset: [0, 1] } },
660
+ { name: "preventOverflow", options: { boundary: "viewport" } },
661
+ { name: "flip", options: { fallbackPlacements: ["top-start"] } },
662
+ ],
663
+ }
664
+ );
665
+ this.popperInstance.update();
666
+ }
667
+ }
668
+ destroyPopper() {
669
+ if (this.popperInstance) {
670
+ this.popperInstance.destroy();
671
+ this.popperInstance = null;
672
+ }
673
+ }
674
+
675
+ // toggle, open, close and clear
676
+ toggleCalendar(): void {
677
+ if (this.disabled || this.readonly) return;
678
+
679
+ // if another calendar is open, close it
680
+ if (
681
+ !this.isOpen &&
682
+ CalendarControl._currentlyOpen &&
683
+ CalendarControl._currentlyOpen !== this
684
+ ) {
685
+ CalendarControl._currentlyOpen.closeCalendar();
686
+ }
687
+
688
+ // flip open/close
689
+ this.isOpen = !this.isOpen;
690
+
691
+ if (this.isOpen) {
692
+ // mark this one as current
693
+ CalendarControl._currentlyOpen = this;
694
+
695
+ // reset the month/year view
696
+ this.currentView = "day";
697
+ if (this.selectedDate) {
698
+ this.displayMonth = this.selectedDate.getMonth();
699
+ this.displayYear = this.selectedDate.getFullYear();
700
+ } else {
701
+ const now = new Date();
702
+ this.displayMonth = now.getMonth();
703
+ this.displayYear = now.getFullYear();
704
+ }
705
+
706
+ // destroy any old popper
707
+ if (this.popperInstance) {
708
+ this.popperInstance.destroy();
709
+ this.popperInstance = null;
710
+ }
711
+ if (this.inputEl && this.datePickerEl) {
712
+ this.popperInstance = createPopper(
713
+ this.inputEl.nativeElement,
714
+ this.datePickerEl.nativeElement,
715
+ {
716
+ placement: "bottom-start",
717
+ modifiers: [
718
+ { name: "offset", options: { offset: [0, 1] } },
719
+ { name: "preventOverflow", options: { boundary: "viewport" } },
720
+ { name: "flip", options: { fallbackPlacements: ["top-start"] } },
721
+ ],
722
+ }
723
+ );
724
+ this.popperInstance.update();
725
+ }
726
+ } else {
727
+ this.destroyPopper();
728
+ }
729
+ }
730
+ openCalendar() {
731
+ if (this.disabled || this.readonly || this.isOpen) return;
732
+ this.toggleCalendar();
733
+ }
734
+ @HostListener("document:click", ["$event"])
735
+ clickOutside(event: Event) {
736
+ if (!this.rootElement.nativeElement.contains(event.target) && this.isOpen) {
737
+ this.isOpen = false;
738
+ }
739
+ }
740
+ public closeCalendar() {
741
+ this.destroyPopper();
742
+ this.isOpen = false;
743
+ if (CalendarControl._currentlyOpen === this) {
744
+ CalendarControl._currentlyOpen = undefined;
745
+ }
746
+ }
747
+ clearDate(event: Event) {
748
+ this.selectionCleared.emit();
749
+ event.stopPropagation();
750
+ this.selectedDate = null;
751
+ this.selectedHour = this.today.getHours();
752
+ this.selectedMinute = this.today.getMinutes();
753
+ this.inputControl.setValue("", { emitEvent: false });
754
+
755
+ this.onChangeFn(null);
756
+ this.onTouchedFn();
757
+ this.dateSelected.emit(null);
758
+ }
759
+ }