@mk-kit/ui 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/block-editor/README.md +254 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-button.mjs +81 -0
- package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-chip.mjs +122 -0
- package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-core.mjs +1576 -0
- package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-data.mjs +6055 -0
- package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
- package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
- package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
- package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
- package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
- package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-icon.mjs +470 -0
- package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-media.mjs +896 -0
- package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
- package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-table.mjs +1378 -0
- package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
- package/fesm2022/mk-kit-ui.mjs +32 -0
- package/fesm2022/mk-kit-ui.mjs.map +1 -0
- package/package.json +130 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +113 -0
- package/schematics/ng-add/schema.json +22 -0
- package/schematics/package.json +3 -0
- package/styles/mk-kit.css +750 -0
- package/types/mk-kit-ui-block-editor.d.ts +292 -0
- package/types/mk-kit-ui-button.d.ts +40 -0
- package/types/mk-kit-ui-checkbox.d.ts +62 -0
- package/types/mk-kit-ui-chip.d.ts +59 -0
- package/types/mk-kit-ui-context-menu.d.ts +57 -0
- package/types/mk-kit-ui-core.d.ts +1105 -0
- package/types/mk-kit-ui-data.d.ts +2580 -0
- package/types/mk-kit-ui-datetime.d.ts +1171 -0
- package/types/mk-kit-ui-directives.d.ts +807 -0
- package/types/mk-kit-ui-dnd.d.ts +423 -0
- package/types/mk-kit-ui-feedback.d.ts +1270 -0
- package/types/mk-kit-ui-forms.d.ts +3586 -0
- package/types/mk-kit-ui-icon.d.ts +108 -0
- package/types/mk-kit-ui-media.d.ts +549 -0
- package/types/mk-kit-ui-navigation.d.ts +1169 -0
- package/types/mk-kit-ui-rich-text.d.ts +187 -0
- package/types/mk-kit-ui-table.d.ts +739 -0
- package/types/mk-kit-ui.d.ts +17 -0
|
@@ -0,0 +1,3409 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, Injector, model, input, numberAttribute, booleanAttribute, output, signal, computed, effect, untracked, afterNextRender, forwardRef, ChangeDetectionStrategy, Component, viewChild, viewChildren } from '@angular/core';
|
|
3
|
+
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
4
|
+
import { MK_I18N, MkLiveAnnouncer, mkUniqueId, mkValidatorChange, MkAnchoredPanel } from '@mk-kit/ui/core';
|
|
5
|
+
import { MkFormField } from '@mk-kit/ui/forms';
|
|
6
|
+
import { DOCUMENT } from '@angular/common';
|
|
7
|
+
|
|
8
|
+
/** English full month names, January (index 0) → December (index 11). */
|
|
9
|
+
const MONTH_NAMES = [
|
|
10
|
+
'January',
|
|
11
|
+
'February',
|
|
12
|
+
'March',
|
|
13
|
+
'April',
|
|
14
|
+
'May',
|
|
15
|
+
'June',
|
|
16
|
+
'July',
|
|
17
|
+
'August',
|
|
18
|
+
'September',
|
|
19
|
+
'October',
|
|
20
|
+
'November',
|
|
21
|
+
'December',
|
|
22
|
+
];
|
|
23
|
+
/** English abbreviated month names (`MMM`). */
|
|
24
|
+
const MONTH_NAMES_SHORT = [
|
|
25
|
+
'Jan',
|
|
26
|
+
'Feb',
|
|
27
|
+
'Mar',
|
|
28
|
+
'Apr',
|
|
29
|
+
'May',
|
|
30
|
+
'Jun',
|
|
31
|
+
'Jul',
|
|
32
|
+
'Aug',
|
|
33
|
+
'Sep',
|
|
34
|
+
'Oct',
|
|
35
|
+
'Nov',
|
|
36
|
+
'Dec',
|
|
37
|
+
];
|
|
38
|
+
/** English full weekday names, Sunday (index 0) → Saturday (index 6). */
|
|
39
|
+
const WEEKDAY_NAMES = [
|
|
40
|
+
'Sunday',
|
|
41
|
+
'Monday',
|
|
42
|
+
'Tuesday',
|
|
43
|
+
'Wednesday',
|
|
44
|
+
'Thursday',
|
|
45
|
+
'Friday',
|
|
46
|
+
'Saturday',
|
|
47
|
+
];
|
|
48
|
+
/** English abbreviated weekday names (`ddd`). */
|
|
49
|
+
const WEEKDAY_NAMES_SHORT = [
|
|
50
|
+
'Sun',
|
|
51
|
+
'Mon',
|
|
52
|
+
'Tue',
|
|
53
|
+
'Wed',
|
|
54
|
+
'Thu',
|
|
55
|
+
'Fri',
|
|
56
|
+
'Sat',
|
|
57
|
+
];
|
|
58
|
+
/** English single-letter weekday names (narrow). */
|
|
59
|
+
const WEEKDAY_NAMES_NARROW = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
60
|
+
function pad2$1(n) {
|
|
61
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
62
|
+
}
|
|
63
|
+
/** True when both dates fall on the same calendar day (local time). */
|
|
64
|
+
function isSameDay(a, b) {
|
|
65
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
66
|
+
a.getMonth() === b.getMonth() &&
|
|
67
|
+
a.getDate() === b.getDate());
|
|
68
|
+
}
|
|
69
|
+
/** True when both dates fall in the same calendar month of the same year. */
|
|
70
|
+
function isSameMonth(a, b) {
|
|
71
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
72
|
+
}
|
|
73
|
+
/** Midnight (00:00:00.000) on the first day of `date`'s month. */
|
|
74
|
+
function startOfMonth(date) {
|
|
75
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
76
|
+
}
|
|
77
|
+
/** Midnight on the last day of `date`'s month. */
|
|
78
|
+
function endOfMonth(date) {
|
|
79
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
80
|
+
}
|
|
81
|
+
/** A new date `count` months after `date` (negative to subtract). Clamps the
|
|
82
|
+
* day of month so e.g. Jan 31 + 1 month = Feb 28/29, not Mar 3. */
|
|
83
|
+
function addMonths(date, count) {
|
|
84
|
+
const targetMonth = date.getMonth() + count;
|
|
85
|
+
const result = new Date(date.getFullYear(), targetMonth, 1, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
86
|
+
const lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
|
|
87
|
+
result.setDate(Math.min(date.getDate(), lastDay));
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/** A new date `count` days after `date` (negative to subtract). */
|
|
91
|
+
function addDays(date, count) {
|
|
92
|
+
const result = new Date(date);
|
|
93
|
+
result.setDate(result.getDate() + count);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/** Midnight (00:00:00.000) on `date`'s calendar day. */
|
|
97
|
+
function startOfDay(date) {
|
|
98
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The first day of the week containing `date`, at midnight. `firstDayOfWeek`
|
|
102
|
+
* is the week's starting weekday (0 = Sunday … 6 = Saturday).
|
|
103
|
+
*/
|
|
104
|
+
function startOfWeek(date, firstDayOfWeek = 0) {
|
|
105
|
+
const start = startOfDay(date);
|
|
106
|
+
const diff = (start.getDay() - firstDayOfWeek + 7) % 7;
|
|
107
|
+
return addDays(start, -diff);
|
|
108
|
+
}
|
|
109
|
+
/** The last day of the week containing `date`, at midnight. */
|
|
110
|
+
function endOfWeek(date, firstDayOfWeek = 0) {
|
|
111
|
+
return addDays(startOfWeek(date, firstDayOfWeek), 6);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* ISO-8601 week number (1–53) of `date`. Weeks start on Monday and week 1 is
|
|
115
|
+
* the week containing the first Thursday of the year.
|
|
116
|
+
*/
|
|
117
|
+
function getISOWeek(date) {
|
|
118
|
+
// Shift to the Thursday of this ISO week, then count weeks from Jan 1.
|
|
119
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
120
|
+
const day = (d.getDay() + 6) % 7; // Monday = 0 … Sunday = 6
|
|
121
|
+
d.setDate(d.getDate() - day + 3);
|
|
122
|
+
const firstThursday = new Date(d.getFullYear(), 0, 4);
|
|
123
|
+
const firstDay = (firstThursday.getDay() + 6) % 7;
|
|
124
|
+
firstThursday.setDate(firstThursday.getDate() - firstDay + 3);
|
|
125
|
+
return (1 + Math.round((d.getTime() - firstThursday.getTime()) / (7 * 86400000)));
|
|
126
|
+
}
|
|
127
|
+
/** True when `a` is strictly before `b` (full timestamp comparison). */
|
|
128
|
+
function isBefore(a, b) {
|
|
129
|
+
return a.getTime() < b.getTime();
|
|
130
|
+
}
|
|
131
|
+
/** True when `a` is strictly after `b` (full timestamp comparison). */
|
|
132
|
+
function isAfter(a, b) {
|
|
133
|
+
return a.getTime() > b.getTime();
|
|
134
|
+
}
|
|
135
|
+
/** Clamp `date` into the inclusive `[min, max]` range (either bound optional). */
|
|
136
|
+
function clampDate(date, min, max) {
|
|
137
|
+
if (min && isBefore(date, min))
|
|
138
|
+
return new Date(min);
|
|
139
|
+
if (max && isAfter(date, max))
|
|
140
|
+
return new Date(max);
|
|
141
|
+
return date;
|
|
142
|
+
}
|
|
143
|
+
/** Format `date` as an ISO calendar date `YYYY-MM-DD` (local time). */
|
|
144
|
+
function formatISODate(date) {
|
|
145
|
+
return `${date.getFullYear().toString().padStart(4, '0')}-${pad2$1(date.getMonth() + 1)}-${pad2$1(date.getDate())}`;
|
|
146
|
+
}
|
|
147
|
+
/** Parse a `YYYY-MM-DD` string into a local-time `Date`, or `null` if invalid. */
|
|
148
|
+
function parseISODate(value) {
|
|
149
|
+
if (!value)
|
|
150
|
+
return null;
|
|
151
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
|
152
|
+
if (!match)
|
|
153
|
+
return null;
|
|
154
|
+
const year = Number(match[1]);
|
|
155
|
+
const month = Number(match[2]) - 1;
|
|
156
|
+
const day = Number(match[3]);
|
|
157
|
+
const date = new Date(year, month, day);
|
|
158
|
+
// Reject rollovers like 2024-02-31.
|
|
159
|
+
if (date.getFullYear() !== year ||
|
|
160
|
+
date.getMonth() !== month ||
|
|
161
|
+
date.getDate() !== day) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return date;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Format `date` using a small pattern set. Supported tokens:
|
|
168
|
+
* `yyyy` (4-digit year), `MMMM` (full month), `MMM` (short month),
|
|
169
|
+
* `MM` (2-digit month), `dd` (2-digit day), `d` (day), `ddd` (short weekday).
|
|
170
|
+
* Longer tokens are matched first so `MMMM` wins over `MM`.
|
|
171
|
+
* Month/weekday names come from `names` when given, else English.
|
|
172
|
+
*/
|
|
173
|
+
function formatDate(date, pattern, names) {
|
|
174
|
+
const tokens = [
|
|
175
|
+
['yyyy', () => date.getFullYear().toString().padStart(4, '0')],
|
|
176
|
+
['MMMM', () => (names?.months ?? MONTH_NAMES)[date.getMonth()]],
|
|
177
|
+
['MMM', () => (names?.monthsShort ?? MONTH_NAMES_SHORT)[date.getMonth()]],
|
|
178
|
+
['MM', () => pad2$1(date.getMonth() + 1)],
|
|
179
|
+
// Longest-first: 'ddd' (short weekday) must be matched before 'dd'/'d',
|
|
180
|
+
// otherwise 'ddd' is consumed as 'dd' + 'd' and produces day numbers.
|
|
181
|
+
['ddd', () => (names?.weekdaysShort ?? WEEKDAY_NAMES_SHORT)[date.getDay()]],
|
|
182
|
+
['dd', () => pad2$1(date.getDate())],
|
|
183
|
+
['d', () => date.getDate().toString()],
|
|
184
|
+
];
|
|
185
|
+
let result = '';
|
|
186
|
+
let i = 0;
|
|
187
|
+
outer: while (i < pattern.length) {
|
|
188
|
+
for (const [token, resolve] of tokens) {
|
|
189
|
+
if (pattern.startsWith(token, i)) {
|
|
190
|
+
result += resolve();
|
|
191
|
+
i += token.length;
|
|
192
|
+
continue outer;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
result += pattern[i];
|
|
196
|
+
i += 1;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build a fixed 6×7 matrix of `Date` cells for `viewDate`'s month, including
|
|
202
|
+
* the leading days from the previous month and trailing days from the next so
|
|
203
|
+
* every row is full. `firstDayOfWeek` is 0 (Sunday) … 6 (Saturday).
|
|
204
|
+
*/
|
|
205
|
+
function buildMonthMatrix(viewDate, firstDayOfWeek = 0) {
|
|
206
|
+
const first = startOfMonth(viewDate);
|
|
207
|
+
const firstDow = first.getDay();
|
|
208
|
+
const offset = (firstDow - firstDayOfWeek + 7) % 7;
|
|
209
|
+
const gridStart = addDays(first, -offset);
|
|
210
|
+
const weeks = [];
|
|
211
|
+
let cursor = gridStart;
|
|
212
|
+
for (let week = 0; week < 6; week++) {
|
|
213
|
+
const row = [];
|
|
214
|
+
for (let day = 0; day < 7; day++) {
|
|
215
|
+
row.push(cursor);
|
|
216
|
+
cursor = addDays(cursor, 1);
|
|
217
|
+
}
|
|
218
|
+
weeks.push(row);
|
|
219
|
+
}
|
|
220
|
+
return weeks;
|
|
221
|
+
}
|
|
222
|
+
/** Full month names, index 0 = January (from `names`, else English). */
|
|
223
|
+
function getMonthNames(names) {
|
|
224
|
+
return names?.months ?? MONTH_NAMES;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Weekday header labels ordered to start at `firstDayOfWeek`.
|
|
228
|
+
* `format` selects `'short'` (e.g. `Mon`) or `'narrow'` (e.g. `M`).
|
|
229
|
+
* Labels come from `names` when given, else English.
|
|
230
|
+
*/
|
|
231
|
+
function getWeekdayNames(firstDayOfWeek = 0, format = 'short', names) {
|
|
232
|
+
const source = format === 'narrow'
|
|
233
|
+
? (names?.weekdaysNarrow ?? WEEKDAY_NAMES_NARROW)
|
|
234
|
+
: (names?.weekdaysShort ?? WEEKDAY_NAMES_SHORT);
|
|
235
|
+
const start = ((firstDayOfWeek % 7) + 7) % 7;
|
|
236
|
+
const labels = [];
|
|
237
|
+
for (let i = 0; i < 7; i++) {
|
|
238
|
+
labels.push(source[(start + i) % 7]);
|
|
239
|
+
}
|
|
240
|
+
return labels;
|
|
241
|
+
}
|
|
242
|
+
/** Full weekday name for `date` (for screen-reader labels). */
|
|
243
|
+
function getWeekdayFullName(date, names) {
|
|
244
|
+
return (names?.weekdays ?? WEEKDAY_NAMES)[date.getDay()];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Calendar — an accessible month-grid date picker following the WAI-ARIA
|
|
249
|
+
* grid / date-picker pattern. It is the reusable core behind
|
|
250
|
+
* `mk-date-picker` and `mk-date-range-picker`.
|
|
251
|
+
*
|
|
252
|
+
* Keyboard (when a day has focus): Arrow keys move by day/week (crossing month
|
|
253
|
+
* boundaries updates the view), Home/End jump to the start/end of the week,
|
|
254
|
+
* PageUp/PageDown change month, Shift+PageUp/PageDown change year, and
|
|
255
|
+
* Enter/Space selects the focused day. A roving tabindex keeps a single tab
|
|
256
|
+
* stop. Month changes are announced via {@link MkLiveAnnouncer}.
|
|
257
|
+
*
|
|
258
|
+
* Exposes a two-way `value` model and implements `ControlValueAccessor`, so it
|
|
259
|
+
* also works directly with `ngModel` / `formControl` (single-date mode).
|
|
260
|
+
* Additive range inputs (`rangeMode`,
|
|
261
|
+
* `rangeStart`, `rangeEnd`) let it highlight an in-progress selection without
|
|
262
|
+
* affecting single-date use; range hosts listen to `dateSelected`.
|
|
263
|
+
*
|
|
264
|
+
* ```html
|
|
265
|
+
* <mk-calendar [(value)]="date" [min]="minDate" [firstDayOfWeek]="1" />
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
class MkCalendar {
|
|
269
|
+
host = inject(ElementRef);
|
|
270
|
+
i18n = inject(MK_I18N);
|
|
271
|
+
injector = inject(Injector);
|
|
272
|
+
announcer = inject(MkLiveAnnouncer);
|
|
273
|
+
/** Two-way selected date (single-date mode). */
|
|
274
|
+
value = model(null, /* @ts-ignore */
|
|
275
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
276
|
+
/** Earliest selectable date (inclusive, day granularity). */
|
|
277
|
+
min = input(null, /* @ts-ignore */
|
|
278
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
279
|
+
/** Latest selectable date (inclusive, day granularity). */
|
|
280
|
+
max = input(null, /* @ts-ignore */
|
|
281
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
282
|
+
/** First column of the week: 0 = Sunday … 6 = Saturday. */
|
|
283
|
+
firstDayOfWeek = input(0, { ...(ngDevMode ? { debugName: "firstDayOfWeek" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
284
|
+
/** Disable the whole calendar. */
|
|
285
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
286
|
+
/** Force the invalid visual + `aria-invalid` when used standalone. */
|
|
287
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
288
|
+
/** Predicate marking individual days as disabled. */
|
|
289
|
+
disabledDate = input(null, /* @ts-ignore */
|
|
290
|
+
...(ngDevMode ? [{ debugName: "disabledDate" }] : /* istanbul ignore next */ []));
|
|
291
|
+
/** Control size. */
|
|
292
|
+
size = input('md', /* @ts-ignore */
|
|
293
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
294
|
+
/**
|
|
295
|
+
* Stretch to fill the container instead of sizing to content.
|
|
296
|
+
*
|
|
297
|
+
* Off by default, because the common host is a date-picker popover that must
|
|
298
|
+
* stay a compact widget. Turn it on when the calendar is laid out as part of
|
|
299
|
+
* a page — a sidebar or a column — where sizing to content leaves the space
|
|
300
|
+
* beside it empty. Cells share the width evenly and `--_cell-size` becomes
|
|
301
|
+
* their minimum rather than their fixed size.
|
|
302
|
+
*/
|
|
303
|
+
fullWidth = input(false, { ...(ngDevMode ? { debugName: "fullWidth" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
304
|
+
// --- Range mode (additive; does not affect single-date use) --------------
|
|
305
|
+
/** Enable range highlighting. Hosts drive selection via `dateSelected`. */
|
|
306
|
+
rangeMode = input(false, { ...(ngDevMode ? { debugName: "rangeMode" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
307
|
+
/** Range start endpoint to highlight (range mode). */
|
|
308
|
+
rangeStart = input(null, /* @ts-ignore */
|
|
309
|
+
...(ngDevMode ? [{ debugName: "rangeStart" }] : /* istanbul ignore next */ []));
|
|
310
|
+
/** Range end endpoint to highlight (range mode). */
|
|
311
|
+
rangeEnd = input(null, /* @ts-ignore */
|
|
312
|
+
...(ngDevMode ? [{ debugName: "rangeEnd" }] : /* istanbul ignore next */ []));
|
|
313
|
+
/** Emitted whenever a (non-disabled) day is activated. */
|
|
314
|
+
dateSelected = output();
|
|
315
|
+
/**
|
|
316
|
+
* Emitted (in range mode) as the pointer hovers days — the hovered day, or
|
|
317
|
+
* `null` on leave. Lets hosts preview a selection (e.g. a whole-week
|
|
318
|
+
* highlight in `mk-week-picker`).
|
|
319
|
+
*/
|
|
320
|
+
dateHovered = output();
|
|
321
|
+
/** id of the visible month/year label — wire as the grid's label. */
|
|
322
|
+
labelId = mkUniqueId('mk-calendar-label');
|
|
323
|
+
/** Disabled state driven by forms (`setDisabledState`). */
|
|
324
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
325
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
326
|
+
/** The day that currently owns the roving tabindex; also drives the view. */
|
|
327
|
+
focusedDate = signal(this.initialFocus(), /* @ts-ignore */
|
|
328
|
+
...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
|
|
329
|
+
/** Hovered day used to preview a range before the end is picked. */
|
|
330
|
+
hoveredDate = signal(null, /* @ts-ignore */
|
|
331
|
+
...(ngDevMode ? [{ debugName: "hoveredDate" }] : /* istanbul ignore next */ []));
|
|
332
|
+
/** Disabled by the `disabled` input or by the bound form control. */
|
|
333
|
+
isControlDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
334
|
+
...(ngDevMode ? [{ debugName: "isControlDisabled" }] : /* istanbul ignore next */ []));
|
|
335
|
+
isInvalid = computed(() => this.invalid(), /* @ts-ignore */
|
|
336
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
337
|
+
/** First day of the visible month. */
|
|
338
|
+
viewMonth = computed(() => startOfMonth(this.focusedDate()), /* @ts-ignore */
|
|
339
|
+
...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
|
|
340
|
+
/** Human label for the visible month, e.g. `July 2026`. */
|
|
341
|
+
monthLabel = computed(() => formatDate(this.viewMonth(), 'MMMM yyyy', this.i18n.dateNames), /* @ts-ignore */
|
|
342
|
+
...(ngDevMode ? [{ debugName: "monthLabel" }] : /* istanbul ignore next */ []));
|
|
343
|
+
/** 6×7 grid of dates for the visible month. */
|
|
344
|
+
weeks = computed(() => buildMonthMatrix(this.viewMonth(), this.firstDayOfWeek()), /* @ts-ignore */
|
|
345
|
+
...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
|
|
346
|
+
/** Weekday column headers ordered from `firstDayOfWeek`. */
|
|
347
|
+
weekdays = computed(() => this.weeks()[0].map((d) => ({
|
|
348
|
+
short: formatDate(d, 'ddd', this.i18n.dateNames),
|
|
349
|
+
full: getWeekdayFullName(d, this.i18n.dateNames),
|
|
350
|
+
})), /* @ts-ignore */
|
|
351
|
+
...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
|
|
352
|
+
announcedMonth = this.monthLabel();
|
|
353
|
+
constructor() {
|
|
354
|
+
// Follow external value changes into the view/focus. Read focusedDate
|
|
355
|
+
// untracked so this effect only reacts to `value` — otherwise navigating
|
|
356
|
+
// months (which moves focusedDate) would re-run it and snap the view back.
|
|
357
|
+
effect(() => {
|
|
358
|
+
const v = this.value();
|
|
359
|
+
if (v && !isSameDay(v, untracked(this.focusedDate))) {
|
|
360
|
+
this.focusedDate.set(startOfDay(v));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// Announce month transitions for screen-reader users.
|
|
364
|
+
effect(() => {
|
|
365
|
+
const label = this.monthLabel();
|
|
366
|
+
if (label !== this.announcedMonth) {
|
|
367
|
+
this.announcedMonth = label;
|
|
368
|
+
this.announcer.announce(label);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// --- ControlValueAccessor ---------------------------------------------------
|
|
373
|
+
onChange = () => { };
|
|
374
|
+
onTouched = () => { };
|
|
375
|
+
writeValue(value) {
|
|
376
|
+
this.value.set(value instanceof Date ? startOfDay(value) : null);
|
|
377
|
+
}
|
|
378
|
+
registerOnChange(fn) {
|
|
379
|
+
this.onChange = fn;
|
|
380
|
+
}
|
|
381
|
+
registerOnTouched(fn) {
|
|
382
|
+
this.onTouched = fn;
|
|
383
|
+
}
|
|
384
|
+
setDisabledState(isDisabled) {
|
|
385
|
+
this.cvaDisabled.set(isDisabled);
|
|
386
|
+
}
|
|
387
|
+
/** Marks the control touched when focus leaves the calendar entirely. */
|
|
388
|
+
onFocusout(event) {
|
|
389
|
+
const next = event.relatedTarget;
|
|
390
|
+
if (!next || !this.host.nativeElement.contains(next))
|
|
391
|
+
this.onTouched();
|
|
392
|
+
}
|
|
393
|
+
initialFocus() {
|
|
394
|
+
const base = this.value() ?? new Date();
|
|
395
|
+
return startOfDay(clampDate(startOfDay(base), this.min(), this.max()));
|
|
396
|
+
}
|
|
397
|
+
// --- Day classification ---------------------------------------------------
|
|
398
|
+
isOutside(d) {
|
|
399
|
+
return !isSameMonth(d, this.viewMonth());
|
|
400
|
+
}
|
|
401
|
+
isDisabled(d) {
|
|
402
|
+
if (this.isControlDisabled())
|
|
403
|
+
return true;
|
|
404
|
+
const day = startOfDay(d);
|
|
405
|
+
const min = this.min();
|
|
406
|
+
const max = this.max();
|
|
407
|
+
if (min && isBefore(day, startOfDay(min)))
|
|
408
|
+
return true;
|
|
409
|
+
if (max && isAfter(day, startOfDay(max)))
|
|
410
|
+
return true;
|
|
411
|
+
return this.disabledDate()?.(d) ?? false;
|
|
412
|
+
}
|
|
413
|
+
isToday(d) {
|
|
414
|
+
return isSameDay(d, new Date());
|
|
415
|
+
}
|
|
416
|
+
isSelected(d) {
|
|
417
|
+
const v = this.value();
|
|
418
|
+
return !this.rangeMode() && !!v && isSameDay(d, v);
|
|
419
|
+
}
|
|
420
|
+
isFocused(d) {
|
|
421
|
+
return isSameDay(d, this.focusedDate());
|
|
422
|
+
}
|
|
423
|
+
/** Effective range end while previewing (falls back to the hovered day). */
|
|
424
|
+
previewEnd() {
|
|
425
|
+
return this.rangeEnd() ?? this.hoveredDate();
|
|
426
|
+
}
|
|
427
|
+
isRangeStart(d) {
|
|
428
|
+
const s = this.rangeStart();
|
|
429
|
+
return this.rangeMode() && !!s && isSameDay(d, s);
|
|
430
|
+
}
|
|
431
|
+
isRangeEnd(d) {
|
|
432
|
+
if (!this.rangeMode())
|
|
433
|
+
return false;
|
|
434
|
+
const end = this.previewEnd();
|
|
435
|
+
return !!end && isSameDay(d, end);
|
|
436
|
+
}
|
|
437
|
+
isInRange(d) {
|
|
438
|
+
if (!this.rangeMode())
|
|
439
|
+
return false;
|
|
440
|
+
const s = this.rangeStart();
|
|
441
|
+
const end = this.previewEnd();
|
|
442
|
+
if (!s || !end)
|
|
443
|
+
return false;
|
|
444
|
+
const [lo, hi] = isAfter(end, s) ? [s, end] : [end, s];
|
|
445
|
+
const day = startOfDay(d);
|
|
446
|
+
return isAfter(day, startOfDay(lo)) && isBefore(day, startOfDay(hi));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* `aria-selected` state of a day's gridcell. Single-date mode mirrors
|
|
450
|
+
* `isSelected`; range mode exposes the committed endpoints and the days
|
|
451
|
+
* between them (hover previews stay visual-only so the accessibility tree
|
|
452
|
+
* doesn't churn while the pointer moves).
|
|
453
|
+
*/
|
|
454
|
+
isAriaSelected(d) {
|
|
455
|
+
if (!this.rangeMode())
|
|
456
|
+
return this.isSelected(d);
|
|
457
|
+
const s = this.rangeStart();
|
|
458
|
+
const e = this.rangeEnd();
|
|
459
|
+
if (s && isSameDay(d, s))
|
|
460
|
+
return true;
|
|
461
|
+
if (e && isSameDay(d, e))
|
|
462
|
+
return true;
|
|
463
|
+
if (!s || !e)
|
|
464
|
+
return false;
|
|
465
|
+
const [lo, hi] = isAfter(e, s) ? [s, e] : [e, s];
|
|
466
|
+
const day = startOfDay(d);
|
|
467
|
+
return isAfter(day, startOfDay(lo)) && isBefore(day, startOfDay(hi));
|
|
468
|
+
}
|
|
469
|
+
dayLabel(d) {
|
|
470
|
+
return formatDate(d, 'MMMM d, yyyy', this.i18n.dateNames);
|
|
471
|
+
}
|
|
472
|
+
// --- Interaction ----------------------------------------------------------
|
|
473
|
+
select(d) {
|
|
474
|
+
if (this.isDisabled(d))
|
|
475
|
+
return;
|
|
476
|
+
const day = startOfDay(d);
|
|
477
|
+
this.focusedDate.set(day);
|
|
478
|
+
if (!this.rangeMode()) {
|
|
479
|
+
this.value.set(day);
|
|
480
|
+
this.onChange(day);
|
|
481
|
+
}
|
|
482
|
+
this.dateSelected.emit(day);
|
|
483
|
+
this.focusActiveCell();
|
|
484
|
+
}
|
|
485
|
+
onHover(d) {
|
|
486
|
+
if (!this.rangeMode())
|
|
487
|
+
return;
|
|
488
|
+
const day = startOfDay(d);
|
|
489
|
+
this.hoveredDate.set(day);
|
|
490
|
+
this.dateHovered.emit(day);
|
|
491
|
+
}
|
|
492
|
+
clearHover() {
|
|
493
|
+
if (!this.rangeMode())
|
|
494
|
+
return;
|
|
495
|
+
this.hoveredDate.set(null);
|
|
496
|
+
this.dateHovered.emit(null);
|
|
497
|
+
}
|
|
498
|
+
prevMonth() {
|
|
499
|
+
this.moveFocus(addMonths(this.focusedDate(), -1));
|
|
500
|
+
}
|
|
501
|
+
nextMonth() {
|
|
502
|
+
this.moveFocus(addMonths(this.focusedDate(), 1));
|
|
503
|
+
}
|
|
504
|
+
prevYear() {
|
|
505
|
+
this.moveFocus(addMonths(this.focusedDate(), -12));
|
|
506
|
+
}
|
|
507
|
+
nextYear() {
|
|
508
|
+
this.moveFocus(addMonths(this.focusedDate(), 12));
|
|
509
|
+
}
|
|
510
|
+
onGridKeydown(event) {
|
|
511
|
+
const e = event;
|
|
512
|
+
const current = this.focusedDate();
|
|
513
|
+
let next = null;
|
|
514
|
+
switch (e.key) {
|
|
515
|
+
case 'ArrowLeft':
|
|
516
|
+
next = addDays(current, -1);
|
|
517
|
+
break;
|
|
518
|
+
case 'ArrowRight':
|
|
519
|
+
next = addDays(current, 1);
|
|
520
|
+
break;
|
|
521
|
+
case 'ArrowUp':
|
|
522
|
+
next = addDays(current, -7);
|
|
523
|
+
break;
|
|
524
|
+
case 'ArrowDown':
|
|
525
|
+
next = addDays(current, 7);
|
|
526
|
+
break;
|
|
527
|
+
case 'Home':
|
|
528
|
+
next = addDays(current, -this.weekdayOffset(current));
|
|
529
|
+
break;
|
|
530
|
+
case 'End':
|
|
531
|
+
next = addDays(current, 6 - this.weekdayOffset(current));
|
|
532
|
+
break;
|
|
533
|
+
case 'PageUp':
|
|
534
|
+
next = addMonths(current, e.shiftKey ? -12 : -1);
|
|
535
|
+
break;
|
|
536
|
+
case 'PageDown':
|
|
537
|
+
next = addMonths(current, e.shiftKey ? 12 : 1);
|
|
538
|
+
break;
|
|
539
|
+
case 'Enter':
|
|
540
|
+
case ' ':
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
this.select(current);
|
|
543
|
+
return;
|
|
544
|
+
default:
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
if (next)
|
|
549
|
+
this.moveFocus(next);
|
|
550
|
+
}
|
|
551
|
+
weekdayOffset(d) {
|
|
552
|
+
return (d.getDay() - this.firstDayOfWeek() + 7) % 7;
|
|
553
|
+
}
|
|
554
|
+
moveFocus(target) {
|
|
555
|
+
this.focusedDate.set(startOfDay(target));
|
|
556
|
+
this.focusActiveCell();
|
|
557
|
+
}
|
|
558
|
+
focusActiveCell() {
|
|
559
|
+
afterNextRender({
|
|
560
|
+
write: () => {
|
|
561
|
+
const el = this.host.nativeElement.querySelector('.mk-calendar__day[tabindex="0"]');
|
|
562
|
+
el?.focus();
|
|
563
|
+
},
|
|
564
|
+
}, { injector: this.injector });
|
|
565
|
+
}
|
|
566
|
+
// --- Validator ------------------------------------------------------------
|
|
567
|
+
validatorChange = mkValidatorChange(() => {
|
|
568
|
+
this.min();
|
|
569
|
+
this.max();
|
|
570
|
+
this.disabledDate();
|
|
571
|
+
});
|
|
572
|
+
/**
|
|
573
|
+
* Reports `mkMinDate` / `mkMaxDate` for a date outside `[min]`/`[max]`, and
|
|
574
|
+
* `mkDateFilter` for one rejected by `[disabledDate]`.
|
|
575
|
+
*/
|
|
576
|
+
validate(control) {
|
|
577
|
+
const v = control.value;
|
|
578
|
+
if (!(v instanceof Date) || Number.isNaN(v.getTime()))
|
|
579
|
+
return null;
|
|
580
|
+
const day = startOfDay(v);
|
|
581
|
+
const min = this.min();
|
|
582
|
+
if (min && isBefore(day, startOfDay(min)))
|
|
583
|
+
return { mkMinDate: { min, actual: v } };
|
|
584
|
+
const max = this.max();
|
|
585
|
+
if (max && isAfter(day, startOfDay(max)))
|
|
586
|
+
return { mkMaxDate: { max, actual: v } };
|
|
587
|
+
return this.disabledDate()?.(v) ? { mkDateFilter: true } : null;
|
|
588
|
+
}
|
|
589
|
+
registerOnValidatorChange(fn) {
|
|
590
|
+
this.validatorChange.register(fn);
|
|
591
|
+
}
|
|
592
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCalendar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
593
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkCalendar, isStandalone: true, selector: "mk-calendar", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, disabledDate: { classPropertyName: "disabledDate", publicName: "disabledDate", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, rangeStart: { classPropertyName: "rangeStart", publicName: "rangeStart", isSignal: true, isRequired: false, transformFunction: null }, rangeEnd: { classPropertyName: "rangeEnd", publicName: "rangeEnd", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", dateSelected: "dateSelected", dateHovered: "dateHovered" }, host: { listeners: { "focusout": "onFocusout($event)" }, properties: { "class.mk-calendar--disabled": "isControlDisabled()", "class.mk-calendar--sm": "size() === 'sm'", "class.mk-calendar--md": "size() === 'md'", "class.mk-calendar--lg": "size() === 'lg'", "class.mk-calendar--full": "fullWidth()", "class.mk-calendar--range": "rangeMode()", "class.mk-calendar--invalid": "isInvalid()", "attr.aria-invalid": "isInvalid() || null" }, classAttribute: "mk-calendar" }, providers: [
|
|
594
|
+
{
|
|
595
|
+
provide: NG_VALUE_ACCESSOR,
|
|
596
|
+
useExisting: forwardRef(() => MkCalendar),
|
|
597
|
+
multi: true,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
provide: NG_VALIDATORS,
|
|
601
|
+
useExisting: forwardRef(() => MkCalendar),
|
|
602
|
+
multi: true,
|
|
603
|
+
},
|
|
604
|
+
], ngImport: i0, template: "<div class=\"mk-calendar__header\">\n <button\n type=\"button\"\n class=\"mk-calendar__nav\"\n [attr.aria-label]=\"i18n.previousMonth\"\n (click)=\"prevMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M15 5l-7 7 7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span class=\"mk-calendar__label\" [id]=\"labelId\" aria-live=\"polite\">{{\n monthLabel()\n }}</span>\n\n <button\n type=\"button\"\n class=\"mk-calendar__nav\"\n [attr.aria-label]=\"i18n.nextMonth\"\n (click)=\"nextMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M9 5l7 7-7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n</div>\n\n<table\n class=\"mk-calendar__grid\"\n role=\"grid\"\n [attr.aria-labelledby]=\"labelId\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <thead>\n <tr>\n @for (wd of weekdays(); track $index) {\n <th class=\"mk-calendar__weekday\" scope=\"col\">\n <abbr [title]=\"wd.full\">{{ wd.short }}</abbr>\n </th>\n }\n </tr>\n </thead>\n <tbody (mouseleave)=\"clearHover()\">\n @for (week of weeks(); track $index) {\n <tr>\n @for (day of week; track day.getTime()) {\n <td\n class=\"mk-calendar__cell\"\n role=\"gridcell\"\n [attr.aria-selected]=\"isAriaSelected(day) || null\"\n >\n <button\n type=\"button\"\n class=\"mk-calendar__day\"\n [class.mk-calendar__day--outside]=\"isOutside(day)\"\n [class.mk-calendar__day--today]=\"isToday(day)\"\n [class.mk-calendar__day--selected]=\"isSelected(day)\"\n [class.mk-calendar__day--range-start]=\"isRangeStart(day)\"\n [class.mk-calendar__day--range-end]=\"isRangeEnd(day)\"\n [class.mk-calendar__day--in-range]=\"isInRange(day)\"\n [class.mk-calendar__day--disabled]=\"isDisabled(day)\"\n [attr.tabindex]=\"isFocused(day) ? 0 : -1\"\n [attr.aria-label]=\"dayLabel(day)\"\n [attr.aria-current]=\"isToday(day) ? 'date' : null\"\n [attr.aria-disabled]=\"isDisabled(day) || null\"\n (click)=\"select(day)\"\n (mouseenter)=\"onHover(day)\"\n >\n <span class=\"mk-calendar__day-num\">{{ day.getDate() }}</span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: ["@charset \"UTF-8\";:host{--_cell-size: 2.5rem;display:inline-block;width:fit-content;max-width:100%;padding:var(--mk-space-4);font-family:var(--mk-font-sans);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-xs)}:host(.mk-calendar--full){display:block;width:100%;min-width:min-content}:host(.mk-calendar--full) .mk-calendar__grid{width:100%}:host(.mk-calendar--full) .mk-calendar__weekday{width:auto;min-width:var(--_cell-size)}:host(.mk-calendar--full) .mk-calendar__day{width:100%;min-width:var(--_cell-size)}:host(.mk-calendar--sm){--_cell-size: 2.1rem;padding:var(--mk-space-3);font-size:var(--mk-font-size-sm)}:host(.mk-calendar--lg){--_cell-size: 2.9rem;font-size:var(--mk-font-size-lg)}.mk-calendar__header{display:flex;align-items:center;gap:var(--mk-space-1);margin-bottom:var(--mk-space-2)}.mk-calendar__label{flex:1 1 auto;text-align:center;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-md);white-space:nowrap}.mk-calendar__nav{display:inline-flex;align-items:center;justify-content:center;width:var(--mk-control-height-sm);height:var(--mk-control-height-sm);flex:none;color:var(--mk-text-muted);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-calendar__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-calendar__nav:active{background-color:var(--mk-active-overlay)}.mk-calendar__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-calendar__nav svg{display:block}.mk-calendar__grid{border-collapse:separate;border-spacing:0;table-layout:fixed}.mk-calendar__weekday{width:var(--_cell-size);padding:0 0 var(--mk-space-2);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);letter-spacing:var(--mk-letter-spacing-wide);text-transform:uppercase;color:var(--mk-text-subtle)}.mk-calendar__weekday abbr{text-decoration:none;cursor:default}.mk-calendar__cell{padding:1px;text-align:center}.mk-calendar__day{display:inline-flex;align-items:center;justify-content:center;position:relative;width:var(--_cell-size);height:var(--_cell-size);font-family:inherit;font-size:var(--mk-font-size-sm);color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-calendar__day:hover:not(.mk-calendar__day--disabled){background-color:var(--mk-hover-overlay)}.mk-calendar__day:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1)}.mk-calendar__day--outside{color:var(--mk-text-disabled)}.mk-calendar__day--today:after{content:\"\";position:absolute;left:50%;bottom:4px;width:4px;height:4px;border-radius:var(--mk-radius-circle);background-color:var(--mk-primary);transform:translate(-50%)}.mk-calendar__day--in-range{background-color:var(--mk-primary-subtle);border-radius:0}.mk-calendar__day--selected,.mk-calendar__day--range-start,.mk-calendar__day--range-end{background-color:var(--mk-primary);color:var(--mk-primary-contrast);font-weight:var(--mk-font-weight-semibold)}.mk-calendar__day--selected:hover,.mk-calendar__day--range-start:hover,.mk-calendar__day--range-end:hover{background-color:var(--mk-primary-hover)}.mk-calendar__day--range-start{border-start-end-radius:0;border-end-end-radius:0}.mk-calendar__day--range-end{border-start-start-radius:0;border-end-start-radius:0}.mk-calendar__day--today.mk-calendar__day--selected:after,.mk-calendar__day--today.mk-calendar__day--range-start:after,.mk-calendar__day--today.mk-calendar__day--range-end:after{background-color:var(--mk-primary-contrast)}.mk-calendar__day--disabled{color:var(--mk-text-disabled);cursor:not-allowed;opacity:.6}.mk-calendar__day-num{line-height:1}@media(prefers-reduced-motion:reduce){.mk-calendar__nav,.mk-calendar__day{transition:none}}.mk-calendar--disabled{opacity:.55;pointer-events:none}:host(.mk-calendar--invalid){border-color:var(--mk-danger)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
605
|
+
}
|
|
606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCalendar, decorators: [{
|
|
607
|
+
type: Component,
|
|
608
|
+
args: [{ selector: 'mk-calendar', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
609
|
+
{
|
|
610
|
+
provide: NG_VALUE_ACCESSOR,
|
|
611
|
+
useExisting: forwardRef(() => MkCalendar),
|
|
612
|
+
multi: true,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
provide: NG_VALIDATORS,
|
|
616
|
+
useExisting: forwardRef(() => MkCalendar),
|
|
617
|
+
multi: true,
|
|
618
|
+
},
|
|
619
|
+
], host: {
|
|
620
|
+
class: 'mk-calendar',
|
|
621
|
+
'[class.mk-calendar--disabled]': 'isControlDisabled()',
|
|
622
|
+
'(focusout)': 'onFocusout($event)',
|
|
623
|
+
'[class.mk-calendar--sm]': "size() === 'sm'",
|
|
624
|
+
'[class.mk-calendar--md]': "size() === 'md'",
|
|
625
|
+
'[class.mk-calendar--lg]': "size() === 'lg'",
|
|
626
|
+
'[class.mk-calendar--full]': 'fullWidth()',
|
|
627
|
+
'[class.mk-calendar--range]': 'rangeMode()',
|
|
628
|
+
'[class.mk-calendar--invalid]': 'isInvalid()',
|
|
629
|
+
'[attr.aria-invalid]': 'isInvalid() || null',
|
|
630
|
+
}, template: "<div class=\"mk-calendar__header\">\n <button\n type=\"button\"\n class=\"mk-calendar__nav\"\n [attr.aria-label]=\"i18n.previousMonth\"\n (click)=\"prevMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M15 5l-7 7 7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span class=\"mk-calendar__label\" [id]=\"labelId\" aria-live=\"polite\">{{\n monthLabel()\n }}</span>\n\n <button\n type=\"button\"\n class=\"mk-calendar__nav\"\n [attr.aria-label]=\"i18n.nextMonth\"\n (click)=\"nextMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M9 5l7 7-7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n</div>\n\n<table\n class=\"mk-calendar__grid\"\n role=\"grid\"\n [attr.aria-labelledby]=\"labelId\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <thead>\n <tr>\n @for (wd of weekdays(); track $index) {\n <th class=\"mk-calendar__weekday\" scope=\"col\">\n <abbr [title]=\"wd.full\">{{ wd.short }}</abbr>\n </th>\n }\n </tr>\n </thead>\n <tbody (mouseleave)=\"clearHover()\">\n @for (week of weeks(); track $index) {\n <tr>\n @for (day of week; track day.getTime()) {\n <td\n class=\"mk-calendar__cell\"\n role=\"gridcell\"\n [attr.aria-selected]=\"isAriaSelected(day) || null\"\n >\n <button\n type=\"button\"\n class=\"mk-calendar__day\"\n [class.mk-calendar__day--outside]=\"isOutside(day)\"\n [class.mk-calendar__day--today]=\"isToday(day)\"\n [class.mk-calendar__day--selected]=\"isSelected(day)\"\n [class.mk-calendar__day--range-start]=\"isRangeStart(day)\"\n [class.mk-calendar__day--range-end]=\"isRangeEnd(day)\"\n [class.mk-calendar__day--in-range]=\"isInRange(day)\"\n [class.mk-calendar__day--disabled]=\"isDisabled(day)\"\n [attr.tabindex]=\"isFocused(day) ? 0 : -1\"\n [attr.aria-label]=\"dayLabel(day)\"\n [attr.aria-current]=\"isToday(day) ? 'date' : null\"\n [attr.aria-disabled]=\"isDisabled(day) || null\"\n (click)=\"select(day)\"\n (mouseenter)=\"onHover(day)\"\n >\n <span class=\"mk-calendar__day-num\">{{ day.getDate() }}</span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: ["@charset \"UTF-8\";:host{--_cell-size: 2.5rem;display:inline-block;width:fit-content;max-width:100%;padding:var(--mk-space-4);font-family:var(--mk-font-sans);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-xs)}:host(.mk-calendar--full){display:block;width:100%;min-width:min-content}:host(.mk-calendar--full) .mk-calendar__grid{width:100%}:host(.mk-calendar--full) .mk-calendar__weekday{width:auto;min-width:var(--_cell-size)}:host(.mk-calendar--full) .mk-calendar__day{width:100%;min-width:var(--_cell-size)}:host(.mk-calendar--sm){--_cell-size: 2.1rem;padding:var(--mk-space-3);font-size:var(--mk-font-size-sm)}:host(.mk-calendar--lg){--_cell-size: 2.9rem;font-size:var(--mk-font-size-lg)}.mk-calendar__header{display:flex;align-items:center;gap:var(--mk-space-1);margin-bottom:var(--mk-space-2)}.mk-calendar__label{flex:1 1 auto;text-align:center;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-md);white-space:nowrap}.mk-calendar__nav{display:inline-flex;align-items:center;justify-content:center;width:var(--mk-control-height-sm);height:var(--mk-control-height-sm);flex:none;color:var(--mk-text-muted);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-calendar__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-calendar__nav:active{background-color:var(--mk-active-overlay)}.mk-calendar__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-calendar__nav svg{display:block}.mk-calendar__grid{border-collapse:separate;border-spacing:0;table-layout:fixed}.mk-calendar__weekday{width:var(--_cell-size);padding:0 0 var(--mk-space-2);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);letter-spacing:var(--mk-letter-spacing-wide);text-transform:uppercase;color:var(--mk-text-subtle)}.mk-calendar__weekday abbr{text-decoration:none;cursor:default}.mk-calendar__cell{padding:1px;text-align:center}.mk-calendar__day{display:inline-flex;align-items:center;justify-content:center;position:relative;width:var(--_cell-size);height:var(--_cell-size);font-family:inherit;font-size:var(--mk-font-size-sm);color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-calendar__day:hover:not(.mk-calendar__day--disabled){background-color:var(--mk-hover-overlay)}.mk-calendar__day:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1)}.mk-calendar__day--outside{color:var(--mk-text-disabled)}.mk-calendar__day--today:after{content:\"\";position:absolute;left:50%;bottom:4px;width:4px;height:4px;border-radius:var(--mk-radius-circle);background-color:var(--mk-primary);transform:translate(-50%)}.mk-calendar__day--in-range{background-color:var(--mk-primary-subtle);border-radius:0}.mk-calendar__day--selected,.mk-calendar__day--range-start,.mk-calendar__day--range-end{background-color:var(--mk-primary);color:var(--mk-primary-contrast);font-weight:var(--mk-font-weight-semibold)}.mk-calendar__day--selected:hover,.mk-calendar__day--range-start:hover,.mk-calendar__day--range-end:hover{background-color:var(--mk-primary-hover)}.mk-calendar__day--range-start{border-start-end-radius:0;border-end-end-radius:0}.mk-calendar__day--range-end{border-start-start-radius:0;border-end-start-radius:0}.mk-calendar__day--today.mk-calendar__day--selected:after,.mk-calendar__day--today.mk-calendar__day--range-start:after,.mk-calendar__day--today.mk-calendar__day--range-end:after{background-color:var(--mk-primary-contrast)}.mk-calendar__day--disabled{color:var(--mk-text-disabled);cursor:not-allowed;opacity:.6}.mk-calendar__day-num{line-height:1}@media(prefers-reduced-motion:reduce){.mk-calendar__nav,.mk-calendar__day{transition:none}}.mk-calendar--disabled{opacity:.55;pointer-events:none}:host(.mk-calendar--invalid){border-color:var(--mk-danger)}\n"] }]
|
|
631
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], disabledDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDate", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], rangeStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeStart", required: false }] }], rangeEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeEnd", required: false }] }], dateSelected: [{ type: i0.Output, args: ["dateSelected"] }], dateHovered: [{ type: i0.Output, args: ["dateHovered"] }] } });
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* DatePicker — a text field paired with a calendar popover. Type a date (parsed
|
|
635
|
+
* on blur/Enter) or pick one from the `mk-calendar`. Implements
|
|
636
|
+
* `ControlValueAccessor` and a two-way `value` model, so it works with
|
|
637
|
+
* `[(ngModel)]`, reactive forms and `[(value)]`.
|
|
638
|
+
*
|
|
639
|
+
* The calendar opens below the field, closes on Escape / outside click / after
|
|
640
|
+
* a selection, and wires `aria-expanded` / `aria-haspopup` / `aria-invalid`.
|
|
641
|
+
* When nested in an `mk-form-field` it adopts the field's id and aria wiring.
|
|
642
|
+
*
|
|
643
|
+
* ```html
|
|
644
|
+
* <mk-date-picker [(value)]="date" [min]="minDate" clearable />
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
class MkDatePicker {
|
|
648
|
+
i18n = inject(MK_I18N);
|
|
649
|
+
field = inject(MkFormField, { optional: true });
|
|
650
|
+
host = inject(ElementRef);
|
|
651
|
+
injector = inject(Injector);
|
|
652
|
+
inputRef = viewChild('textInput', /* @ts-ignore */
|
|
653
|
+
...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
654
|
+
/** The calendar panel — lives in the top layer once opened. */
|
|
655
|
+
panelRef = viewChild('panel', /* @ts-ignore */
|
|
656
|
+
...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
|
|
657
|
+
/** Two-way selected date. */
|
|
658
|
+
value = model(null, /* @ts-ignore */
|
|
659
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
660
|
+
/** Earliest selectable date (inclusive). */
|
|
661
|
+
min = input(null, /* @ts-ignore */
|
|
662
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
663
|
+
/** Latest selectable date (inclusive). */
|
|
664
|
+
max = input(null, /* @ts-ignore */
|
|
665
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
666
|
+
/** Predicate marking individual days as disabled (threaded to the calendar). */
|
|
667
|
+
disabledDate = input(null, /* @ts-ignore */
|
|
668
|
+
...(ngDevMode ? [{ debugName: "disabledDate" }] : /* istanbul ignore next */ []));
|
|
669
|
+
/** Placeholder shown when empty. */
|
|
670
|
+
placeholder = input(this.i18n.selectDate, /* @ts-ignore */
|
|
671
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
672
|
+
/** Pattern used to render the selected date in the field. */
|
|
673
|
+
displayFormat = input('MMM d, yyyy', /* @ts-ignore */
|
|
674
|
+
...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
675
|
+
/** Disable the control. */
|
|
676
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
677
|
+
/** Show a clear button when a date is selected. */
|
|
678
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
679
|
+
/** Force invalid styling + `aria-invalid`. */
|
|
680
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
681
|
+
/** First column of the calendar week (0 = Sunday). */
|
|
682
|
+
firstDayOfWeek = input(0, { ...(ngDevMode ? { debugName: "firstDayOfWeek" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
683
|
+
/** Control size. Ignored when nested in an `mk-form-field`. */
|
|
684
|
+
size = input('md', /* @ts-ignore */
|
|
685
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
686
|
+
open = signal(false, /* @ts-ignore */
|
|
687
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
688
|
+
/** Editable text mirror of the field. */
|
|
689
|
+
inputText = signal('', /* @ts-ignore */
|
|
690
|
+
...(ngDevMode ? [{ debugName: "inputText" }] : /* istanbul ignore next */ []));
|
|
691
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
692
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
693
|
+
onChange = () => { };
|
|
694
|
+
onTouched = () => { };
|
|
695
|
+
inputId = this.field?.controlId ?? mkUniqueId('mk-date-picker');
|
|
696
|
+
panelId = mkUniqueId('mk-date-picker-panel');
|
|
697
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
698
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
699
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
700
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
701
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
702
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
703
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
704
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
705
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
706
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
707
|
+
showClear = computed(() => this.clearable() && !!this.value() && !this.isDisabled(), /* @ts-ignore */
|
|
708
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
709
|
+
constructor() {
|
|
710
|
+
// Keep the text field in sync with the model (typing does not touch value).
|
|
711
|
+
effect(() => {
|
|
712
|
+
const v = this.value();
|
|
713
|
+
this.inputText.set(v ? formatDate(v, this.displayFormat(), this.i18n.dateNames) : '');
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
toggle() {
|
|
717
|
+
if (this.isDisabled())
|
|
718
|
+
return;
|
|
719
|
+
this.open() ? this.close() : this.openPanel();
|
|
720
|
+
}
|
|
721
|
+
openPanel() {
|
|
722
|
+
if (this.isDisabled() || this.open())
|
|
723
|
+
return;
|
|
724
|
+
this.open.set(true);
|
|
725
|
+
afterNextRender({
|
|
726
|
+
write: () => {
|
|
727
|
+
const el = this.panelRef()?.nativeElement.querySelector('.mk-calendar__day[tabindex="0"]');
|
|
728
|
+
el?.focus();
|
|
729
|
+
},
|
|
730
|
+
}, { injector: this.injector });
|
|
731
|
+
}
|
|
732
|
+
close() {
|
|
733
|
+
if (this.open())
|
|
734
|
+
this.open.set(false);
|
|
735
|
+
}
|
|
736
|
+
onCalendarPick(date) {
|
|
737
|
+
this.setValue(date);
|
|
738
|
+
this.close();
|
|
739
|
+
this.inputRef()?.nativeElement.focus();
|
|
740
|
+
}
|
|
741
|
+
onInput(event) {
|
|
742
|
+
this.inputText.set(event.target.value);
|
|
743
|
+
}
|
|
744
|
+
onInputKeydown(event) {
|
|
745
|
+
const e = event;
|
|
746
|
+
if (this.isDisabled())
|
|
747
|
+
return;
|
|
748
|
+
switch (e.key) {
|
|
749
|
+
case 'Enter':
|
|
750
|
+
e.preventDefault();
|
|
751
|
+
this.commitInput();
|
|
752
|
+
break;
|
|
753
|
+
case 'ArrowDown':
|
|
754
|
+
e.preventDefault();
|
|
755
|
+
this.openPanel();
|
|
756
|
+
break;
|
|
757
|
+
case 'Escape':
|
|
758
|
+
if (this.open()) {
|
|
759
|
+
e.preventDefault();
|
|
760
|
+
this.close();
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
clear() {
|
|
766
|
+
this.setValue(null);
|
|
767
|
+
this.inputRef()?.nativeElement.focus();
|
|
768
|
+
}
|
|
769
|
+
commitInput() {
|
|
770
|
+
const text = this.inputText().trim();
|
|
771
|
+
if (!text) {
|
|
772
|
+
this.setValue(null);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const parsed = this.parse(text);
|
|
776
|
+
if (parsed) {
|
|
777
|
+
this.setValue(clampDate(parsed, this.min(), this.max()));
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
// Invalid entry — revert to the current model's display.
|
|
781
|
+
const v = this.value();
|
|
782
|
+
this.inputText.set(v ? formatDate(v, this.displayFormat(), this.i18n.dateNames) : '');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
parse(text) {
|
|
786
|
+
const iso = parseISODate(text);
|
|
787
|
+
if (iso)
|
|
788
|
+
return iso;
|
|
789
|
+
const ms = Date.parse(text);
|
|
790
|
+
return Number.isNaN(ms) ? null : startOfDay(new Date(ms));
|
|
791
|
+
}
|
|
792
|
+
setValue(date) {
|
|
793
|
+
this.value.set(date);
|
|
794
|
+
this.onChange(date);
|
|
795
|
+
}
|
|
796
|
+
onFocusOut(event) {
|
|
797
|
+
const related = event.relatedTarget;
|
|
798
|
+
// Ignore focus moving into the field or the (top-layer) calendar panel.
|
|
799
|
+
if (related && this.host.nativeElement.contains(related))
|
|
800
|
+
return;
|
|
801
|
+
if (related && this.panelRef()?.nativeElement.contains(related))
|
|
802
|
+
return;
|
|
803
|
+
this.commitInput();
|
|
804
|
+
this.close();
|
|
805
|
+
this.onTouched();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Escape pressed inside the (teleported) calendar panel — close and return
|
|
809
|
+
* focus to the input. `preventDefault` + `stopPropagation` keep any outer
|
|
810
|
+
* Escape handling (e.g. a containing dialog) from also firing.
|
|
811
|
+
*/
|
|
812
|
+
onPanelEscape(event) {
|
|
813
|
+
event.preventDefault();
|
|
814
|
+
event.stopPropagation();
|
|
815
|
+
this.close();
|
|
816
|
+
this.inputRef()?.nativeElement.focus();
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Focus left the teleported panel. The host `(focusout)` never sees this —
|
|
820
|
+
* the panel lives under `document.body` — so Tab-out of the calendar is
|
|
821
|
+
* handled here: close unless focus moved back into the field or the panel.
|
|
822
|
+
*/
|
|
823
|
+
onPanelFocusout(event) {
|
|
824
|
+
const related = event.relatedTarget;
|
|
825
|
+
// Focus fell to a non-focusable spot (body) — keep open; an outside
|
|
826
|
+
// pointerdown is dismissed by the anchored panel itself.
|
|
827
|
+
if (!related)
|
|
828
|
+
return;
|
|
829
|
+
if (this.host.nativeElement.contains(related))
|
|
830
|
+
return;
|
|
831
|
+
if (this.panelRef()?.nativeElement.contains(related))
|
|
832
|
+
return;
|
|
833
|
+
this.commitInput();
|
|
834
|
+
this.close();
|
|
835
|
+
this.onTouched();
|
|
836
|
+
}
|
|
837
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
838
|
+
writeValue(value) {
|
|
839
|
+
this.value.set(value ?? null);
|
|
840
|
+
}
|
|
841
|
+
registerOnChange(fn) {
|
|
842
|
+
this.onChange = fn;
|
|
843
|
+
}
|
|
844
|
+
registerOnTouched(fn) {
|
|
845
|
+
this.onTouched = fn;
|
|
846
|
+
}
|
|
847
|
+
setDisabledState(isDisabled) {
|
|
848
|
+
this.cvaDisabled.set(isDisabled);
|
|
849
|
+
}
|
|
850
|
+
// --- Validator ------------------------------------------------------------
|
|
851
|
+
validatorChange = mkValidatorChange(() => {
|
|
852
|
+
this.min();
|
|
853
|
+
this.max();
|
|
854
|
+
});
|
|
855
|
+
/**
|
|
856
|
+
* Reports `mkMinDate` / `mkMaxDate` against the `[min]` and `[max]` inputs.
|
|
857
|
+
*/
|
|
858
|
+
validate(control) {
|
|
859
|
+
const v = control.value;
|
|
860
|
+
if (!(v instanceof Date) || Number.isNaN(v.getTime()))
|
|
861
|
+
return null;
|
|
862
|
+
const day = startOfDay(v);
|
|
863
|
+
const min = this.min();
|
|
864
|
+
if (min && isBefore(day, startOfDay(min)))
|
|
865
|
+
return { mkMinDate: { min, actual: v } };
|
|
866
|
+
const max = this.max();
|
|
867
|
+
if (max && isAfter(day, startOfDay(max)))
|
|
868
|
+
return { mkMaxDate: { max, actual: v } };
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
registerOnValidatorChange(fn) {
|
|
872
|
+
this.validatorChange.register(fn);
|
|
873
|
+
}
|
|
874
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDatePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
875
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkDatePicker, isStandalone: true, selector: "mk-date-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, disabledDate: { classPropertyName: "disabledDate", publicName: "disabledDate", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class.mk-date-picker--sm": "effectiveSize() === 'sm'", "class.mk-date-picker--md": "effectiveSize() === 'md'", "class.mk-date-picker--lg": "effectiveSize() === 'lg'", "class.mk-date-picker--open": "open()", "class.mk-date-picker--invalid": "isInvalid()", "class.mk-date-picker--disabled": "isDisabled()" }, classAttribute: "mk-date-picker" }, providers: [
|
|
876
|
+
{
|
|
877
|
+
provide: NG_VALUE_ACCESSOR,
|
|
878
|
+
useExisting: forwardRef(() => MkDatePicker),
|
|
879
|
+
multi: true,
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
provide: NG_VALIDATORS,
|
|
883
|
+
useExisting: forwardRef(() => MkDatePicker),
|
|
884
|
+
multi: true,
|
|
885
|
+
},
|
|
886
|
+
], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["textInput"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #field class=\"mk-date-picker__field\">\n <input\n #textInput\n type=\"text\"\n class=\"mk-date-picker__input\"\n [id]=\"inputId\"\n [value]=\"inputText()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n />\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-date-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-date-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-date-picker__toggle\"\n [attr.aria-label]=\"i18n.openCalendar\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-date-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-date-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseDate\"\n (keydown.escape)=\"onPanelEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <mk-calendar\n [value]=\"value()\"\n (valueChange)=\"onCalendarPick($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n [disabledDate]=\"disabledDate()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-date-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-date-picker--sm) .mk-date-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-date-picker--lg) .mk-date-picker__field{height:var(--mk-control-height-lg)}.mk-date-picker__field:hover{border-color:var(--mk-border-strong)}.mk-date-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-date-picker--invalid) .mk-date-picker__field{border-color:var(--mk-danger)}:host(.mk-date-picker--invalid) .mk-date-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-date-picker--disabled) .mk-date-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-date-picker__input{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background:transparent;border:0;outline:none}:host(.mk-date-picker--sm) .mk-date-picker__input{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-date-picker--lg) .mk-date-picker__input{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-date-picker__input::placeholder{color:var(--mk-text-subtle);opacity:1}.mk-date-picker__input:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-date-picker__toggle,.mk-date-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-date-picker__toggle:hover:not(:disabled),.mk-date-picker__clear:hover{color:var(--mk-text)}.mk-date-picker__toggle:focus-visible,.mk-date-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-date-picker__toggle:disabled{cursor:not-allowed}.mk-date-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-date-picker__clear-icon:before,.mk-date-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-date-picker__clear-icon:before{transform:rotate(45deg)}.mk-date-picker__clear-icon:after{transform:rotate(-45deg)}.mk-date-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-date-picker__field,.mk-date-picker__toggle,.mk-date-picker__clear{transition:none}}@media(pointer:coarse){.mk-date-picker__input{font-size:max(var(--mk-font-size-md),16px)}:host(.mk-date-picker--sm) .mk-date-picker__input{font-size:max(var(--mk-font-size-sm),16px)}}\n"], dependencies: [{ kind: "component", type: MkCalendar, selector: "mk-calendar", inputs: ["value", "min", "max", "firstDayOfWeek", "disabled", "invalid", "disabledDate", "size", "fullWidth", "rangeMode", "rangeStart", "rangeEnd"], outputs: ["valueChange", "dateSelected", "dateHovered"] }, { kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
887
|
+
}
|
|
888
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDatePicker, decorators: [{
|
|
889
|
+
type: Component,
|
|
890
|
+
args: [{ selector: 'mk-date-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkCalendar, MkAnchoredPanel], host: {
|
|
891
|
+
class: 'mk-date-picker',
|
|
892
|
+
'[class.mk-date-picker--sm]': "effectiveSize() === 'sm'",
|
|
893
|
+
'[class.mk-date-picker--md]': "effectiveSize() === 'md'",
|
|
894
|
+
'[class.mk-date-picker--lg]': "effectiveSize() === 'lg'",
|
|
895
|
+
'[class.mk-date-picker--open]': 'open()',
|
|
896
|
+
'[class.mk-date-picker--invalid]': 'isInvalid()',
|
|
897
|
+
'[class.mk-date-picker--disabled]': 'isDisabled()',
|
|
898
|
+
'(focusout)': 'onFocusOut($event)',
|
|
899
|
+
}, providers: [
|
|
900
|
+
{
|
|
901
|
+
provide: NG_VALUE_ACCESSOR,
|
|
902
|
+
useExisting: forwardRef(() => MkDatePicker),
|
|
903
|
+
multi: true,
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
provide: NG_VALIDATORS,
|
|
907
|
+
useExisting: forwardRef(() => MkDatePicker),
|
|
908
|
+
multi: true,
|
|
909
|
+
},
|
|
910
|
+
], template: "<div #field class=\"mk-date-picker__field\">\n <input\n #textInput\n type=\"text\"\n class=\"mk-date-picker__input\"\n [id]=\"inputId\"\n [value]=\"inputText()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n />\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-date-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-date-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-date-picker__toggle\"\n [attr.aria-label]=\"i18n.openCalendar\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-date-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-date-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseDate\"\n (keydown.escape)=\"onPanelEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <mk-calendar\n [value]=\"value()\"\n (valueChange)=\"onCalendarPick($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n [disabledDate]=\"disabledDate()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-date-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-date-picker--sm) .mk-date-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-date-picker--lg) .mk-date-picker__field{height:var(--mk-control-height-lg)}.mk-date-picker__field:hover{border-color:var(--mk-border-strong)}.mk-date-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-date-picker--invalid) .mk-date-picker__field{border-color:var(--mk-danger)}:host(.mk-date-picker--invalid) .mk-date-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-date-picker--disabled) .mk-date-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-date-picker__input{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background:transparent;border:0;outline:none}:host(.mk-date-picker--sm) .mk-date-picker__input{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-date-picker--lg) .mk-date-picker__input{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-date-picker__input::placeholder{color:var(--mk-text-subtle);opacity:1}.mk-date-picker__input:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-date-picker__toggle,.mk-date-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-date-picker__toggle:hover:not(:disabled),.mk-date-picker__clear:hover{color:var(--mk-text)}.mk-date-picker__toggle:focus-visible,.mk-date-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-date-picker__toggle:disabled{cursor:not-allowed}.mk-date-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-date-picker__clear-icon:before,.mk-date-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-date-picker__clear-icon:before{transform:rotate(45deg)}.mk-date-picker__clear-icon:after{transform:rotate(-45deg)}.mk-date-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-date-picker__field,.mk-date-picker__toggle,.mk-date-picker__clear{transition:none}}@media(pointer:coarse){.mk-date-picker__input{font-size:max(var(--mk-font-size-md),16px)}:host(.mk-date-picker--sm) .mk-date-picker__input{font-size:max(var(--mk-font-size-sm),16px)}}\n"] }]
|
|
911
|
+
}], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['textInput', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], disabledDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
912
|
+
|
|
913
|
+
function pad2(n) {
|
|
914
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* TimePicker — a 24-hour-canonical time field with an editable input plus a
|
|
918
|
+
* scrollable list of times generated from `step`. The time itself is always
|
|
919
|
+
* 24h canonical regardless of the `hour12` display. Implements
|
|
920
|
+
* `ControlValueAccessor` and a two-way `value` model.
|
|
921
|
+
*
|
|
922
|
+
* The form value is either a `HH:mm` string (`"14:30"`, default) or a `Date`
|
|
923
|
+
* carrying the time in its **local** hours/minutes (`valueFormat="date"`, for
|
|
924
|
+
* hosts whose model is a datetime — e.g. migrating off Material's
|
|
925
|
+
* `mat-timepicker`); both modes read back either shape and `null` when empty.
|
|
926
|
+
* Only the *emitted* value depends on `valueFormat` — internally the time is
|
|
927
|
+
* always normalised to `HH:mm`, which is also what `min` / `max` compare.
|
|
928
|
+
*
|
|
929
|
+
* In `date` mode seconds and milliseconds are zeroed, and the **date part** is
|
|
930
|
+
* taken from the current model value when that is already a `Date` (so editing
|
|
931
|
+
* the time repeatedly never drifts the day); otherwise today's local date is
|
|
932
|
+
* used. Assign a `Date` first if the day matters.
|
|
933
|
+
*
|
|
934
|
+
* Type a time (`14:30`, `2:30 pm`) or pick from the ARIA listbox
|
|
935
|
+
* (Up/Down/Home/End/Enter/Esc; the active option scrolls into view). When
|
|
936
|
+
* nested in an `mk-form-field` it adopts the field's id and aria wiring.
|
|
937
|
+
*
|
|
938
|
+
* ```html
|
|
939
|
+
* <mk-time-picker [(value)]="time" [step]="15" hour12 clearable />
|
|
940
|
+
* <mk-time-picker valueFormat="date" [(value)]="pickupAt" [step]="15" />
|
|
941
|
+
* ```
|
|
942
|
+
*/
|
|
943
|
+
class MkTimePicker {
|
|
944
|
+
i18n = inject(MK_I18N);
|
|
945
|
+
field = inject(MkFormField, { optional: true });
|
|
946
|
+
host = inject(ElementRef);
|
|
947
|
+
injector = inject(Injector);
|
|
948
|
+
inputRef = viewChild('textInput', /* @ts-ignore */
|
|
949
|
+
...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
950
|
+
/** The listbox panel — lives in the top layer once opened. */
|
|
951
|
+
listRef = viewChild('list', /* @ts-ignore */
|
|
952
|
+
...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
|
|
953
|
+
/**
|
|
954
|
+
* Two-way selected time: a canonical `HH:mm` (24h) string, a `Date` when
|
|
955
|
+
* `valueFormat="date"`, or `null`. Either shape may be assigned in either
|
|
956
|
+
* mode; it is normalised to the configured format.
|
|
957
|
+
*/
|
|
958
|
+
value = model(null, /* @ts-ignore */
|
|
959
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
960
|
+
/** Shape of the form value: a `HH:mm` string or a `Date`. */
|
|
961
|
+
valueFormat = input('string', /* @ts-ignore */
|
|
962
|
+
...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
|
|
963
|
+
/** Earliest selectable time `HH:mm` (inclusive). */
|
|
964
|
+
min = input(null, /* @ts-ignore */
|
|
965
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
966
|
+
/** Latest selectable time `HH:mm` (inclusive). */
|
|
967
|
+
max = input(null, /* @ts-ignore */
|
|
968
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
969
|
+
/** Interval between generated options, in minutes. */
|
|
970
|
+
step = input(30, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
971
|
+
/** Display 12-hour time with AM/PM (the model stays 24h). */
|
|
972
|
+
hour12 = input(false, { ...(ngDevMode ? { debugName: "hour12" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
973
|
+
/** Disable the control. */
|
|
974
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
975
|
+
/** Placeholder shown when empty. */
|
|
976
|
+
placeholder = input(this.i18n.selectTime, /* @ts-ignore */
|
|
977
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
978
|
+
/** Show a clear button when a time is selected. */
|
|
979
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
980
|
+
/** Force invalid styling + `aria-invalid`. */
|
|
981
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
982
|
+
/** Control size. Ignored when nested in an `mk-form-field`. */
|
|
983
|
+
size = input('md', /* @ts-ignore */
|
|
984
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
985
|
+
open = signal(false, /* @ts-ignore */
|
|
986
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
987
|
+
activeIndex = signal(-1, /* @ts-ignore */
|
|
988
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
989
|
+
inputText = signal('', /* @ts-ignore */
|
|
990
|
+
...(ngDevMode ? [{ debugName: "inputText" }] : /* istanbul ignore next */ []));
|
|
991
|
+
/** The canonical `HH:mm` time behind whatever shape `value` carries. */
|
|
992
|
+
time = signal(null, /* @ts-ignore */
|
|
993
|
+
...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
|
|
994
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
995
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
996
|
+
onChange = () => { };
|
|
997
|
+
onTouched = () => { };
|
|
998
|
+
/** The last value this component itself wrote into the `value` model. */
|
|
999
|
+
lastDerived = null;
|
|
1000
|
+
/** The day `date`-mode values land on — see {@link toDate}. */
|
|
1001
|
+
datePart = null;
|
|
1002
|
+
inputId = this.field?.controlId ?? mkUniqueId('mk-time-picker');
|
|
1003
|
+
listId = mkUniqueId('mk-time-picker-list');
|
|
1004
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
1005
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
1006
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
1007
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1008
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
1009
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
1010
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
1011
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
1012
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
1013
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1014
|
+
showClear = computed(() => this.clearable() && !!this.time() && !this.isDisabled(), /* @ts-ignore */
|
|
1015
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
1016
|
+
/** Generated, min/max-filtered list of selectable times. */
|
|
1017
|
+
options = computed(() => {
|
|
1018
|
+
const step = Math.max(1, Math.floor(this.step()));
|
|
1019
|
+
const min = this.min();
|
|
1020
|
+
const max = this.max();
|
|
1021
|
+
const out = [];
|
|
1022
|
+
for (let mins = 0; mins < 24 * 60; mins += step) {
|
|
1023
|
+
const value = `${pad2(Math.floor(mins / 60))}:${pad2(mins % 60)}`;
|
|
1024
|
+
if (min && value < min)
|
|
1025
|
+
continue;
|
|
1026
|
+
if (max && value > max)
|
|
1027
|
+
continue;
|
|
1028
|
+
out.push({ value, label: this.display(value) });
|
|
1029
|
+
}
|
|
1030
|
+
return out;
|
|
1031
|
+
}, /* @ts-ignore */
|
|
1032
|
+
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1033
|
+
selectedIndex = computed(() => this.options().findIndex((o) => o.value === this.time()), /* @ts-ignore */
|
|
1034
|
+
...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
|
|
1035
|
+
constructor() {
|
|
1036
|
+
effect(() => {
|
|
1037
|
+
const v = this.time();
|
|
1038
|
+
// hour12 is read so the label follows the display mode.
|
|
1039
|
+
this.inputText.set(v ? this.display(v) : '');
|
|
1040
|
+
});
|
|
1041
|
+
// Parse values assigned from outside ([(value)] writes we didn't derive),
|
|
1042
|
+
// and re-shape the model when `valueFormat` changes.
|
|
1043
|
+
effect(() => {
|
|
1044
|
+
const v = this.value();
|
|
1045
|
+
const external = v !== this.lastDerived;
|
|
1046
|
+
this.valueFormat();
|
|
1047
|
+
untracked(() => {
|
|
1048
|
+
if (external)
|
|
1049
|
+
this.applyExternal(v);
|
|
1050
|
+
this.deriveValue(false);
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
optionId(index) {
|
|
1055
|
+
return `${this.listId}-opt-${index}`;
|
|
1056
|
+
}
|
|
1057
|
+
display(hhmm) {
|
|
1058
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1059
|
+
if (this.hour12()) {
|
|
1060
|
+
const ap = h < 12 ? 'AM' : 'PM';
|
|
1061
|
+
const hh = h % 12 || 12;
|
|
1062
|
+
return `${hh}:${pad2(m)} ${ap}`;
|
|
1063
|
+
}
|
|
1064
|
+
return `${pad2(h)}:${pad2(m)}`;
|
|
1065
|
+
}
|
|
1066
|
+
parse(text) {
|
|
1067
|
+
const m = /^\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*$/i.exec(text);
|
|
1068
|
+
if (!m)
|
|
1069
|
+
return null;
|
|
1070
|
+
let h = Number(m[1]);
|
|
1071
|
+
const min = m[2] ? Number(m[2]) : 0;
|
|
1072
|
+
const ap = m[3]?.toLowerCase();
|
|
1073
|
+
if (ap) {
|
|
1074
|
+
if (h < 1 || h > 12)
|
|
1075
|
+
return null;
|
|
1076
|
+
if (ap === 'pm' && h !== 12)
|
|
1077
|
+
h += 12;
|
|
1078
|
+
if (ap === 'am' && h === 12)
|
|
1079
|
+
h = 0;
|
|
1080
|
+
}
|
|
1081
|
+
if (h > 23 || min > 59)
|
|
1082
|
+
return null;
|
|
1083
|
+
return `${pad2(h)}:${pad2(min)}`;
|
|
1084
|
+
}
|
|
1085
|
+
toggle() {
|
|
1086
|
+
if (this.isDisabled())
|
|
1087
|
+
return;
|
|
1088
|
+
this.open() ? this.close() : this.openList();
|
|
1089
|
+
}
|
|
1090
|
+
openList() {
|
|
1091
|
+
if (this.isDisabled() || this.open())
|
|
1092
|
+
return;
|
|
1093
|
+
this.open.set(true);
|
|
1094
|
+
const sel = this.selectedIndex();
|
|
1095
|
+
this.activeIndex.set(sel >= 0 ? sel : 0);
|
|
1096
|
+
this.scrollActiveIntoView();
|
|
1097
|
+
}
|
|
1098
|
+
close() {
|
|
1099
|
+
if (this.open()) {
|
|
1100
|
+
this.open.set(false);
|
|
1101
|
+
this.activeIndex.set(-1);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
selectOption(index) {
|
|
1105
|
+
const opt = this.options()[index];
|
|
1106
|
+
if (!opt)
|
|
1107
|
+
return;
|
|
1108
|
+
this.setValue(opt.value);
|
|
1109
|
+
this.close();
|
|
1110
|
+
this.inputRef()?.nativeElement.focus();
|
|
1111
|
+
}
|
|
1112
|
+
clear() {
|
|
1113
|
+
this.setValue(null);
|
|
1114
|
+
this.inputRef()?.nativeElement.focus();
|
|
1115
|
+
}
|
|
1116
|
+
onInput(event) {
|
|
1117
|
+
this.inputText.set(event.target.value);
|
|
1118
|
+
}
|
|
1119
|
+
onInputKeydown(event) {
|
|
1120
|
+
const e = event;
|
|
1121
|
+
if (this.isDisabled())
|
|
1122
|
+
return;
|
|
1123
|
+
if (!this.open()) {
|
|
1124
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
1125
|
+
e.preventDefault();
|
|
1126
|
+
this.openList();
|
|
1127
|
+
}
|
|
1128
|
+
else if (e.key === 'Enter') {
|
|
1129
|
+
e.preventDefault();
|
|
1130
|
+
this.commitInput();
|
|
1131
|
+
}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
switch (e.key) {
|
|
1135
|
+
case 'ArrowDown':
|
|
1136
|
+
e.preventDefault();
|
|
1137
|
+
this.moveActive(1);
|
|
1138
|
+
break;
|
|
1139
|
+
case 'ArrowUp':
|
|
1140
|
+
e.preventDefault();
|
|
1141
|
+
this.moveActive(-1);
|
|
1142
|
+
break;
|
|
1143
|
+
case 'Home':
|
|
1144
|
+
e.preventDefault();
|
|
1145
|
+
this.activeIndex.set(0);
|
|
1146
|
+
this.scrollActiveIntoView();
|
|
1147
|
+
break;
|
|
1148
|
+
case 'End':
|
|
1149
|
+
e.preventDefault();
|
|
1150
|
+
this.activeIndex.set(this.options().length - 1);
|
|
1151
|
+
this.scrollActiveIntoView();
|
|
1152
|
+
break;
|
|
1153
|
+
case 'Enter':
|
|
1154
|
+
e.preventDefault();
|
|
1155
|
+
if (this.activeIndex() >= 0)
|
|
1156
|
+
this.selectOption(this.activeIndex());
|
|
1157
|
+
else
|
|
1158
|
+
this.commitInput();
|
|
1159
|
+
break;
|
|
1160
|
+
case 'Escape':
|
|
1161
|
+
e.preventDefault();
|
|
1162
|
+
this.close();
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
moveActive(delta) {
|
|
1167
|
+
const len = this.options().length;
|
|
1168
|
+
if (!len)
|
|
1169
|
+
return;
|
|
1170
|
+
const next = (this.activeIndex() + delta + len) % len;
|
|
1171
|
+
this.activeIndex.set(next);
|
|
1172
|
+
this.scrollActiveIntoView();
|
|
1173
|
+
}
|
|
1174
|
+
commitInput() {
|
|
1175
|
+
const text = this.inputText().trim();
|
|
1176
|
+
if (!text) {
|
|
1177
|
+
this.setValue(null);
|
|
1178
|
+
this.close();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const parsed = this.parse(text);
|
|
1182
|
+
if (parsed && this.withinBounds(parsed)) {
|
|
1183
|
+
this.setValue(parsed);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
const v = this.time();
|
|
1187
|
+
this.inputText.set(v ? this.display(v) : '');
|
|
1188
|
+
}
|
|
1189
|
+
this.close();
|
|
1190
|
+
}
|
|
1191
|
+
withinBounds(value) {
|
|
1192
|
+
const min = this.min();
|
|
1193
|
+
const max = this.max();
|
|
1194
|
+
if (min && value < min)
|
|
1195
|
+
return false;
|
|
1196
|
+
if (max && value > max)
|
|
1197
|
+
return false;
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
// --- Value plumbing -------------------------------------------------------
|
|
1201
|
+
setValue(value) {
|
|
1202
|
+
this.time.set(value);
|
|
1203
|
+
this.deriveValue(true);
|
|
1204
|
+
}
|
|
1205
|
+
/** Compute the outward value and push it to the model (+ form when `emit`). */
|
|
1206
|
+
deriveValue(emit) {
|
|
1207
|
+
const hhmm = this.time();
|
|
1208
|
+
const next = hhmm === null ? null : this.valueFormat() === 'date' ? this.toDate(hhmm) : hhmm;
|
|
1209
|
+
// Keep the previous instance when it means the same time, so repeated
|
|
1210
|
+
// edits don't churn `Date` identities (nor the date part they carry).
|
|
1211
|
+
if (!this.sameValue(next, this.lastDerived) || this.value() !== this.lastDerived) {
|
|
1212
|
+
this.lastDerived = next;
|
|
1213
|
+
this.value.set(next);
|
|
1214
|
+
}
|
|
1215
|
+
if (emit)
|
|
1216
|
+
this.onChange(this.lastDerived);
|
|
1217
|
+
}
|
|
1218
|
+
sameValue(a, b) {
|
|
1219
|
+
if (a === b)
|
|
1220
|
+
return true;
|
|
1221
|
+
if (a instanceof Date && b instanceof Date)
|
|
1222
|
+
return a.getTime() === b.getTime();
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Interpret an externally-assigned value (`writeValue` or `[value]`): it
|
|
1227
|
+
* seeds the canonical time and, when it is a `Date`, the day that `date`-mode
|
|
1228
|
+
* values land on.
|
|
1229
|
+
*/
|
|
1230
|
+
applyExternal(v) {
|
|
1231
|
+
this.datePart = v instanceof Date && !Number.isNaN(v.getTime()) ? new Date(v) : null;
|
|
1232
|
+
this.time.set(this.toHhmm(v));
|
|
1233
|
+
}
|
|
1234
|
+
/** Normalise any accepted shape to canonical `HH:mm`, or `null`. */
|
|
1235
|
+
toHhmm(v) {
|
|
1236
|
+
if (v instanceof Date) {
|
|
1237
|
+
return Number.isNaN(v.getTime()) ? null : `${pad2(v.getHours())}:${pad2(v.getMinutes())}`;
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof v !== 'string' || !v.trim())
|
|
1240
|
+
return null;
|
|
1241
|
+
return this.parse(v);
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Put `hh:mm` on a `Date`. The date part comes from the current value when
|
|
1245
|
+
* that is already a `Date` (so repeated edits never drift the day),
|
|
1246
|
+
* otherwise from today — always via the local accessors, never the UTC ones.
|
|
1247
|
+
*/
|
|
1248
|
+
toDate(hhmm) {
|
|
1249
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1250
|
+
const base = this.datePart ? new Date(this.datePart) : new Date();
|
|
1251
|
+
base.setHours(h, m, 0, 0);
|
|
1252
|
+
this.datePart = base;
|
|
1253
|
+
return base;
|
|
1254
|
+
}
|
|
1255
|
+
scrollActiveIntoView() {
|
|
1256
|
+
afterNextRender({
|
|
1257
|
+
write: () => {
|
|
1258
|
+
const el = this.listRef()?.nativeElement.querySelector('.mk-time-picker__option--active');
|
|
1259
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
1260
|
+
},
|
|
1261
|
+
}, { injector: this.injector });
|
|
1262
|
+
}
|
|
1263
|
+
onFocusOut(event) {
|
|
1264
|
+
const related = event.relatedTarget;
|
|
1265
|
+
if (related && this.host.nativeElement.contains(related))
|
|
1266
|
+
return;
|
|
1267
|
+
if (related && this.listRef()?.nativeElement.contains(related))
|
|
1268
|
+
return;
|
|
1269
|
+
this.commitInput();
|
|
1270
|
+
this.onTouched();
|
|
1271
|
+
}
|
|
1272
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
1273
|
+
writeValue(value) {
|
|
1274
|
+
this.applyExternal(value);
|
|
1275
|
+
this.deriveValue(false);
|
|
1276
|
+
}
|
|
1277
|
+
registerOnChange(fn) {
|
|
1278
|
+
this.onChange = fn;
|
|
1279
|
+
}
|
|
1280
|
+
registerOnTouched(fn) {
|
|
1281
|
+
this.onTouched = fn;
|
|
1282
|
+
}
|
|
1283
|
+
setDisabledState(isDisabled) {
|
|
1284
|
+
this.cvaDisabled.set(isDisabled);
|
|
1285
|
+
}
|
|
1286
|
+
// --- Validator ------------------------------------------------------------
|
|
1287
|
+
validatorChange = mkValidatorChange(() => {
|
|
1288
|
+
this.min();
|
|
1289
|
+
this.max();
|
|
1290
|
+
});
|
|
1291
|
+
/**
|
|
1292
|
+
* Reports `mkMinTime` / `mkMaxTime` against the `[min]` and `[max]` inputs.
|
|
1293
|
+
* The control value is normalised to `HH:mm` first (so a `Date` model
|
|
1294
|
+
* validates too); both sides are then zero-padded `HH:mm`, so a
|
|
1295
|
+
* lexicographic compare is a chronological one.
|
|
1296
|
+
*/
|
|
1297
|
+
validate(control) {
|
|
1298
|
+
const v = this.toHhmm(control.value);
|
|
1299
|
+
if (!v)
|
|
1300
|
+
return null;
|
|
1301
|
+
const min = this.min();
|
|
1302
|
+
if (min && v < min)
|
|
1303
|
+
return { mkMinTime: { min, actual: v } };
|
|
1304
|
+
const max = this.max();
|
|
1305
|
+
if (max && v > max)
|
|
1306
|
+
return { mkMaxTime: { max, actual: v } };
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
registerOnValidatorChange(fn) {
|
|
1310
|
+
this.validatorChange.register(fn);
|
|
1311
|
+
}
|
|
1312
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTimePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1313
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkTimePicker, isStandalone: true, selector: "mk-time-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class.mk-time-picker--sm": "effectiveSize() === 'sm'", "class.mk-time-picker--md": "effectiveSize() === 'md'", "class.mk-time-picker--lg": "effectiveSize() === 'lg'", "class.mk-time-picker--open": "open()", "class.mk-time-picker--invalid": "isInvalid()", "class.mk-time-picker--disabled": "isDisabled()" }, classAttribute: "mk-time-picker" }, providers: [
|
|
1314
|
+
{
|
|
1315
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1316
|
+
useExisting: forwardRef(() => MkTimePicker),
|
|
1317
|
+
multi: true,
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
provide: NG_VALIDATORS,
|
|
1321
|
+
useExisting: forwardRef(() => MkTimePicker),
|
|
1322
|
+
multi: true,
|
|
1323
|
+
},
|
|
1324
|
+
], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["textInput"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["list"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #field class=\"mk-time-picker__field\">\n <input\n #textInput\n type=\"text\"\n class=\"mk-time-picker__input\"\n [id]=\"inputId\"\n [value]=\"inputText()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"none\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"\n open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\n \"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n />\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-time-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-time-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-time-picker__toggle\"\n [attr.aria-label]=\"i18n.openTimeList\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-time-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 7v5l3 2\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <ul\n #list\n class=\"mk-time-picker__list\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n [matchWidth]=\"true\"\n (dismiss)=\"close()\"\n [id]=\"listId\"\n role=\"listbox\"\n >\n @for (opt of options(); track opt.value; let i = $index) {\n <li\n class=\"mk-time-picker__option\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [class.mk-time-picker__option--active]=\"i === activeIndex()\"\n [class.mk-time-picker__option--selected]=\"i === selectedIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (mousemove)=\"activeIndex.set(i)\"\n (click)=\"selectOption(i)\"\n >\n {{ opt.label }}\n </li>\n } @empty {\n <li class=\"mk-time-picker__empty\" role=\"presentation\">\n {{ i18n.noOptions }}\n </li>\n }\n </ul>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-time-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-time-picker--sm) .mk-time-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-time-picker--lg) .mk-time-picker__field{height:var(--mk-control-height-lg)}.mk-time-picker__field:hover{border-color:var(--mk-border-strong)}.mk-time-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-time-picker--invalid) .mk-time-picker__field{border-color:var(--mk-danger)}:host(.mk-time-picker--invalid) .mk-time-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-time-picker--disabled) .mk-time-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-time-picker__input{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background:transparent;border:0;outline:none}:host(.mk-time-picker--sm) .mk-time-picker__input{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-time-picker--lg) .mk-time-picker__input{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-time-picker__input::placeholder{color:var(--mk-text-subtle);opacity:1}.mk-time-picker__input:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-time-picker__toggle,.mk-time-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-time-picker__toggle:hover:not(:disabled),.mk-time-picker__clear:hover{color:var(--mk-text)}.mk-time-picker__toggle:focus-visible,.mk-time-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-time-picker__toggle:disabled{cursor:not-allowed}.mk-time-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-time-picker__clear-icon:before,.mk-time-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-time-picker__clear-icon:before{transform:rotate(45deg)}.mk-time-picker__clear-icon:after{transform:rotate(-45deg)}.mk-time-picker__list{padding:var(--mk-space-1);list-style:none;max-height:15rem;overflow-y:auto;background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg)}.mk-time-picker__option{padding:var(--mk-space-2) var(--mk-space-3);font-size:var(--mk-font-size-md);color:var(--mk-text);border-radius:var(--mk-radius-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.mk-time-picker__option--active{background-color:var(--mk-selected-bg)}.mk-time-picker__option--selected{color:var(--mk-selected-text);font-weight:var(--mk-font-weight-medium)}.mk-time-picker__empty{padding:var(--mk-space-2) var(--mk-space-3);color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm)}@media(prefers-reduced-motion:reduce){.mk-time-picker__field,.mk-time-picker__toggle,.mk-time-picker__clear{transition:none}}@media(pointer:coarse){.mk-time-picker__input{font-size:max(var(--mk-font-size-md),16px)}:host(.mk-time-picker--sm) .mk-time-picker__input{font-size:max(var(--mk-font-size-sm),16px)}}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1325
|
+
}
|
|
1326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTimePicker, decorators: [{
|
|
1327
|
+
type: Component,
|
|
1328
|
+
args: [{ selector: 'mk-time-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel], host: {
|
|
1329
|
+
class: 'mk-time-picker',
|
|
1330
|
+
'[class.mk-time-picker--sm]': "effectiveSize() === 'sm'",
|
|
1331
|
+
'[class.mk-time-picker--md]': "effectiveSize() === 'md'",
|
|
1332
|
+
'[class.mk-time-picker--lg]': "effectiveSize() === 'lg'",
|
|
1333
|
+
'[class.mk-time-picker--open]': 'open()',
|
|
1334
|
+
'[class.mk-time-picker--invalid]': 'isInvalid()',
|
|
1335
|
+
'[class.mk-time-picker--disabled]': 'isDisabled()',
|
|
1336
|
+
'(focusout)': 'onFocusOut($event)',
|
|
1337
|
+
}, providers: [
|
|
1338
|
+
{
|
|
1339
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1340
|
+
useExisting: forwardRef(() => MkTimePicker),
|
|
1341
|
+
multi: true,
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
provide: NG_VALIDATORS,
|
|
1345
|
+
useExisting: forwardRef(() => MkTimePicker),
|
|
1346
|
+
multi: true,
|
|
1347
|
+
},
|
|
1348
|
+
], template: "<div #field class=\"mk-time-picker__field\">\n <input\n #textInput\n type=\"text\"\n class=\"mk-time-picker__input\"\n [id]=\"inputId\"\n [value]=\"inputText()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"none\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"\n open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\n \"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n />\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-time-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-time-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-time-picker__toggle\"\n [attr.aria-label]=\"i18n.openTimeList\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-time-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 7v5l3 2\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <ul\n #list\n class=\"mk-time-picker__list\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n [matchWidth]=\"true\"\n (dismiss)=\"close()\"\n [id]=\"listId\"\n role=\"listbox\"\n >\n @for (opt of options(); track opt.value; let i = $index) {\n <li\n class=\"mk-time-picker__option\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"i === selectedIndex()\"\n [class.mk-time-picker__option--active]=\"i === activeIndex()\"\n [class.mk-time-picker__option--selected]=\"i === selectedIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (mousemove)=\"activeIndex.set(i)\"\n (click)=\"selectOption(i)\"\n >\n {{ opt.label }}\n </li>\n } @empty {\n <li class=\"mk-time-picker__empty\" role=\"presentation\">\n {{ i18n.noOptions }}\n </li>\n }\n </ul>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-time-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-time-picker--sm) .mk-time-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-time-picker--lg) .mk-time-picker__field{height:var(--mk-control-height-lg)}.mk-time-picker__field:hover{border-color:var(--mk-border-strong)}.mk-time-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-time-picker--invalid) .mk-time-picker__field{border-color:var(--mk-danger)}:host(.mk-time-picker--invalid) .mk-time-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-time-picker--disabled) .mk-time-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-time-picker__input{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background:transparent;border:0;outline:none}:host(.mk-time-picker--sm) .mk-time-picker__input{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-time-picker--lg) .mk-time-picker__input{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-time-picker__input::placeholder{color:var(--mk-text-subtle);opacity:1}.mk-time-picker__input:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-time-picker__toggle,.mk-time-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-time-picker__toggle:hover:not(:disabled),.mk-time-picker__clear:hover{color:var(--mk-text)}.mk-time-picker__toggle:focus-visible,.mk-time-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-time-picker__toggle:disabled{cursor:not-allowed}.mk-time-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-time-picker__clear-icon:before,.mk-time-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-time-picker__clear-icon:before{transform:rotate(45deg)}.mk-time-picker__clear-icon:after{transform:rotate(-45deg)}.mk-time-picker__list{padding:var(--mk-space-1);list-style:none;max-height:15rem;overflow-y:auto;background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg)}.mk-time-picker__option{padding:var(--mk-space-2) var(--mk-space-3);font-size:var(--mk-font-size-md);color:var(--mk-text);border-radius:var(--mk-radius-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.mk-time-picker__option--active{background-color:var(--mk-selected-bg)}.mk-time-picker__option--selected{color:var(--mk-selected-text);font-weight:var(--mk-font-weight-medium)}.mk-time-picker__empty{padding:var(--mk-space-2) var(--mk-space-3);color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm)}@media(prefers-reduced-motion:reduce){.mk-time-picker__field,.mk-time-picker__toggle,.mk-time-picker__clear{transition:none}}@media(pointer:coarse){.mk-time-picker__input{font-size:max(var(--mk-font-size-md),16px)}:host(.mk-time-picker--sm) .mk-time-picker__input{font-size:max(var(--mk-font-size-sm),16px)}}\n"] }]
|
|
1349
|
+
}], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['textInput', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['list', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* DateRangePicker — select a start + end date from a single calendar popover.
|
|
1353
|
+
* The first click sets the start, the second sets the end (endpoints are
|
|
1354
|
+
* swapped if picked in reverse), and the hovered range is previewed live.
|
|
1355
|
+
* Implements `ControlValueAccessor` and a two-way `value` model of
|
|
1356
|
+
* {@link MkDateRange}.
|
|
1357
|
+
*
|
|
1358
|
+
* ```html
|
|
1359
|
+
* <mk-date-range-picker [(value)]="range" [min]="minDate" clearable />
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
class MkDateRangePicker {
|
|
1363
|
+
i18n = inject(MK_I18N);
|
|
1364
|
+
field = inject(MkFormField, { optional: true });
|
|
1365
|
+
host = inject(ElementRef);
|
|
1366
|
+
injector = inject(Injector);
|
|
1367
|
+
triggerRef = viewChild('trigger', /* @ts-ignore */
|
|
1368
|
+
...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
|
|
1369
|
+
/** The calendar panel — lives in the top layer once opened. */
|
|
1370
|
+
panelRef = viewChild('panel', /* @ts-ignore */
|
|
1371
|
+
...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
|
|
1372
|
+
/** Two-way selected range. */
|
|
1373
|
+
value = model({ start: null, end: null }, /* @ts-ignore */
|
|
1374
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1375
|
+
/** Earliest selectable date (inclusive). */
|
|
1376
|
+
min = input(null, /* @ts-ignore */
|
|
1377
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
1378
|
+
/** Latest selectable date (inclusive). */
|
|
1379
|
+
max = input(null, /* @ts-ignore */
|
|
1380
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
1381
|
+
/** Predicate marking individual days as disabled. */
|
|
1382
|
+
disabledDate = input(null, /* @ts-ignore */
|
|
1383
|
+
...(ngDevMode ? [{ debugName: "disabledDate" }] : /* istanbul ignore next */ []));
|
|
1384
|
+
/** Placeholder shown when no range is selected. */
|
|
1385
|
+
placeholder = input(this.i18n.selectRange, /* @ts-ignore */
|
|
1386
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1387
|
+
/** Pattern used to render each endpoint. */
|
|
1388
|
+
displayFormat = input('MMM d, yyyy', /* @ts-ignore */
|
|
1389
|
+
...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
1390
|
+
/** Disable the control. */
|
|
1391
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1392
|
+
/** Show a clear button when a range is selected. */
|
|
1393
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1394
|
+
/** Force invalid styling + `aria-invalid`. */
|
|
1395
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1396
|
+
/** First column of the calendar week (0 = Sunday). */
|
|
1397
|
+
firstDayOfWeek = input(0, { ...(ngDevMode ? { debugName: "firstDayOfWeek" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1398
|
+
/** Control size. Ignored when nested in an `mk-form-field`. */
|
|
1399
|
+
size = input('md', /* @ts-ignore */
|
|
1400
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1401
|
+
open = signal(false, /* @ts-ignore */
|
|
1402
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1403
|
+
/** In-progress start endpoint (drives calendar highlighting). */
|
|
1404
|
+
selStart = signal(null, /* @ts-ignore */
|
|
1405
|
+
...(ngDevMode ? [{ debugName: "selStart" }] : /* istanbul ignore next */ []));
|
|
1406
|
+
/** In-progress end endpoint. */
|
|
1407
|
+
selEnd = signal(null, /* @ts-ignore */
|
|
1408
|
+
...(ngDevMode ? [{ debugName: "selEnd" }] : /* istanbul ignore next */ []));
|
|
1409
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
1410
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
1411
|
+
onChange = () => { };
|
|
1412
|
+
onTouched = () => { };
|
|
1413
|
+
triggerId = this.field?.controlId ?? mkUniqueId('mk-date-range');
|
|
1414
|
+
panelId = mkUniqueId('mk-date-range-panel');
|
|
1415
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
1416
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
1417
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
1418
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1419
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
1420
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
1421
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
1422
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
1423
|
+
labelledBy = computed(() => this.field?.labelId ?? null, /* @ts-ignore */
|
|
1424
|
+
...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
1425
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
1426
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1427
|
+
showClear = computed(() => {
|
|
1428
|
+
const v = this.value();
|
|
1429
|
+
return this.clearable() && !!(v.start || v.end) && !this.isDisabled();
|
|
1430
|
+
}, /* @ts-ignore */
|
|
1431
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
1432
|
+
/** Display label, e.g. `Jul 1, 2026 – Jul 9, 2026`. */
|
|
1433
|
+
displayLabel = computed(() => {
|
|
1434
|
+
const { start, end } = this.value();
|
|
1435
|
+
const fmt = this.displayFormat();
|
|
1436
|
+
if (!start && !end)
|
|
1437
|
+
return '';
|
|
1438
|
+
const names = this.i18n.dateNames;
|
|
1439
|
+
const s = start ? formatDate(start, fmt, names) : '…';
|
|
1440
|
+
const e = end ? formatDate(end, fmt, names) : '…';
|
|
1441
|
+
return `${s} – ${e}`;
|
|
1442
|
+
}, /* @ts-ignore */
|
|
1443
|
+
...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
|
|
1444
|
+
toggle() {
|
|
1445
|
+
if (this.isDisabled())
|
|
1446
|
+
return;
|
|
1447
|
+
this.open() ? this.close() : this.openPanel();
|
|
1448
|
+
}
|
|
1449
|
+
openPanel() {
|
|
1450
|
+
if (this.isDisabled() || this.open())
|
|
1451
|
+
return;
|
|
1452
|
+
const v = this.value();
|
|
1453
|
+
this.selStart.set(v.start);
|
|
1454
|
+
this.selEnd.set(v.end);
|
|
1455
|
+
this.open.set(true);
|
|
1456
|
+
afterNextRender({
|
|
1457
|
+
write: () => {
|
|
1458
|
+
const el = this.panelRef()?.nativeElement.querySelector('.mk-calendar__day[tabindex="0"]');
|
|
1459
|
+
el?.focus();
|
|
1460
|
+
},
|
|
1461
|
+
}, { injector: this.injector });
|
|
1462
|
+
}
|
|
1463
|
+
close() {
|
|
1464
|
+
if (this.open())
|
|
1465
|
+
this.open.set(false);
|
|
1466
|
+
}
|
|
1467
|
+
onPick(date) {
|
|
1468
|
+
const start = this.selStart();
|
|
1469
|
+
const end = this.selEnd();
|
|
1470
|
+
const day = startOfDay(date);
|
|
1471
|
+
if (!start || (start && end)) {
|
|
1472
|
+
// Begin a fresh range.
|
|
1473
|
+
this.selStart.set(day);
|
|
1474
|
+
this.selEnd.set(null);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
// Complete the range, swapping if picked in reverse.
|
|
1478
|
+
if (isBefore(day, start)) {
|
|
1479
|
+
this.selStart.set(day);
|
|
1480
|
+
this.selEnd.set(start);
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
this.selEnd.set(day);
|
|
1484
|
+
}
|
|
1485
|
+
this.commit();
|
|
1486
|
+
this.close();
|
|
1487
|
+
this.triggerRef()?.nativeElement.focus();
|
|
1488
|
+
}
|
|
1489
|
+
clear() {
|
|
1490
|
+
this.selStart.set(null);
|
|
1491
|
+
this.selEnd.set(null);
|
|
1492
|
+
this.setValue({ start: null, end: null });
|
|
1493
|
+
this.triggerRef()?.nativeElement.focus();
|
|
1494
|
+
}
|
|
1495
|
+
commit() {
|
|
1496
|
+
this.setValue({ start: this.selStart(), end: this.selEnd() });
|
|
1497
|
+
}
|
|
1498
|
+
setValue(range) {
|
|
1499
|
+
this.value.set(range);
|
|
1500
|
+
this.onChange(range);
|
|
1501
|
+
}
|
|
1502
|
+
onFocusOut(event) {
|
|
1503
|
+
const related = event.relatedTarget;
|
|
1504
|
+
if (related && this.host.nativeElement.contains(related))
|
|
1505
|
+
return;
|
|
1506
|
+
if (related && this.panelRef()?.nativeElement.contains(related))
|
|
1507
|
+
return;
|
|
1508
|
+
this.close();
|
|
1509
|
+
this.onTouched();
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Escape pressed inside the (teleported) calendar panel — close and return
|
|
1513
|
+
* focus to the trigger. `preventDefault` + `stopPropagation` keep any outer
|
|
1514
|
+
* Escape handling (e.g. a containing dialog) from also firing.
|
|
1515
|
+
*/
|
|
1516
|
+
onPanelEscape(event) {
|
|
1517
|
+
event.preventDefault();
|
|
1518
|
+
event.stopPropagation();
|
|
1519
|
+
this.close();
|
|
1520
|
+
this.triggerRef()?.nativeElement.focus();
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Focus left the teleported panel. The host `(focusout)` never sees this —
|
|
1524
|
+
* the panel lives under `document.body` — so Tab-out of the calendar is
|
|
1525
|
+
* handled here: close unless focus moved back into the field or the panel.
|
|
1526
|
+
*/
|
|
1527
|
+
onPanelFocusout(event) {
|
|
1528
|
+
const related = event.relatedTarget;
|
|
1529
|
+
// Focus fell to a non-focusable spot (body) — keep open; an outside
|
|
1530
|
+
// pointerdown is dismissed by the anchored panel itself.
|
|
1531
|
+
if (!related)
|
|
1532
|
+
return;
|
|
1533
|
+
if (this.host.nativeElement.contains(related))
|
|
1534
|
+
return;
|
|
1535
|
+
if (this.panelRef()?.nativeElement.contains(related))
|
|
1536
|
+
return;
|
|
1537
|
+
this.close();
|
|
1538
|
+
this.onTouched();
|
|
1539
|
+
}
|
|
1540
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
1541
|
+
writeValue(value) {
|
|
1542
|
+
this.value.set(value ?? { start: null, end: null });
|
|
1543
|
+
}
|
|
1544
|
+
registerOnChange(fn) {
|
|
1545
|
+
this.onChange = fn;
|
|
1546
|
+
}
|
|
1547
|
+
registerOnTouched(fn) {
|
|
1548
|
+
this.onTouched = fn;
|
|
1549
|
+
}
|
|
1550
|
+
setDisabledState(isDisabled) {
|
|
1551
|
+
this.cvaDisabled.set(isDisabled);
|
|
1552
|
+
}
|
|
1553
|
+
// --- Validator ------------------------------------------------------------
|
|
1554
|
+
validatorChange = mkValidatorChange(() => {
|
|
1555
|
+
this.min();
|
|
1556
|
+
this.max();
|
|
1557
|
+
this.disabledDate();
|
|
1558
|
+
});
|
|
1559
|
+
/**
|
|
1560
|
+
* Reports `mkDateRangeIncomplete` for a half-picked range, `mkMinDate` /
|
|
1561
|
+
* `mkMaxDate` for ends outside `[min]`/`[max]`, and `mkDateFilter` for an
|
|
1562
|
+
* end rejected by `[disabledDate]`. A fully empty range passes; compose
|
|
1563
|
+
* with `Validators.required` to reject it.
|
|
1564
|
+
*/
|
|
1565
|
+
validate(control) {
|
|
1566
|
+
const v = control.value;
|
|
1567
|
+
const start = v?.start instanceof Date ? v.start : null;
|
|
1568
|
+
const end = v?.end instanceof Date ? v.end : null;
|
|
1569
|
+
if (!start && !end)
|
|
1570
|
+
return null;
|
|
1571
|
+
if (!start || !end)
|
|
1572
|
+
return { mkDateRangeIncomplete: true };
|
|
1573
|
+
const min = this.min();
|
|
1574
|
+
if (min && isBefore(startOfDay(start), startOfDay(min))) {
|
|
1575
|
+
return { mkMinDate: { min, actual: start } };
|
|
1576
|
+
}
|
|
1577
|
+
const max = this.max();
|
|
1578
|
+
if (max && isAfter(startOfDay(end), startOfDay(max))) {
|
|
1579
|
+
return { mkMaxDate: { max, actual: end } };
|
|
1580
|
+
}
|
|
1581
|
+
const filter = this.disabledDate();
|
|
1582
|
+
if (filter?.(start) || filter?.(end))
|
|
1583
|
+
return { mkDateFilter: true };
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
registerOnValidatorChange(fn) {
|
|
1587
|
+
this.validatorChange.register(fn);
|
|
1588
|
+
}
|
|
1589
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1590
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkDateRangePicker, isStandalone: true, selector: "mk-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, disabledDate: { classPropertyName: "disabledDate", publicName: "disabledDate", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class.mk-date-range-picker--sm": "effectiveSize() === 'sm'", "class.mk-date-range-picker--md": "effectiveSize() === 'md'", "class.mk-date-range-picker--lg": "effectiveSize() === 'lg'", "class.mk-date-range-picker--open": "open()", "class.mk-date-range-picker--invalid": "isInvalid()", "class.mk-date-range-picker--disabled": "isDisabled()" }, classAttribute: "mk-date-range-picker" }, providers: [
|
|
1591
|
+
{
|
|
1592
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1593
|
+
useExisting: forwardRef(() => MkDateRangePicker),
|
|
1594
|
+
multi: true,
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
provide: NG_VALIDATORS,
|
|
1598
|
+
useExisting: forwardRef(() => MkDateRangePicker),
|
|
1599
|
+
multi: true,
|
|
1600
|
+
},
|
|
1601
|
+
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #field class=\"mk-date-range-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-date-range-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-date-range-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-date-range-picker__value mk-date-range-picker__value--placeholder\">{{\n placeholder()\n }}</span>\n }\n </button>\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-date-range-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-date-range-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-date-range-picker__toggle\"\n [attr.aria-label]=\"i18n.openCalendar\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-date-range-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-date-range-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseDateRange\"\n (keydown.escape)=\"onPanelEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <mk-calendar\n [rangeMode]=\"true\"\n [rangeStart]=\"selStart()\"\n [rangeEnd]=\"selEnd()\"\n (dateSelected)=\"onPick($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [disabledDate]=\"disabledDate()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-date-range-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-date-range-picker--sm) .mk-date-range-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-date-range-picker--lg) .mk-date-range-picker__field{height:var(--mk-control-height-lg)}.mk-date-range-picker__field:hover{border-color:var(--mk-border-strong)}.mk-date-range-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-date-range-picker--invalid) .mk-date-range-picker__field{border-color:var(--mk-danger)}:host(.mk-date-range-picker--invalid) .mk-date-range-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-date-range-picker--disabled) .mk-date-range-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-date-range-picker__trigger{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);text-align:start;background:transparent;border:0;outline:none;cursor:pointer}:host(.mk-date-range-picker--sm) .mk-date-range-picker__trigger{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-date-range-picker--lg) .mk-date-range-picker__trigger{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-date-range-picker__trigger:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-date-range-picker__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-date-range-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-date-range-picker__toggle,.mk-date-range-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-date-range-picker__toggle:hover:not(:disabled),.mk-date-range-picker__clear:hover{color:var(--mk-text)}.mk-date-range-picker__toggle:focus-visible,.mk-date-range-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-date-range-picker__toggle:disabled{cursor:not-allowed}.mk-date-range-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-date-range-picker__clear-icon:before,.mk-date-range-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-date-range-picker__clear-icon:before{transform:rotate(45deg)}.mk-date-range-picker__clear-icon:after{transform:rotate(-45deg)}.mk-date-range-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-date-range-picker__field,.mk-date-range-picker__toggle,.mk-date-range-picker__clear{transition:none}}\n"], dependencies: [{ kind: "component", type: MkCalendar, selector: "mk-calendar", inputs: ["value", "min", "max", "firstDayOfWeek", "disabled", "invalid", "disabledDate", "size", "fullWidth", "rangeMode", "rangeStart", "rangeEnd"], outputs: ["valueChange", "dateSelected", "dateHovered"] }, { kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1602
|
+
}
|
|
1603
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDateRangePicker, decorators: [{
|
|
1604
|
+
type: Component,
|
|
1605
|
+
args: [{ selector: 'mk-date-range-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkCalendar, MkAnchoredPanel], host: {
|
|
1606
|
+
class: 'mk-date-range-picker',
|
|
1607
|
+
'[class.mk-date-range-picker--sm]': "effectiveSize() === 'sm'",
|
|
1608
|
+
'[class.mk-date-range-picker--md]': "effectiveSize() === 'md'",
|
|
1609
|
+
'[class.mk-date-range-picker--lg]': "effectiveSize() === 'lg'",
|
|
1610
|
+
'[class.mk-date-range-picker--open]': 'open()',
|
|
1611
|
+
'[class.mk-date-range-picker--invalid]': 'isInvalid()',
|
|
1612
|
+
'[class.mk-date-range-picker--disabled]': 'isDisabled()',
|
|
1613
|
+
'(focusout)': 'onFocusOut($event)',
|
|
1614
|
+
}, providers: [
|
|
1615
|
+
{
|
|
1616
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1617
|
+
useExisting: forwardRef(() => MkDateRangePicker),
|
|
1618
|
+
multi: true,
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
provide: NG_VALIDATORS,
|
|
1622
|
+
useExisting: forwardRef(() => MkDateRangePicker),
|
|
1623
|
+
multi: true,
|
|
1624
|
+
},
|
|
1625
|
+
], template: "<div #field class=\"mk-date-range-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-date-range-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-date-range-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-date-range-picker__value mk-date-range-picker__value--placeholder\">{{\n placeholder()\n }}</span>\n }\n </button>\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-date-range-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-date-range-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"mk-date-range-picker__toggle\"\n [attr.aria-label]=\"i18n.openCalendar\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n <svg\n class=\"mk-date-range-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-date-range-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseDateRange\"\n (keydown.escape)=\"onPanelEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <mk-calendar\n [rangeMode]=\"true\"\n [rangeStart]=\"selStart()\"\n [rangeEnd]=\"selEnd()\"\n (dateSelected)=\"onPick($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [disabledDate]=\"disabledDate()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-date-range-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-date-range-picker--sm) .mk-date-range-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-date-range-picker--lg) .mk-date-range-picker__field{height:var(--mk-control-height-lg)}.mk-date-range-picker__field:hover{border-color:var(--mk-border-strong)}.mk-date-range-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-date-range-picker--invalid) .mk-date-range-picker__field{border-color:var(--mk-danger)}:host(.mk-date-range-picker--invalid) .mk-date-range-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-date-range-picker--disabled) .mk-date-range-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-date-range-picker__trigger{flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);text-align:start;background:transparent;border:0;outline:none;cursor:pointer}:host(.mk-date-range-picker--sm) .mk-date-range-picker__trigger{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-date-range-picker--lg) .mk-date-range-picker__trigger{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-date-range-picker__trigger:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-date-range-picker__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-date-range-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-date-range-picker__toggle,.mk-date-range-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-date-range-picker__toggle:hover:not(:disabled),.mk-date-range-picker__clear:hover{color:var(--mk-text)}.mk-date-range-picker__toggle:focus-visible,.mk-date-range-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-date-range-picker__toggle:disabled{cursor:not-allowed}.mk-date-range-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-date-range-picker__clear-icon:before,.mk-date-range-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-date-range-picker__clear-icon:before{transform:rotate(45deg)}.mk-date-range-picker__clear-icon:after{transform:rotate(-45deg)}.mk-date-range-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-date-range-picker__field,.mk-date-range-picker__toggle,.mk-date-range-picker__clear{transition:none}}\n"] }]
|
|
1626
|
+
}], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], disabledDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* MonthPicker — a compact field + popover for picking a **month** (`MMM yyyy`)
|
|
1630
|
+
* or a **year**. The panel shows a 12-month grid (with year nav) or a 12-year
|
|
1631
|
+
* grid (with decade nav). Value is a `Date` at the first day of the selection.
|
|
1632
|
+
* Implements `ControlValueAccessor` + a two-way `value` model; the panel renders
|
|
1633
|
+
* in the top layer so it is never clipped.
|
|
1634
|
+
*
|
|
1635
|
+
* ```html
|
|
1636
|
+
* <mk-month-picker [(value)]="month" />
|
|
1637
|
+
* <mk-month-picker mode="year" [(value)]="year" />
|
|
1638
|
+
* ```
|
|
1639
|
+
*/
|
|
1640
|
+
class MkMonthPicker {
|
|
1641
|
+
i18n = inject(MK_I18N);
|
|
1642
|
+
field = inject(MkFormField, { optional: true });
|
|
1643
|
+
host = inject(ElementRef);
|
|
1644
|
+
injector = inject(Injector);
|
|
1645
|
+
triggerRef = viewChild('trigger', /* @ts-ignore */
|
|
1646
|
+
...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
|
|
1647
|
+
panelRef = viewChild('panel', /* @ts-ignore */
|
|
1648
|
+
...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
|
|
1649
|
+
cellRefs = viewChildren('cell', /* @ts-ignore */
|
|
1650
|
+
...(ngDevMode ? [{ debugName: "cellRefs" }] : /* istanbul ignore next */ []));
|
|
1651
|
+
/** Two-way selected date (first of the month / year). */
|
|
1652
|
+
value = model(null, /* @ts-ignore */
|
|
1653
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1654
|
+
/** Pick a month or a whole year. */
|
|
1655
|
+
mode = input('month', /* @ts-ignore */
|
|
1656
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1657
|
+
/** Earliest selectable date. */
|
|
1658
|
+
min = input(null, /* @ts-ignore */
|
|
1659
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
1660
|
+
/** Latest selectable date. */
|
|
1661
|
+
max = input(null, /* @ts-ignore */
|
|
1662
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
1663
|
+
/** Placeholder shown when empty. */
|
|
1664
|
+
placeholder = input('', /* @ts-ignore */
|
|
1665
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1666
|
+
/** Display pattern (defaults to `MMM yyyy` / `yyyy`). */
|
|
1667
|
+
displayFormat = input('', /* @ts-ignore */
|
|
1668
|
+
...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
1669
|
+
/** Disable the control. */
|
|
1670
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1671
|
+
/** Show a clear button when a value is selected. */
|
|
1672
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1673
|
+
/** Force invalid styling. */
|
|
1674
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1675
|
+
/** Control size. Ignored when nested in an `mk-form-field`. */
|
|
1676
|
+
size = input('md', /* @ts-ignore */
|
|
1677
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1678
|
+
open = signal(false, /* @ts-ignore */
|
|
1679
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1680
|
+
/** The year currently shown in the panel (base for the month/decade grid). */
|
|
1681
|
+
viewYear = signal(new Date().getFullYear(), /* @ts-ignore */
|
|
1682
|
+
...(ngDevMode ? [{ debugName: "viewYear" }] : /* istanbul ignore next */ []));
|
|
1683
|
+
activeIndex = signal(0, /* @ts-ignore */
|
|
1684
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
1685
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
1686
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
1687
|
+
onChange = () => { };
|
|
1688
|
+
onTouched = () => { };
|
|
1689
|
+
triggerId = this.field?.controlId ?? mkUniqueId('mk-month-picker');
|
|
1690
|
+
panelId = mkUniqueId('mk-month-picker-panel');
|
|
1691
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
1692
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
1693
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
1694
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1695
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
1696
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
1697
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
1698
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
1699
|
+
labelledBy = computed(() => this.field?.labelId ?? null, /* @ts-ignore */
|
|
1700
|
+
...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
1701
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
1702
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1703
|
+
resolvedPlaceholder = computed(() => this.placeholder() ||
|
|
1704
|
+
(this.mode() === 'year' ? this.i18n.selectYear : this.i18n.selectMonth), /* @ts-ignore */
|
|
1705
|
+
...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
1706
|
+
pattern = computed(() => this.displayFormat() || (this.mode() === 'year' ? 'yyyy' : 'MMM yyyy'), /* @ts-ignore */
|
|
1707
|
+
...(ngDevMode ? [{ debugName: "pattern" }] : /* istanbul ignore next */ []));
|
|
1708
|
+
displayLabel = computed(() => {
|
|
1709
|
+
const v = this.value();
|
|
1710
|
+
return v ? formatDate(v, this.pattern(), this.i18n.dateNames) : '';
|
|
1711
|
+
}, /* @ts-ignore */
|
|
1712
|
+
...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
|
|
1713
|
+
/** The decade start when in year mode. */
|
|
1714
|
+
decadeStart = computed(() => Math.floor(this.viewYear() / 12) * 12, /* @ts-ignore */
|
|
1715
|
+
...(ngDevMode ? [{ debugName: "decadeStart" }] : /* istanbul ignore next */ []));
|
|
1716
|
+
/** Header label (year, or decade range). */
|
|
1717
|
+
headerLabel = computed(() => {
|
|
1718
|
+
if (this.mode() === 'year') {
|
|
1719
|
+
const s = this.decadeStart();
|
|
1720
|
+
return `${s} – ${s + 11}`;
|
|
1721
|
+
}
|
|
1722
|
+
return `${this.viewYear()}`;
|
|
1723
|
+
}, /* @ts-ignore */
|
|
1724
|
+
...(ngDevMode ? [{ debugName: "headerLabel" }] : /* istanbul ignore next */ []));
|
|
1725
|
+
/** Abbreviated month labels for the grid, from the active locale. */
|
|
1726
|
+
months = this.i18n.dateNames.monthsShort;
|
|
1727
|
+
/** The 12 grid cells (months or years). */
|
|
1728
|
+
cells = computed(() => {
|
|
1729
|
+
const value = this.value();
|
|
1730
|
+
if (this.mode() === 'year') {
|
|
1731
|
+
const start = this.decadeStart();
|
|
1732
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
1733
|
+
const year = start + i;
|
|
1734
|
+
const date = new Date(year, 0, 1);
|
|
1735
|
+
return {
|
|
1736
|
+
label: `${year}`,
|
|
1737
|
+
year,
|
|
1738
|
+
month: 0,
|
|
1739
|
+
disabled: this.outOfRange(date, new Date(year, 11, 31)),
|
|
1740
|
+
selected: value?.getFullYear() === year,
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
const year = this.viewYear();
|
|
1745
|
+
return this.months.map((label, m) => {
|
|
1746
|
+
const date = new Date(year, m, 1);
|
|
1747
|
+
return {
|
|
1748
|
+
label,
|
|
1749
|
+
year,
|
|
1750
|
+
month: m,
|
|
1751
|
+
disabled: this.outOfRange(date, endOfMonth(date)),
|
|
1752
|
+
selected: value?.getFullYear() === year && value?.getMonth() === m,
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
}, /* @ts-ignore */
|
|
1756
|
+
...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
|
|
1757
|
+
constructor() {
|
|
1758
|
+
// Keep the panel's view year in sync with the selected value.
|
|
1759
|
+
effect(() => {
|
|
1760
|
+
const v = this.value();
|
|
1761
|
+
if (v)
|
|
1762
|
+
this.viewYear.set(v.getFullYear());
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
toggle() {
|
|
1766
|
+
if (this.isDisabled())
|
|
1767
|
+
return;
|
|
1768
|
+
this.open() ? this.close() : this.openPanel();
|
|
1769
|
+
}
|
|
1770
|
+
openPanel() {
|
|
1771
|
+
if (this.isDisabled() || this.open())
|
|
1772
|
+
return;
|
|
1773
|
+
const v = this.value();
|
|
1774
|
+
if (v)
|
|
1775
|
+
this.viewYear.set(v.getFullYear());
|
|
1776
|
+
// Focus the selected (or first enabled) cell once the panel is in the DOM.
|
|
1777
|
+
const cells = this.cells();
|
|
1778
|
+
const sel = cells.findIndex((c) => c.selected);
|
|
1779
|
+
this.activeIndex.set(sel >= 0 ? sel : cells.findIndex((c) => !c.disabled));
|
|
1780
|
+
this.open.set(true);
|
|
1781
|
+
afterNextRender(() => this.focusActive(), { injector: this.injector });
|
|
1782
|
+
}
|
|
1783
|
+
close(restoreFocus = false) {
|
|
1784
|
+
if (!this.open())
|
|
1785
|
+
return;
|
|
1786
|
+
this.open.set(false);
|
|
1787
|
+
if (restoreFocus)
|
|
1788
|
+
this.triggerRef()?.nativeElement.focus();
|
|
1789
|
+
}
|
|
1790
|
+
shiftView(delta) {
|
|
1791
|
+
const step = this.mode() === 'year' ? 12 : 1;
|
|
1792
|
+
this.viewYear.set(this.viewYear() + delta * step);
|
|
1793
|
+
}
|
|
1794
|
+
selectCell(cell) {
|
|
1795
|
+
if (cell.disabled)
|
|
1796
|
+
return;
|
|
1797
|
+
const date = new Date(cell.year, this.mode() === 'year' ? 0 : cell.month, 1);
|
|
1798
|
+
this.value.set(date);
|
|
1799
|
+
this.onChange(date);
|
|
1800
|
+
this.close(true);
|
|
1801
|
+
}
|
|
1802
|
+
clear(event) {
|
|
1803
|
+
event.stopPropagation();
|
|
1804
|
+
this.value.set(null);
|
|
1805
|
+
this.onChange(null);
|
|
1806
|
+
this.triggerRef()?.nativeElement.focus();
|
|
1807
|
+
}
|
|
1808
|
+
onGridKeydown(event) {
|
|
1809
|
+
const cols = 3;
|
|
1810
|
+
const total = this.cells().length;
|
|
1811
|
+
let next = this.activeIndex();
|
|
1812
|
+
switch (event.key) {
|
|
1813
|
+
case 'ArrowRight':
|
|
1814
|
+
next += 1;
|
|
1815
|
+
break;
|
|
1816
|
+
case 'ArrowLeft':
|
|
1817
|
+
next -= 1;
|
|
1818
|
+
break;
|
|
1819
|
+
case 'ArrowDown':
|
|
1820
|
+
next += cols;
|
|
1821
|
+
break;
|
|
1822
|
+
case 'ArrowUp':
|
|
1823
|
+
next -= cols;
|
|
1824
|
+
break;
|
|
1825
|
+
case 'Home':
|
|
1826
|
+
next = 0;
|
|
1827
|
+
break;
|
|
1828
|
+
case 'End':
|
|
1829
|
+
next = total - 1;
|
|
1830
|
+
break;
|
|
1831
|
+
case 'Escape':
|
|
1832
|
+
event.preventDefault();
|
|
1833
|
+
this.close(true);
|
|
1834
|
+
return;
|
|
1835
|
+
default:
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
event.preventDefault();
|
|
1839
|
+
this.activeIndex.set(Math.max(0, Math.min(total - 1, next)));
|
|
1840
|
+
this.focusActive();
|
|
1841
|
+
}
|
|
1842
|
+
focusActive() {
|
|
1843
|
+
this.cellRefs()[this.activeIndex()]?.nativeElement.focus();
|
|
1844
|
+
}
|
|
1845
|
+
onFocusOut(event) {
|
|
1846
|
+
const related = event.relatedTarget;
|
|
1847
|
+
if (related && this.host.nativeElement.contains(related))
|
|
1848
|
+
return;
|
|
1849
|
+
if (related && this.panelRef()?.nativeElement.contains(related))
|
|
1850
|
+
return;
|
|
1851
|
+
this.close();
|
|
1852
|
+
this.onTouched();
|
|
1853
|
+
}
|
|
1854
|
+
outOfRange(start, end) {
|
|
1855
|
+
const min = this.min();
|
|
1856
|
+
const max = this.max();
|
|
1857
|
+
if (min && isBefore(end, min))
|
|
1858
|
+
return true;
|
|
1859
|
+
if (max && isAfter(start, max))
|
|
1860
|
+
return true;
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
1864
|
+
writeValue(value) {
|
|
1865
|
+
this.value.set(value ?? null);
|
|
1866
|
+
}
|
|
1867
|
+
registerOnChange(fn) {
|
|
1868
|
+
this.onChange = fn;
|
|
1869
|
+
}
|
|
1870
|
+
registerOnTouched(fn) {
|
|
1871
|
+
this.onTouched = fn;
|
|
1872
|
+
}
|
|
1873
|
+
setDisabledState(isDisabled) {
|
|
1874
|
+
this.cvaDisabled.set(isDisabled);
|
|
1875
|
+
}
|
|
1876
|
+
// --- Validator ------------------------------------------------------------
|
|
1877
|
+
validatorChange = mkValidatorChange(() => {
|
|
1878
|
+
this.min();
|
|
1879
|
+
this.max();
|
|
1880
|
+
});
|
|
1881
|
+
/**
|
|
1882
|
+
* Reports `mkMinDate` / `mkMaxDate` against the `[min]` and `[max]` inputs,
|
|
1883
|
+
* compared at month granularity.
|
|
1884
|
+
*/
|
|
1885
|
+
validate(control) {
|
|
1886
|
+
const v = control.value;
|
|
1887
|
+
if (!(v instanceof Date) || Number.isNaN(v.getTime()))
|
|
1888
|
+
return null;
|
|
1889
|
+
const month = startOfMonth(v);
|
|
1890
|
+
const min = this.min();
|
|
1891
|
+
if (min && isBefore(month, startOfMonth(min)))
|
|
1892
|
+
return { mkMinDate: { min, actual: v } };
|
|
1893
|
+
const max = this.max();
|
|
1894
|
+
if (max && isAfter(month, startOfMonth(max)))
|
|
1895
|
+
return { mkMaxDate: { max, actual: v } };
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
registerOnValidatorChange(fn) {
|
|
1899
|
+
this.validatorChange.register(fn);
|
|
1900
|
+
}
|
|
1901
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMonthPicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1902
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkMonthPicker, isStandalone: true, selector: "mk-month-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class.mk-month-picker--sm": "effectiveSize() === 'sm'", "class.mk-month-picker--lg": "effectiveSize() === 'lg'", "class.mk-month-picker--open": "open()", "class.mk-month-picker--invalid": "isInvalid()", "class.mk-month-picker--disabled": "isDisabled()" }, classAttribute: "mk-month-picker" }, providers: [
|
|
1903
|
+
{
|
|
1904
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1905
|
+
useExisting: forwardRef(() => MkMonthPicker),
|
|
1906
|
+
multi: true,
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
provide: NG_VALIDATORS,
|
|
1910
|
+
useExisting: forwardRef(() => MkMonthPicker),
|
|
1911
|
+
multi: true,
|
|
1912
|
+
},
|
|
1913
|
+
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "cellRefs", predicate: ["cell"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #field class=\"mk-month-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-month-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-month-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-month-picker__value mk-month-picker__value--placeholder\">{{\n resolvedPlaceholder()\n }}</span>\n }\n <span class=\"mk-month-picker__icon\" aria-hidden=\"true\"></span>\n </button>\n\n @if (clearable() && value() && !isDisabled()) {\n <button\n type=\"button\"\n class=\"mk-month-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear($event)\"\n >\n <span class=\"mk-month-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-month-picker__panel\"\n role=\"dialog\"\n [id]=\"panelId\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.chooseYear : i18n.chooseMonth\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n >\n <div class=\"mk-month-picker__header\">\n <button\n type=\"button\"\n class=\"mk-month-picker__nav\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.previousYears : i18n.previousYear\"\n (click)=\"shiftView(-1)\"\n >\n \u2039\n </button>\n <span class=\"mk-month-picker__header-label\" aria-live=\"polite\">{{ headerLabel() }}</span>\n <button\n type=\"button\"\n class=\"mk-month-picker__nav\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.nextYears : i18n.nextYear\"\n (click)=\"shiftView(1)\"\n >\n \u203A\n </button>\n </div>\n\n <div class=\"mk-month-picker__grid\" role=\"grid\" (keydown)=\"onGridKeydown($event)\">\n @for (cell of cells(); track cell.label; let i = $index) {\n <button\n #cell\n type=\"button\"\n class=\"mk-month-picker__cell\"\n [class.mk-month-picker__cell--selected]=\"cell.selected\"\n [attr.tabindex]=\"i === activeIndex() ? 0 : -1\"\n [attr.aria-selected]=\"cell.selected\"\n [disabled]=\"cell.disabled\"\n (click)=\"selectCell(cell)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-month-picker__field{display:flex;align-items:center;position:relative}.mk-month-picker__trigger{flex:1 1 auto;display:flex;align-items:center;gap:var(--mk-space-2);width:100%;height:var(--mk-control-height-md);padding:0 var(--mk-space-3);text-align:start;font:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);cursor:pointer}:host(.mk-month-picker--sm) .mk-month-picker__trigger{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm);border-radius:var(--mk-radius-sm)}:host(.mk-month-picker--lg) .mk-month-picker__trigger{height:var(--mk-control-height-lg);padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-month-picker__trigger:hover:not(:disabled){border-color:var(--mk-border-strong)}.mk-month-picker__trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-month-picker--invalid) .mk-month-picker__trigger{border-color:var(--mk-danger)}:host(.mk-month-picker--disabled) .mk-month-picker__trigger{background-color:var(--mk-surface-2);color:var(--mk-text-disabled);cursor:not-allowed;opacity:.7}.mk-month-picker__value{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-month-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-month-picker__icon{flex:none;width:1rem;height:1rem;border:2px solid var(--mk-text-muted);border-radius:2px;position:relative}.mk-month-picker__icon:before{content:\"\";position:absolute;top:-3px;left:2px;right:2px;border-top:2px solid var(--mk-text-muted)}.mk-month-picker__clear{position:absolute;inset-inline-end:var(--mk-space-8);display:grid;place-items:center;width:1.25rem;height:1.25rem;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-circle);cursor:pointer}.mk-month-picker__clear:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-month-picker__clear-icon:before,.mk-month-picker__clear-icon:after{content:\"\";position:absolute;width:.7rem;height:2px;background-color:currentColor}.mk-month-picker__clear-icon:before{transform:rotate(45deg)}.mk-month-picker__clear-icon:after{transform:rotate(-45deg)}.mk-month-picker__panel{width:17rem;padding:var(--mk-space-2);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg)}.mk-month-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1)}.mk-month-picker__header-label{font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-month-picker__nav{display:grid;place-items:center;width:2rem;height:2rem;font-size:var(--mk-font-size-lg);color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-month-picker__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-month-picker__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-month-picker__grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--mk-space-1)}.mk-month-picker__cell{padding:var(--mk-space-2) 0;font-size:var(--mk-font-size-sm);color:var(--mk-text);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-month-picker__cell:hover:not(:disabled){background-color:var(--mk-hover-overlay)}.mk-month-picker__cell:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-month-picker__cell--selected{background-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-month-picker__cell--selected:hover:not(:disabled){background-color:var(--mk-primary)}.mk-month-picker__cell:disabled{color:var(--mk-text-disabled);cursor:not-allowed}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1914
|
+
}
|
|
1915
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMonthPicker, decorators: [{
|
|
1916
|
+
type: Component,
|
|
1917
|
+
args: [{ selector: 'mk-month-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel], host: {
|
|
1918
|
+
class: 'mk-month-picker',
|
|
1919
|
+
'[class.mk-month-picker--sm]': "effectiveSize() === 'sm'",
|
|
1920
|
+
'[class.mk-month-picker--lg]': "effectiveSize() === 'lg'",
|
|
1921
|
+
'[class.mk-month-picker--open]': 'open()',
|
|
1922
|
+
'[class.mk-month-picker--invalid]': 'isInvalid()',
|
|
1923
|
+
'[class.mk-month-picker--disabled]': 'isDisabled()',
|
|
1924
|
+
'(focusout)': 'onFocusOut($event)',
|
|
1925
|
+
}, providers: [
|
|
1926
|
+
{
|
|
1927
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1928
|
+
useExisting: forwardRef(() => MkMonthPicker),
|
|
1929
|
+
multi: true,
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
provide: NG_VALIDATORS,
|
|
1933
|
+
useExisting: forwardRef(() => MkMonthPicker),
|
|
1934
|
+
multi: true,
|
|
1935
|
+
},
|
|
1936
|
+
], template: "<div #field class=\"mk-month-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-month-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-month-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-month-picker__value mk-month-picker__value--placeholder\">{{\n resolvedPlaceholder()\n }}</span>\n }\n <span class=\"mk-month-picker__icon\" aria-hidden=\"true\"></span>\n </button>\n\n @if (clearable() && value() && !isDisabled()) {\n <button\n type=\"button\"\n class=\"mk-month-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear($event)\"\n >\n <span class=\"mk-month-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-month-picker__panel\"\n role=\"dialog\"\n [id]=\"panelId\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.chooseYear : i18n.chooseMonth\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n >\n <div class=\"mk-month-picker__header\">\n <button\n type=\"button\"\n class=\"mk-month-picker__nav\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.previousYears : i18n.previousYear\"\n (click)=\"shiftView(-1)\"\n >\n \u2039\n </button>\n <span class=\"mk-month-picker__header-label\" aria-live=\"polite\">{{ headerLabel() }}</span>\n <button\n type=\"button\"\n class=\"mk-month-picker__nav\"\n [attr.aria-label]=\"mode() === 'year' ? i18n.nextYears : i18n.nextYear\"\n (click)=\"shiftView(1)\"\n >\n \u203A\n </button>\n </div>\n\n <div class=\"mk-month-picker__grid\" role=\"grid\" (keydown)=\"onGridKeydown($event)\">\n @for (cell of cells(); track cell.label; let i = $index) {\n <button\n #cell\n type=\"button\"\n class=\"mk-month-picker__cell\"\n [class.mk-month-picker__cell--selected]=\"cell.selected\"\n [attr.tabindex]=\"i === activeIndex() ? 0 : -1\"\n [attr.aria-selected]=\"cell.selected\"\n [disabled]=\"cell.disabled\"\n (click)=\"selectCell(cell)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-month-picker__field{display:flex;align-items:center;position:relative}.mk-month-picker__trigger{flex:1 1 auto;display:flex;align-items:center;gap:var(--mk-space-2);width:100%;height:var(--mk-control-height-md);padding:0 var(--mk-space-3);text-align:start;font:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);cursor:pointer}:host(.mk-month-picker--sm) .mk-month-picker__trigger{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm);border-radius:var(--mk-radius-sm)}:host(.mk-month-picker--lg) .mk-month-picker__trigger{height:var(--mk-control-height-lg);padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-month-picker__trigger:hover:not(:disabled){border-color:var(--mk-border-strong)}.mk-month-picker__trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-month-picker--invalid) .mk-month-picker__trigger{border-color:var(--mk-danger)}:host(.mk-month-picker--disabled) .mk-month-picker__trigger{background-color:var(--mk-surface-2);color:var(--mk-text-disabled);cursor:not-allowed;opacity:.7}.mk-month-picker__value{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-month-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-month-picker__icon{flex:none;width:1rem;height:1rem;border:2px solid var(--mk-text-muted);border-radius:2px;position:relative}.mk-month-picker__icon:before{content:\"\";position:absolute;top:-3px;left:2px;right:2px;border-top:2px solid var(--mk-text-muted)}.mk-month-picker__clear{position:absolute;inset-inline-end:var(--mk-space-8);display:grid;place-items:center;width:1.25rem;height:1.25rem;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-circle);cursor:pointer}.mk-month-picker__clear:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-month-picker__clear-icon:before,.mk-month-picker__clear-icon:after{content:\"\";position:absolute;width:.7rem;height:2px;background-color:currentColor}.mk-month-picker__clear-icon:before{transform:rotate(45deg)}.mk-month-picker__clear-icon:after{transform:rotate(-45deg)}.mk-month-picker__panel{width:17rem;padding:var(--mk-space-2);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg)}.mk-month-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1)}.mk-month-picker__header-label{font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-month-picker__nav{display:grid;place-items:center;width:2rem;height:2rem;font-size:var(--mk-font-size-lg);color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-month-picker__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-month-picker__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-month-picker__grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--mk-space-1)}.mk-month-picker__cell{padding:var(--mk-space-2) 0;font-size:var(--mk-font-size-sm);color:var(--mk-text);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-month-picker__cell:hover:not(:disabled){background-color:var(--mk-hover-overlay)}.mk-month-picker__cell:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-month-picker__cell--selected{background-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-month-picker__cell--selected:hover:not(:disabled){background-color:var(--mk-primary)}.mk-month-picker__cell:disabled{color:var(--mk-text-disabled);cursor:not-allowed}\n"] }]
|
|
1937
|
+
}], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], cellRefs: [{ type: i0.ViewChildren, args: ['cell', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* WeekPicker — pick a whole calendar week from a popover calendar. Hovering a
|
|
1941
|
+
* day previews its entire week; clicking selects it. The value is an
|
|
1942
|
+
* {@link MkWeek} (`{ start, end }`, aligned to `firstDayOfWeek`). Implements
|
|
1943
|
+
* `ControlValueAccessor` and a two-way `value` model; the panel renders in the
|
|
1944
|
+
* top layer so it is never clipped.
|
|
1945
|
+
*
|
|
1946
|
+
* ```html
|
|
1947
|
+
* <mk-week-picker [(value)]="week" [firstDayOfWeek]="1" clearable />
|
|
1948
|
+
* ```
|
|
1949
|
+
*/
|
|
1950
|
+
class MkWeekPicker {
|
|
1951
|
+
i18n = inject(MK_I18N);
|
|
1952
|
+
field = inject(MkFormField, { optional: true });
|
|
1953
|
+
host = inject(ElementRef);
|
|
1954
|
+
injector = inject(Injector);
|
|
1955
|
+
triggerRef = viewChild('trigger', /* @ts-ignore */
|
|
1956
|
+
...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
|
|
1957
|
+
panelRef = viewChild('panel', /* @ts-ignore */
|
|
1958
|
+
...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
|
|
1959
|
+
/** Two-way selected week. */
|
|
1960
|
+
value = model(null, /* @ts-ignore */
|
|
1961
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1962
|
+
/** Earliest selectable date (inclusive). */
|
|
1963
|
+
min = input(null, /* @ts-ignore */
|
|
1964
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
1965
|
+
/** Latest selectable date (inclusive). */
|
|
1966
|
+
max = input(null, /* @ts-ignore */
|
|
1967
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
1968
|
+
/** First column of the calendar week (0 = Sunday). Also anchors the week. */
|
|
1969
|
+
firstDayOfWeek = input(0, { ...(ngDevMode ? { debugName: "firstDayOfWeek" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1970
|
+
/** Placeholder shown when empty. */
|
|
1971
|
+
placeholder = input(this.i18n.selectWeek, /* @ts-ignore */
|
|
1972
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1973
|
+
/** Pattern used to render each endpoint of the week range. */
|
|
1974
|
+
displayFormat = input('MMM d', /* @ts-ignore */
|
|
1975
|
+
...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
1976
|
+
/** Prefix the label with the ISO week number, e.g. `W28 · …`. */
|
|
1977
|
+
showWeekNumber = input(false, { ...(ngDevMode ? { debugName: "showWeekNumber" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1978
|
+
/** Disable the control. */
|
|
1979
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1980
|
+
/** Show a clear button when a week is selected. */
|
|
1981
|
+
clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1982
|
+
/** Force invalid styling + `aria-invalid`. */
|
|
1983
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1984
|
+
/** Control size. Ignored when nested in an `mk-form-field`. */
|
|
1985
|
+
size = input('md', /* @ts-ignore */
|
|
1986
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1987
|
+
open = signal(false, /* @ts-ignore */
|
|
1988
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1989
|
+
/** The week currently previewed under the pointer (drives highlighting). */
|
|
1990
|
+
hoverWeek = signal(null, /* @ts-ignore */
|
|
1991
|
+
...(ngDevMode ? [{ debugName: "hoverWeek" }] : /* istanbul ignore next */ []));
|
|
1992
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
1993
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
1994
|
+
onChange = () => { };
|
|
1995
|
+
onTouched = () => { };
|
|
1996
|
+
triggerId = this.field?.controlId ?? mkUniqueId('mk-week-picker');
|
|
1997
|
+
panelId = mkUniqueId('mk-week-picker-panel');
|
|
1998
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
1999
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
2000
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
2001
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
2002
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
2003
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
2004
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
2005
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
2006
|
+
labelledBy = computed(() => this.field?.labelId ?? null, /* @ts-ignore */
|
|
2007
|
+
...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
2008
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
2009
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
2010
|
+
showClear = computed(() => this.clearable() && !!this.value() && !this.isDisabled(), /* @ts-ignore */
|
|
2011
|
+
...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
|
|
2012
|
+
/** The week to highlight: the hovered one while previewing, else the value. */
|
|
2013
|
+
highlightWeek = computed(() => this.hoverWeek() ?? this.value(), /* @ts-ignore */
|
|
2014
|
+
...(ngDevMode ? [{ debugName: "highlightWeek" }] : /* istanbul ignore next */ []));
|
|
2015
|
+
/** Display label, e.g. `W28 · Jul 6 – Jul 12, 2026`. */
|
|
2016
|
+
displayLabel = computed(() => {
|
|
2017
|
+
const week = this.value();
|
|
2018
|
+
if (!week)
|
|
2019
|
+
return '';
|
|
2020
|
+
const fmt = this.displayFormat();
|
|
2021
|
+
const names = this.i18n.dateNames;
|
|
2022
|
+
const range = `${formatDate(week.start, fmt, names)} – ${formatDate(week.end, `${fmt}, yyyy`, names)}`;
|
|
2023
|
+
return this.showWeekNumber() ? `W${getISOWeek(week.start)} · ${range}` : range;
|
|
2024
|
+
}, /* @ts-ignore */
|
|
2025
|
+
...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
|
|
2026
|
+
toggle() {
|
|
2027
|
+
if (this.isDisabled())
|
|
2028
|
+
return;
|
|
2029
|
+
this.open() ? this.close() : this.openPanel();
|
|
2030
|
+
}
|
|
2031
|
+
openPanel() {
|
|
2032
|
+
if (this.isDisabled() || this.open())
|
|
2033
|
+
return;
|
|
2034
|
+
this.open.set(true);
|
|
2035
|
+
afterNextRender({
|
|
2036
|
+
write: () => {
|
|
2037
|
+
const el = this.panelRef()?.nativeElement.querySelector('.mk-calendar__day[tabindex="0"]');
|
|
2038
|
+
el?.focus();
|
|
2039
|
+
},
|
|
2040
|
+
}, { injector: this.injector });
|
|
2041
|
+
}
|
|
2042
|
+
close() {
|
|
2043
|
+
if (this.open()) {
|
|
2044
|
+
this.open.set(false);
|
|
2045
|
+
this.hoverWeek.set(null);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
/** A day inside the calendar was activated — select its whole week. */
|
|
2049
|
+
onPick(date) {
|
|
2050
|
+
const week = this.weekOf(date);
|
|
2051
|
+
this.value.set(week);
|
|
2052
|
+
this.onChange(week);
|
|
2053
|
+
this.close();
|
|
2054
|
+
this.triggerRef()?.nativeElement.focus();
|
|
2055
|
+
}
|
|
2056
|
+
/** Preview the hovered day's week. */
|
|
2057
|
+
onHover(date) {
|
|
2058
|
+
this.hoverWeek.set(date ? this.weekOf(date) : null);
|
|
2059
|
+
}
|
|
2060
|
+
clear() {
|
|
2061
|
+
this.value.set(null);
|
|
2062
|
+
this.onChange(null);
|
|
2063
|
+
this.triggerRef()?.nativeElement.focus();
|
|
2064
|
+
}
|
|
2065
|
+
weekOf(date) {
|
|
2066
|
+
const fdow = this.firstDayOfWeek();
|
|
2067
|
+
return { start: startOfWeek(date, fdow), end: endOfWeek(date, fdow) };
|
|
2068
|
+
}
|
|
2069
|
+
/** Reference date the calendar should open on (the selected week's start). */
|
|
2070
|
+
calendarValue = computed(() => this.value()?.start ?? null, /* @ts-ignore */
|
|
2071
|
+
...(ngDevMode ? [{ debugName: "calendarValue" }] : /* istanbul ignore next */ []));
|
|
2072
|
+
onFocusOut(event) {
|
|
2073
|
+
const related = event.relatedTarget;
|
|
2074
|
+
if (related && this.host.nativeElement.contains(related))
|
|
2075
|
+
return;
|
|
2076
|
+
if (related && this.panelRef()?.nativeElement.contains(related))
|
|
2077
|
+
return;
|
|
2078
|
+
this.close();
|
|
2079
|
+
this.onTouched();
|
|
2080
|
+
}
|
|
2081
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
2082
|
+
writeValue(value) {
|
|
2083
|
+
// Re-anchor any incoming date to a full week for consistent highlighting.
|
|
2084
|
+
if (value?.start)
|
|
2085
|
+
this.value.set(this.weekOf(value.start));
|
|
2086
|
+
else
|
|
2087
|
+
this.value.set(null);
|
|
2088
|
+
}
|
|
2089
|
+
registerOnChange(fn) {
|
|
2090
|
+
this.onChange = fn;
|
|
2091
|
+
}
|
|
2092
|
+
registerOnTouched(fn) {
|
|
2093
|
+
this.onTouched = fn;
|
|
2094
|
+
}
|
|
2095
|
+
setDisabledState(isDisabled) {
|
|
2096
|
+
this.cvaDisabled.set(isDisabled);
|
|
2097
|
+
}
|
|
2098
|
+
/** Expose the raw highlight endpoints to the template. */
|
|
2099
|
+
highlightStart = computed(() => this.highlightWeek()?.start ?? null, /* @ts-ignore */
|
|
2100
|
+
...(ngDevMode ? [{ debugName: "highlightStart" }] : /* istanbul ignore next */ []));
|
|
2101
|
+
highlightEnd = computed(() => this.highlightWeek()?.end ?? null, /* @ts-ignore */
|
|
2102
|
+
...(ngDevMode ? [{ debugName: "highlightEnd" }] : /* istanbul ignore next */ []));
|
|
2103
|
+
// --- Validator ------------------------------------------------------------
|
|
2104
|
+
validatorChange = mkValidatorChange(() => {
|
|
2105
|
+
this.min();
|
|
2106
|
+
this.max();
|
|
2107
|
+
});
|
|
2108
|
+
/**
|
|
2109
|
+
* Reports `mkMinDate` / `mkMaxDate` when the selected week starts before
|
|
2110
|
+
* `[min]` or ends after `[max]`.
|
|
2111
|
+
*/
|
|
2112
|
+
validate(control) {
|
|
2113
|
+
const v = control.value;
|
|
2114
|
+
const start = v?.start;
|
|
2115
|
+
const end = v?.end;
|
|
2116
|
+
if (!(start instanceof Date) || !(end instanceof Date))
|
|
2117
|
+
return null;
|
|
2118
|
+
const min = this.min();
|
|
2119
|
+
if (min && isBefore(startOfDay(start), startOfDay(min))) {
|
|
2120
|
+
return { mkMinDate: { min, actual: start } };
|
|
2121
|
+
}
|
|
2122
|
+
const max = this.max();
|
|
2123
|
+
if (max && isAfter(startOfDay(end), startOfDay(max))) {
|
|
2124
|
+
return { mkMaxDate: { max, actual: end } };
|
|
2125
|
+
}
|
|
2126
|
+
return null;
|
|
2127
|
+
}
|
|
2128
|
+
registerOnValidatorChange(fn) {
|
|
2129
|
+
this.validatorChange.register(fn);
|
|
2130
|
+
}
|
|
2131
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkWeekPicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2132
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkWeekPicker, isStandalone: true, selector: "mk-week-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, showWeekNumber: { classPropertyName: "showWeekNumber", publicName: "showWeekNumber", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class.mk-week-picker--sm": "effectiveSize() === 'sm'", "class.mk-week-picker--lg": "effectiveSize() === 'lg'", "class.mk-week-picker--open": "open()", "class.mk-week-picker--invalid": "isInvalid()", "class.mk-week-picker--disabled": "isDisabled()" }, classAttribute: "mk-week-picker" }, providers: [
|
|
2133
|
+
{
|
|
2134
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2135
|
+
useExisting: forwardRef(() => MkWeekPicker),
|
|
2136
|
+
multi: true,
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
provide: NG_VALIDATORS,
|
|
2140
|
+
useExisting: forwardRef(() => MkWeekPicker),
|
|
2141
|
+
multi: true,
|
|
2142
|
+
},
|
|
2143
|
+
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #field class=\"mk-week-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-week-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-week-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-week-picker__value mk-week-picker__value--placeholder\">{{\n placeholder()\n }}</span>\n }\n <svg\n class=\"mk-week-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-week-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-week-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-week-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseWeek\"\n >\n <mk-calendar\n [value]=\"calendarValue()\"\n [rangeMode]=\"true\"\n [rangeStart]=\"highlightStart()\"\n [rangeEnd]=\"highlightEnd()\"\n (dateSelected)=\"onPick($event)\"\n (dateHovered)=\"onHover($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-week-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-week-picker--sm) .mk-week-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-week-picker--lg) .mk-week-picker__field{height:var(--mk-control-height-lg)}.mk-week-picker__field:hover{border-color:var(--mk-border-strong)}.mk-week-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-week-picker--invalid) .mk-week-picker__field{border-color:var(--mk-danger)}:host(.mk-week-picker--invalid) .mk-week-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-week-picker--disabled) .mk-week-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-week-picker__trigger{display:flex;align-items:center;gap:var(--mk-space-2);flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);text-align:start;background:transparent;border:0;outline:none;cursor:pointer}:host(.mk-week-picker--sm) .mk-week-picker__trigger{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-week-picker--lg) .mk-week-picker__trigger{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-week-picker__trigger:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-week-picker__value{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-week-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-week-picker__icon{flex:none;color:var(--mk-text-muted)}.mk-week-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-week-picker__clear:hover{color:var(--mk-text)}.mk-week-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-week-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-week-picker__clear-icon:before,.mk-week-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-week-picker__clear-icon:before{transform:rotate(45deg)}.mk-week-picker__clear-icon:after{transform:rotate(-45deg)}.mk-week-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-week-picker__field,.mk-week-picker__clear{transition:none}}\n"], dependencies: [{ kind: "component", type: MkCalendar, selector: "mk-calendar", inputs: ["value", "min", "max", "firstDayOfWeek", "disabled", "invalid", "disabledDate", "size", "fullWidth", "rangeMode", "rangeStart", "rangeEnd"], outputs: ["valueChange", "dateSelected", "dateHovered"] }, { kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2144
|
+
}
|
|
2145
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkWeekPicker, decorators: [{
|
|
2146
|
+
type: Component,
|
|
2147
|
+
args: [{ selector: 'mk-week-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkCalendar, MkAnchoredPanel], host: {
|
|
2148
|
+
class: 'mk-week-picker',
|
|
2149
|
+
'[class.mk-week-picker--sm]': "effectiveSize() === 'sm'",
|
|
2150
|
+
'[class.mk-week-picker--lg]': "effectiveSize() === 'lg'",
|
|
2151
|
+
'[class.mk-week-picker--open]': 'open()',
|
|
2152
|
+
'[class.mk-week-picker--invalid]': 'isInvalid()',
|
|
2153
|
+
'[class.mk-week-picker--disabled]': 'isDisabled()',
|
|
2154
|
+
'(focusout)': 'onFocusOut($event)',
|
|
2155
|
+
}, providers: [
|
|
2156
|
+
{
|
|
2157
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2158
|
+
useExisting: forwardRef(() => MkWeekPicker),
|
|
2159
|
+
multi: true,
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
provide: NG_VALIDATORS,
|
|
2163
|
+
useExisting: forwardRef(() => MkWeekPicker),
|
|
2164
|
+
multi: true,
|
|
2165
|
+
},
|
|
2166
|
+
], template: "<div #field class=\"mk-week-picker__field\">\n <button\n #trigger\n type=\"button\"\n class=\"mk-week-picker__trigger\"\n [id]=\"triggerId\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"isInvalid() || null\"\n [attr.aria-required]=\"isRequired() || null\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n >\n @if (displayLabel()) {\n <span class=\"mk-week-picker__value\">{{ displayLabel() }}</span>\n } @else {\n <span class=\"mk-week-picker__value mk-week-picker__value--placeholder\">{{\n placeholder()\n }}</span>\n }\n <svg\n class=\"mk-week-picker__icon\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"17\" rx=\"2\" />\n <path d=\"M3 9h18M8 2v4M16 2v4\" />\n </svg>\n </button>\n\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mk-week-picker__clear\"\n [attr.aria-label]=\"i18n.clear\"\n (click)=\"clear()\"\n >\n <span class=\"mk-week-picker__clear-icon\" aria-hidden=\"true\"></span>\n </button>\n }\n</div>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-week-picker__panel\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"field\"\n (dismiss)=\"close()\"\n [id]=\"panelId\"\n role=\"dialog\"\n [attr.aria-label]=\"i18n.chooseWeek\"\n >\n <mk-calendar\n [value]=\"calendarValue()\"\n [rangeMode]=\"true\"\n [rangeStart]=\"highlightStart()\"\n [rangeEnd]=\"highlightEnd()\"\n (dateSelected)=\"onPick($event)\"\n (dateHovered)=\"onHover($event)\"\n [min]=\"min()\"\n [max]=\"max()\"\n [firstDayOfWeek]=\"firstDayOfWeek()\"\n [size]=\"effectiveSize()\"\n />\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;font-family:var(--mk-font-sans)}.mk-week-picker__field{display:flex;align-items:center;width:100%;height:var(--mk-control-height-md);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-week-picker--sm) .mk-week-picker__field{height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm)}:host(.mk-week-picker--lg) .mk-week-picker__field{height:var(--mk-control-height-lg)}.mk-week-picker__field:hover{border-color:var(--mk-border-strong)}.mk-week-picker__field:focus-within{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-color:var(--mk-primary)}:host(.mk-week-picker--invalid) .mk-week-picker__field{border-color:var(--mk-danger)}:host(.mk-week-picker--invalid) .mk-week-picker__field:focus-within{outline-color:var(--mk-danger)}:host(.mk-week-picker--disabled) .mk-week-picker__field{background-color:var(--mk-surface-2);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}.mk-week-picker__trigger{display:flex;align-items:center;gap:var(--mk-space-2);flex:1 1 auto;min-width:0;height:100%;padding:0 var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);text-align:start;background:transparent;border:0;outline:none;cursor:pointer}:host(.mk-week-picker--sm) .mk-week-picker__trigger{padding:0 var(--mk-space-2);font-size:var(--mk-font-size-sm)}:host(.mk-week-picker--lg) .mk-week-picker__trigger{padding:0 var(--mk-space-4);font-size:var(--mk-font-size-lg)}.mk-week-picker__trigger:disabled{color:var(--mk-text-disabled);cursor:not-allowed}.mk-week-picker__value{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-week-picker__value--placeholder{color:var(--mk-text-subtle)}.mk-week-picker__icon{flex:none;color:var(--mk-text-muted)}.mk-week-picker__clear{display:inline-flex;align-items:center;justify-content:center;flex:none;height:100%;padding:0 var(--mk-space-2);color:var(--mk-text-muted);background:transparent;border:0;cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-week-picker__clear:hover{color:var(--mk-text)}.mk-week-picker__clear:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1);border-radius:var(--mk-radius-sm)}.mk-week-picker__clear-icon{position:relative;width:.85rem;height:.85rem}.mk-week-picker__clear-icon:before,.mk-week-picker__clear-icon:after{content:\"\";position:absolute;top:50%;left:0;width:100%;height:2px;background-color:currentColor}.mk-week-picker__clear-icon:before{transform:rotate(45deg)}.mk-week-picker__clear-icon:after{transform:rotate(-45deg)}.mk-week-picker__panel{box-shadow:var(--mk-shadow-lg);border-radius:var(--mk-radius-lg)}@media(prefers-reduced-motion:reduce){.mk-week-picker__field,.mk-week-picker__clear{transition:none}}\n"] }]
|
|
2167
|
+
}], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], showWeekNumber: [{ type: i0.Input, args: [{ isSignal: true, alias: "showWeekNumber", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
2168
|
+
|
|
2169
|
+
function minutes(d) {
|
|
2170
|
+
return d.getHours() * 60 + d.getMinutes();
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Position a day's timed events on an hour grid, packing overlaps side by
|
|
2174
|
+
* side instead of stacking them: within a cluster of transitively-overlapping
|
|
2175
|
+
* events each takes the lowest free lane, and every member of the cluster is
|
|
2176
|
+
* widened to the same lane count so columns line up (the layout every
|
|
2177
|
+
* scheduler UI uses).
|
|
2178
|
+
*
|
|
2179
|
+
* Pure — no Angular, no DOM — so the packing has direct unit tests.
|
|
2180
|
+
*/
|
|
2181
|
+
function layoutTimedEvents(events, day, startHour, endHour) {
|
|
2182
|
+
const rangeMin = (endHour - startHour) * 60;
|
|
2183
|
+
if (rangeMin <= 0)
|
|
2184
|
+
return [];
|
|
2185
|
+
const timed = events
|
|
2186
|
+
.filter((e) => e.start && isSameDay(e.start, day))
|
|
2187
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime() ||
|
|
2188
|
+
(b.end?.getTime() ?? 0) - (a.end?.getTime() ?? 0));
|
|
2189
|
+
const placed = [];
|
|
2190
|
+
let cluster = [];
|
|
2191
|
+
let clusterEnd = -1;
|
|
2192
|
+
const closeCluster = () => {
|
|
2193
|
+
const lanes = Math.max(1, ...cluster.map((p) => p.lane + 1));
|
|
2194
|
+
for (const p of cluster)
|
|
2195
|
+
p.lanes = lanes;
|
|
2196
|
+
cluster = [];
|
|
2197
|
+
clusterEnd = -1;
|
|
2198
|
+
};
|
|
2199
|
+
for (const event of timed) {
|
|
2200
|
+
const startMin = minutes(event.start);
|
|
2201
|
+
// A missing end reads as 30 minutes — long enough to click.
|
|
2202
|
+
const endMin = event.end && isSameDay(event.end, day)
|
|
2203
|
+
? Math.max(minutes(event.end), startMin + 15)
|
|
2204
|
+
: startMin + 30;
|
|
2205
|
+
if (cluster.length && startMin >= clusterEnd)
|
|
2206
|
+
closeCluster();
|
|
2207
|
+
// Lowest lane whose latest occupant has finished.
|
|
2208
|
+
const laneEnds = [];
|
|
2209
|
+
for (const p of cluster) {
|
|
2210
|
+
laneEnds[p.lane] = Math.max(laneEnds[p.lane] ?? 0, p.endMin);
|
|
2211
|
+
}
|
|
2212
|
+
let lane = 0;
|
|
2213
|
+
while ((laneEnds[lane] ?? 0) > startMin)
|
|
2214
|
+
lane++;
|
|
2215
|
+
const clampedStart = Math.max(startMin, startHour * 60);
|
|
2216
|
+
const clampedEnd = Math.min(endMin, endHour * 60);
|
|
2217
|
+
if (clampedEnd <= startHour * 60 || clampedStart >= endHour * 60)
|
|
2218
|
+
continue;
|
|
2219
|
+
const item = {
|
|
2220
|
+
event,
|
|
2221
|
+
startMin,
|
|
2222
|
+
endMin,
|
|
2223
|
+
lane,
|
|
2224
|
+
lanes: 1,
|
|
2225
|
+
top: ((clampedStart - startHour * 60) / rangeMin) * 100,
|
|
2226
|
+
height: Math.max(((clampedEnd - clampedStart) / rangeMin) * 100, 2.5),
|
|
2227
|
+
};
|
|
2228
|
+
cluster.push(item);
|
|
2229
|
+
placed.push(item);
|
|
2230
|
+
clusterEnd = Math.max(clusterEnd, endMin);
|
|
2231
|
+
}
|
|
2232
|
+
if (cluster.length)
|
|
2233
|
+
closeCluster();
|
|
2234
|
+
return placed.map(({ startMin: _s, endMin: _e, ...p }) => p);
|
|
2235
|
+
}
|
|
2236
|
+
/** Events on `day` with no start time — rendered in the all-day strip. */
|
|
2237
|
+
function allDayEvents(events, day) {
|
|
2238
|
+
return events.filter((e) => !e.start && isSameDay(e.date, day));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/** Pixels the pointer must travel before a press turns into an edit drag. */
|
|
2242
|
+
const DRAG_THRESHOLD = 5;
|
|
2243
|
+
/**
|
|
2244
|
+
* Pixels a *touch* pointer may wander during the long-press delay before the
|
|
2245
|
+
* press is treated as a scroll and the pending drag is abandoned.
|
|
2246
|
+
*/
|
|
2247
|
+
const TOUCH_SLOP = 10;
|
|
2248
|
+
/** Long-press delay (ms) before a touch pointer arms an edit drag. */
|
|
2249
|
+
const TOUCH_DELAY_MS = 300;
|
|
2250
|
+
/** Safety window (ms) for swallowing the synthetic click after a real drag. */
|
|
2251
|
+
const CLICK_SWALLOW_MS = 400;
|
|
2252
|
+
/** Minutes since local midnight. */
|
|
2253
|
+
function minutesOfDay(d) {
|
|
2254
|
+
return d.getHours() * 60 + d.getMinutes();
|
|
2255
|
+
}
|
|
2256
|
+
/** `minutes` since midnight as a zero-padded `HH:mm` label. */
|
|
2257
|
+
function minutesLabel(minutes) {
|
|
2258
|
+
const h = Math.floor(minutes / 60) % 24;
|
|
2259
|
+
const m = Math.round(minutes % 60);
|
|
2260
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
2261
|
+
}
|
|
2262
|
+
function clampNumber(value, lo, hi) {
|
|
2263
|
+
return Math.min(Math.max(value, lo), Math.max(lo, hi));
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* `[startMin, endMin]` of a timed event, mirroring the layout's defaults: a
|
|
2267
|
+
* missing end reads as 30 minutes, an end on the same day never less than 15.
|
|
2268
|
+
*/
|
|
2269
|
+
function eventMinutes(event) {
|
|
2270
|
+
const start = event.start ? minutesOfDay(event.start) : 0;
|
|
2271
|
+
const end = event.end && event.start && isSameDay(event.end, event.start)
|
|
2272
|
+
? Math.max(minutesOfDay(event.end), start + 15)
|
|
2273
|
+
: start + 30;
|
|
2274
|
+
return [start, end];
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Event calendar — a month-grid scheduler view. Renders a 6×7 grid of day
|
|
2278
|
+
* cells built from {@link buildMonthMatrix}; each cell lists the events whose
|
|
2279
|
+
* `date` falls on that day as coloured pills, capped at `maxPerDay` with a
|
|
2280
|
+
* "+N more" line for the remainder.
|
|
2281
|
+
*
|
|
2282
|
+
* Shares the month-grid rendering, header navigation, and weekday header of
|
|
2283
|
+
* `mk-calendar` but is display-only (no date selection). Exposes the viewed
|
|
2284
|
+
* month as a two-way `viewDate` model plus `dayClick` / `eventClick` outputs.
|
|
2285
|
+
*
|
|
2286
|
+
* With `editable`, the week/day time grids additionally support drag-to-move
|
|
2287
|
+
* and drag-to-resize (pointer *and* keyboard — WCAG 2.5.7): times snap to
|
|
2288
|
+
* `snapMinutes` and the would-be range is emitted via `eventMove` /
|
|
2289
|
+
* `eventResize` — the calendar never mutates `events` itself.
|
|
2290
|
+
*
|
|
2291
|
+
* ```html
|
|
2292
|
+
* <mk-event-calendar [events]="events" [(viewDate)]="month"
|
|
2293
|
+
* (eventClick)="open($event)" />
|
|
2294
|
+
* ```
|
|
2295
|
+
*/
|
|
2296
|
+
class MkEventCalendar {
|
|
2297
|
+
i18n = inject(MK_I18N);
|
|
2298
|
+
doc = inject(DOCUMENT);
|
|
2299
|
+
hostEl = inject(ElementRef).nativeElement;
|
|
2300
|
+
announcer = inject(MkLiveAnnouncer);
|
|
2301
|
+
/** Events to plot onto the grid. */
|
|
2302
|
+
events = input([], /* @ts-ignore */
|
|
2303
|
+
...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
2304
|
+
/** Two-way month being viewed (any day within it). Defaults to today. */
|
|
2305
|
+
viewDate = model(new Date(), /* @ts-ignore */
|
|
2306
|
+
...(ngDevMode ? [{ debugName: "viewDate" }] : /* istanbul ignore next */ []));
|
|
2307
|
+
/** First column of the week: 0 = Sunday … 6 = Saturday. */
|
|
2308
|
+
firstDayOfWeek = input(0, { ...(ngDevMode ? { debugName: "firstDayOfWeek" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
2309
|
+
/** Max event pills per day before collapsing the rest into "+N more". */
|
|
2310
|
+
maxPerDay = input(3, { ...(ngDevMode ? { debugName: "maxPerDay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
2311
|
+
/** Presentation: the classic month grid, or a timed `week`/`day` grid. */
|
|
2312
|
+
view = model('month', /* @ts-ignore */
|
|
2313
|
+
...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
2314
|
+
/** First hour shown on the week/day time grid. */
|
|
2315
|
+
dayStartHour = input(8, { ...(ngDevMode ? { debugName: "dayStartHour" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
2316
|
+
/** Hour the time grid ends at (exclusive). */
|
|
2317
|
+
dayEndHour = input(22, { ...(ngDevMode ? { debugName: "dayEndHour" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
2318
|
+
/**
|
|
2319
|
+
* Week/day views: allow rescheduling events by dragging a pill (move) or its
|
|
2320
|
+
* bottom edge (resize end time), by pointer or keyboard. Off by default —
|
|
2321
|
+
* without it the grid renders exactly as before (no handles, no ARIA
|
|
2322
|
+
* additions, no drag behavior).
|
|
2323
|
+
*/
|
|
2324
|
+
editable = input(false, { ...(ngDevMode ? { debugName: "editable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2325
|
+
/** Snap grid (minutes) for drag/keyboard edits; also the minimum duration. */
|
|
2326
|
+
snapMinutes = input(15, { ...(ngDevMode ? { debugName: "snapMinutes" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
2327
|
+
/** Emitted when a day cell is activated (click / Enter). */
|
|
2328
|
+
dayClick = output();
|
|
2329
|
+
/**
|
|
2330
|
+
* Emitted when an event pill is clicked (does not also fire `dayClick`).
|
|
2331
|
+
* Mouse-only convenience: the pills are presentational (`aria-hidden`), so
|
|
2332
|
+
* keyboard / screen-reader users reach events via the day button — its label
|
|
2333
|
+
* announces the day's events and activating it fires `dayClick`.
|
|
2334
|
+
*/
|
|
2335
|
+
eventClick = output();
|
|
2336
|
+
/** Emitted when the viewed range changes via the prev/next buttons. */
|
|
2337
|
+
monthChange = output();
|
|
2338
|
+
/** Week/day view: an empty hour slot was clicked — the slot's start instant. */
|
|
2339
|
+
slotClick = output();
|
|
2340
|
+
/**
|
|
2341
|
+
* Editable week/day grid: an event pill was dragged (or keyboard-moved) to a
|
|
2342
|
+
* new start time and/or day. Carries the would-be `start`/`end` — the
|
|
2343
|
+
* calendar does **not** mutate `events`; the consumer owns the data and
|
|
2344
|
+
* applies the change, typically:
|
|
2345
|
+
*
|
|
2346
|
+
* ```ts
|
|
2347
|
+
* onMove = ({ event, start, end }: MkCalendarEventEdit) =>
|
|
2348
|
+
* (this.events = this.events.map((e) =>
|
|
2349
|
+
* e === event ? { ...e, date: start, start, end } : e,
|
|
2350
|
+
* ));
|
|
2351
|
+
* ```
|
|
2352
|
+
*/
|
|
2353
|
+
eventMove = output();
|
|
2354
|
+
/**
|
|
2355
|
+
* Editable week/day grid: the pill's bottom edge was dragged (or
|
|
2356
|
+
* Shift+Arrow-resized) — only `end` differs, never below `snapMinutes` of
|
|
2357
|
+
* duration. Same consumer-owns-the-data contract as `eventMove`.
|
|
2358
|
+
*/
|
|
2359
|
+
eventResize = output();
|
|
2360
|
+
/** id of the visible month/year label — wired as the grid's label. */
|
|
2361
|
+
labelId = mkUniqueId('mk-event-calendar-label');
|
|
2362
|
+
/** First day of the visible month. */
|
|
2363
|
+
viewMonth = computed(() => startOfMonth(this.viewDate()), /* @ts-ignore */
|
|
2364
|
+
...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
|
|
2365
|
+
/** Header label — month, day, or week range, matching the active view. */
|
|
2366
|
+
monthLabel = computed(() => {
|
|
2367
|
+
const d = this.viewDate();
|
|
2368
|
+
const names = this.i18n.dateNames;
|
|
2369
|
+
switch (this.view()) {
|
|
2370
|
+
case 'day':
|
|
2371
|
+
return formatDate(d, 'MMMM d, yyyy', names);
|
|
2372
|
+
case 'week': {
|
|
2373
|
+
const from = startOfWeek(d, this.firstDayOfWeek());
|
|
2374
|
+
const to = addDays(from, 6);
|
|
2375
|
+
return `${formatDate(from, 'MMM d', names)} – ${formatDate(to, 'MMM d, yyyy', names)}`;
|
|
2376
|
+
}
|
|
2377
|
+
default:
|
|
2378
|
+
return formatDate(d, 'MMMM yyyy', names);
|
|
2379
|
+
}
|
|
2380
|
+
}, /* @ts-ignore */
|
|
2381
|
+
...(ngDevMode ? [{ debugName: "monthLabel" }] : /* istanbul ignore next */ []));
|
|
2382
|
+
/** Days of the active time grid: one for `day`, seven for `week`. */
|
|
2383
|
+
gridDays = computed(() => {
|
|
2384
|
+
const d = startOfDay(this.viewDate());
|
|
2385
|
+
if (this.view() === 'day')
|
|
2386
|
+
return [d];
|
|
2387
|
+
const from = startOfWeek(d, this.firstDayOfWeek());
|
|
2388
|
+
return Array.from({ length: 7 }, (_, i) => addDays(from, i));
|
|
2389
|
+
}, /* @ts-ignore */
|
|
2390
|
+
...(ngDevMode ? [{ debugName: "gridDays" }] : /* istanbul ignore next */ []));
|
|
2391
|
+
/** Hour rows of the time grid. */
|
|
2392
|
+
hours = computed(() => {
|
|
2393
|
+
const from = this.dayStartHour();
|
|
2394
|
+
const to = this.dayEndHour();
|
|
2395
|
+
return Array.from({ length: Math.max(to - from, 0) }, (_, i) => from + i);
|
|
2396
|
+
}, /* @ts-ignore */
|
|
2397
|
+
...(ngDevMode ? [{ debugName: "hours" }] : /* istanbul ignore next */ []));
|
|
2398
|
+
/**
|
|
2399
|
+
* Timed events bucketed by their start's calendar day — a single pass over
|
|
2400
|
+
* the events, shared by every column of the week/day grid instead of
|
|
2401
|
+
* re-filtering (and re-sorting) the whole array once per day.
|
|
2402
|
+
*/
|
|
2403
|
+
timedByDay = computed(() => {
|
|
2404
|
+
const map = new Map();
|
|
2405
|
+
for (const event of this.events()) {
|
|
2406
|
+
if (!event.start)
|
|
2407
|
+
continue;
|
|
2408
|
+
const key = startOfDay(event.start).getTime();
|
|
2409
|
+
const bucket = map.get(key);
|
|
2410
|
+
if (bucket)
|
|
2411
|
+
bucket.push(event);
|
|
2412
|
+
else
|
|
2413
|
+
map.set(key, [event]);
|
|
2414
|
+
}
|
|
2415
|
+
return map;
|
|
2416
|
+
}, /* @ts-ignore */
|
|
2417
|
+
...(ngDevMode ? [{ debugName: "timedByDay" }] : /* istanbul ignore next */ []));
|
|
2418
|
+
/** Untimed events (all-day strip) bucketed by `date`'s calendar day. */
|
|
2419
|
+
allDayByDay = computed(() => {
|
|
2420
|
+
const map = new Map();
|
|
2421
|
+
for (const event of this.events()) {
|
|
2422
|
+
if (event.start)
|
|
2423
|
+
continue;
|
|
2424
|
+
const key = startOfDay(event.date).getTime();
|
|
2425
|
+
const bucket = map.get(key);
|
|
2426
|
+
if (bucket)
|
|
2427
|
+
bucket.push(event);
|
|
2428
|
+
else
|
|
2429
|
+
map.set(key, [event]);
|
|
2430
|
+
}
|
|
2431
|
+
return map;
|
|
2432
|
+
}, /* @ts-ignore */
|
|
2433
|
+
...(ngDevMode ? [{ debugName: "allDayByDay" }] : /* istanbul ignore next */ []));
|
|
2434
|
+
/**
|
|
2435
|
+
* Per-day positioned events + all-day strip for the time grid. The column
|
|
2436
|
+
* head (`Mon 21`) is precomputed here so the template never calls
|
|
2437
|
+
* `formatDate` during change detection.
|
|
2438
|
+
*/
|
|
2439
|
+
timedColumns = computed(() => {
|
|
2440
|
+
const timedByDay = this.timedByDay();
|
|
2441
|
+
const allDayByDay = this.allDayByDay();
|
|
2442
|
+
const startHour = this.dayStartHour();
|
|
2443
|
+
const endHour = this.dayEndHour();
|
|
2444
|
+
const names = this.i18n.dateNames;
|
|
2445
|
+
const today = new Date();
|
|
2446
|
+
// gridDays() are `startOfDay` instants, so `getTime()` matches the keys.
|
|
2447
|
+
return this.gridDays().map((day) => ({
|
|
2448
|
+
day,
|
|
2449
|
+
today: isSameDay(day, today),
|
|
2450
|
+
head: formatDate(day, 'ddd d', names),
|
|
2451
|
+
placements: layoutTimedEvents(timedByDay.get(day.getTime()) ?? [], day, startHour, endHour),
|
|
2452
|
+
allDay: allDayByDay.get(day.getTime()) ?? [],
|
|
2453
|
+
}));
|
|
2454
|
+
}, /* @ts-ignore */
|
|
2455
|
+
...(ngDevMode ? [{ debugName: "timedColumns" }] : /* istanbul ignore next */ []));
|
|
2456
|
+
hourLabel(hour) {
|
|
2457
|
+
return `${String(hour).padStart(2, '0')}:00`;
|
|
2458
|
+
}
|
|
2459
|
+
placementTitle(p) {
|
|
2460
|
+
const from = p.event.start ? minutesLabel(minutesOfDay(p.event.start)) : '';
|
|
2461
|
+
return from ? `${from} ${p.event.title}` : p.event.title;
|
|
2462
|
+
}
|
|
2463
|
+
onSlotClick(day, hour) {
|
|
2464
|
+
const at = new Date(day);
|
|
2465
|
+
at.setHours(hour, 0, 0, 0);
|
|
2466
|
+
this.slotClick.emit(at);
|
|
2467
|
+
}
|
|
2468
|
+
// ===================================================================
|
|
2469
|
+
// Editable time grid — drag-to-move / drag-to-resize (pointer)
|
|
2470
|
+
// ===================================================================
|
|
2471
|
+
/** Template-facing state of the in-flight edit (`null` when idle). */
|
|
2472
|
+
editPreview = signal(null, /* @ts-ignore */
|
|
2473
|
+
...(ngDevMode ? [{ debugName: "editPreview" }] : /* istanbul ignore next */ []));
|
|
2474
|
+
/** The active pointer gesture, if any (only one at a time). */
|
|
2475
|
+
pointerSession = null;
|
|
2476
|
+
/** The active keyboard "move mode" session, if any. */
|
|
2477
|
+
kbSession = null;
|
|
2478
|
+
/** Pending rAF id for the coalesced pointer-move pass. */
|
|
2479
|
+
editRaf = null;
|
|
2480
|
+
pendingX = 0;
|
|
2481
|
+
pendingY = 0;
|
|
2482
|
+
/** Set after a real drag so the trailing synthetic click never fires `eventClick`. */
|
|
2483
|
+
swallowNextClick = false;
|
|
2484
|
+
pointerMoveHandler = (e) => this.onEditPointerMove(e);
|
|
2485
|
+
pointerUpHandler = (e) => this.onEditPointerUp(e);
|
|
2486
|
+
pointerCancelHandler = () => this.abortPointerEdit();
|
|
2487
|
+
/** Escape aborts an in-flight pointer drag (document-level while dragging). */
|
|
2488
|
+
escHandler = (e) => {
|
|
2489
|
+
if (e.key === 'Escape' && this.pointerSession?.started) {
|
|
2490
|
+
e.stopPropagation();
|
|
2491
|
+
this.abortPointerEdit();
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
/**
|
|
2495
|
+
* `touch-action: pan-y` keeps native scrolling alive while the long-press is
|
|
2496
|
+
* pending, so once armed the drag must eat `touchmove` itself
|
|
2497
|
+
* (`passive: false` — same pattern as `MkDrag`).
|
|
2498
|
+
*/
|
|
2499
|
+
touchMoveEater = (e) => {
|
|
2500
|
+
if (this.pointerSession?.armed && e.cancelable)
|
|
2501
|
+
e.preventDefault();
|
|
2502
|
+
};
|
|
2503
|
+
/** Android fires `contextmenu` on long-press — keep it off the gesture. */
|
|
2504
|
+
contextMenuHandler = (e) => e.preventDefault();
|
|
2505
|
+
onBlockPointerDown(event, p, colIndex) {
|
|
2506
|
+
const e = event;
|
|
2507
|
+
if (!this.editable() || this.pointerSession || this.kbSession)
|
|
2508
|
+
return;
|
|
2509
|
+
if (e.button !== undefined && e.button !== 0)
|
|
2510
|
+
return;
|
|
2511
|
+
const el = e.currentTarget;
|
|
2512
|
+
const kind = e.target?.closest?.('.mk-event-calendar__resize-handle')
|
|
2513
|
+
? 'resize'
|
|
2514
|
+
: 'move';
|
|
2515
|
+
const [startMin, endMin] = eventMinutes(p.event);
|
|
2516
|
+
this.pointerSession = {
|
|
2517
|
+
kind,
|
|
2518
|
+
event: p.event,
|
|
2519
|
+
el,
|
|
2520
|
+
pointerId: e.pointerId,
|
|
2521
|
+
started: false,
|
|
2522
|
+
armed: false,
|
|
2523
|
+
startX: e.clientX,
|
|
2524
|
+
startY: e.clientY,
|
|
2525
|
+
touchTimer: null,
|
|
2526
|
+
savedTouchAction: null,
|
|
2527
|
+
colRects: [],
|
|
2528
|
+
pxPerMin: 1,
|
|
2529
|
+
origDay: colIndex,
|
|
2530
|
+
origStartMin: startMin,
|
|
2531
|
+
origEndMin: endMin,
|
|
2532
|
+
day: colIndex,
|
|
2533
|
+
startMin,
|
|
2534
|
+
endMin,
|
|
2535
|
+
};
|
|
2536
|
+
try {
|
|
2537
|
+
el.setPointerCapture(e.pointerId);
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
// Pointer already lifted (fast tap) — nothing left to capture.
|
|
2541
|
+
}
|
|
2542
|
+
el.addEventListener('pointermove', this.pointerMoveHandler);
|
|
2543
|
+
el.addEventListener('pointerup', this.pointerUpHandler);
|
|
2544
|
+
el.addEventListener('pointercancel', this.pointerCancelHandler);
|
|
2545
|
+
if (e.pointerType === 'touch') {
|
|
2546
|
+
// Touch long-press arming, as in MkDrag: until the timer fires this
|
|
2547
|
+
// press may just be the start of a scroll, so nothing is prevented yet.
|
|
2548
|
+
el.addEventListener('touchmove', this.touchMoveEater, { passive: false });
|
|
2549
|
+
el.addEventListener('contextmenu', this.contextMenuHandler);
|
|
2550
|
+
this.pointerSession.touchTimer =
|
|
2551
|
+
this.doc.defaultView?.setTimeout(() => this.armTouch(), TOUCH_DELAY_MS) ??
|
|
2552
|
+
null;
|
|
2553
|
+
}
|
|
2554
|
+
else {
|
|
2555
|
+
// Mouse / pen: armed immediately, the 5px threshold does the rest.
|
|
2556
|
+
this.pointerSession.armed = true;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
/** The long-press delay elapsed with the finger still down — arm the drag. */
|
|
2560
|
+
armTouch() {
|
|
2561
|
+
const s = this.pointerSession;
|
|
2562
|
+
if (!s)
|
|
2563
|
+
return;
|
|
2564
|
+
s.touchTimer = null;
|
|
2565
|
+
s.armed = true;
|
|
2566
|
+
s.savedTouchAction = s.el.style.touchAction;
|
|
2567
|
+
s.el.style.touchAction = 'none';
|
|
2568
|
+
}
|
|
2569
|
+
onEditPointerMove(e) {
|
|
2570
|
+
const s = this.pointerSession;
|
|
2571
|
+
if (!s || e.pointerId !== s.pointerId)
|
|
2572
|
+
return;
|
|
2573
|
+
if (!s.armed) {
|
|
2574
|
+
// Long-press still pending: real movement means the user is scrolling.
|
|
2575
|
+
if (Math.hypot(e.clientX - s.startX, e.clientY - s.startY) > TOUCH_SLOP) {
|
|
2576
|
+
this.teardownPointer(false);
|
|
2577
|
+
}
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
if (!s.started) {
|
|
2581
|
+
if (Math.hypot(e.clientX - s.startX, e.clientY - s.startY) < DRAG_THRESHOLD) {
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
this.beginEdit(s);
|
|
2585
|
+
}
|
|
2586
|
+
e.preventDefault();
|
|
2587
|
+
// Only record the coordinates — the math + signal write is rAF-coalesced
|
|
2588
|
+
// (one pass per frame, same pattern as the table's column resize).
|
|
2589
|
+
this.pendingX = e.clientX;
|
|
2590
|
+
this.pendingY = e.clientY;
|
|
2591
|
+
if (this.editRaf !== null)
|
|
2592
|
+
return;
|
|
2593
|
+
const raf = this.doc.defaultView?.requestAnimationFrame(() => {
|
|
2594
|
+
this.editRaf = null;
|
|
2595
|
+
this.applyPendingEdit();
|
|
2596
|
+
});
|
|
2597
|
+
if (raf === undefined)
|
|
2598
|
+
this.applyPendingEdit(); // no window — degrade to sync
|
|
2599
|
+
else
|
|
2600
|
+
this.editRaf = raf;
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* The drag crossed the threshold — snapshot geometry once (column rects and
|
|
2604
|
+
* the px↔minute scale); every subsequent move works off this cache, so a
|
|
2605
|
+
* drag never forces layout.
|
|
2606
|
+
*/
|
|
2607
|
+
beginEdit(s) {
|
|
2608
|
+
s.started = true;
|
|
2609
|
+
const cols = this.hostEl.querySelectorAll('.mk-event-calendar__col');
|
|
2610
|
+
s.colRects = Array.from(cols, (c) => c.getBoundingClientRect());
|
|
2611
|
+
const rangeMin = Math.max((this.dayEndHour() - this.dayStartHour()) * 60, 1);
|
|
2612
|
+
s.pxPerMin = (s.colRects[s.origDay]?.height || rangeMin) / rangeMin;
|
|
2613
|
+
this.doc.addEventListener('keydown', this.escHandler, true);
|
|
2614
|
+
this.updatePreview(s);
|
|
2615
|
+
}
|
|
2616
|
+
/** Per-frame move pass: snap, clamp, and publish the preview signal. */
|
|
2617
|
+
applyPendingEdit() {
|
|
2618
|
+
const s = this.pointerSession;
|
|
2619
|
+
if (!s || !s.started)
|
|
2620
|
+
return;
|
|
2621
|
+
const snap = Math.max(1, this.snapMinutes());
|
|
2622
|
+
const gridStart = this.dayStartHour() * 60;
|
|
2623
|
+
const gridEnd = this.dayEndHour() * 60;
|
|
2624
|
+
const deltaMin = (this.pendingY - s.startY) / s.pxPerMin;
|
|
2625
|
+
const snapped = Math.round(deltaMin / snap) * snap;
|
|
2626
|
+
if (s.kind === 'move') {
|
|
2627
|
+
const duration = s.origEndMin - s.origStartMin;
|
|
2628
|
+
s.startMin = clampNumber(s.origStartMin + snapped, gridStart, Math.max(gridStart, gridEnd - duration));
|
|
2629
|
+
s.endMin = s.startMin + duration;
|
|
2630
|
+
s.day = this.columnAt(s, this.pendingX);
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
s.endMin = clampNumber(s.origEndMin + snapped, s.startMin + snap, gridEnd);
|
|
2634
|
+
}
|
|
2635
|
+
this.updatePreview(s);
|
|
2636
|
+
}
|
|
2637
|
+
/** Column index under `x`, from the rects snapshotted at lift. */
|
|
2638
|
+
columnAt(s, x) {
|
|
2639
|
+
const rects = s.colRects;
|
|
2640
|
+
if (!rects.length || !rects[s.origDay]?.width)
|
|
2641
|
+
return s.day;
|
|
2642
|
+
for (let i = 0; i < rects.length; i++) {
|
|
2643
|
+
if (x >= rects[i].left && x < rects[i].right)
|
|
2644
|
+
return i;
|
|
2645
|
+
}
|
|
2646
|
+
return x < rects[0].left ? 0 : rects.length - 1;
|
|
2647
|
+
}
|
|
2648
|
+
onEditPointerUp(e) {
|
|
2649
|
+
const s = this.pointerSession;
|
|
2650
|
+
if (!s || e.pointerId !== s.pointerId)
|
|
2651
|
+
return;
|
|
2652
|
+
// Flush (don't drop) the last coalesced move so the drop lands exactly
|
|
2653
|
+
// where the pointer ended, not on the last painted frame.
|
|
2654
|
+
if (this.editRaf !== null) {
|
|
2655
|
+
this.doc.defaultView?.cancelAnimationFrame(this.editRaf);
|
|
2656
|
+
this.editRaf = null;
|
|
2657
|
+
this.applyPendingEdit();
|
|
2658
|
+
}
|
|
2659
|
+
const started = s.started;
|
|
2660
|
+
this.teardownPointer(started);
|
|
2661
|
+
if (!started)
|
|
2662
|
+
return; // was a click, never a drag — eventClick may follow
|
|
2663
|
+
this.editPreview.set(null);
|
|
2664
|
+
this.commitEdit(s, 'pointer');
|
|
2665
|
+
}
|
|
2666
|
+
/** Escape / pointercancel: the pill snaps back, nothing is emitted. */
|
|
2667
|
+
abortPointerEdit() {
|
|
2668
|
+
const s = this.pointerSession;
|
|
2669
|
+
if (!s)
|
|
2670
|
+
return;
|
|
2671
|
+
const started = s.started;
|
|
2672
|
+
this.teardownPointer(started);
|
|
2673
|
+
this.editPreview.set(null);
|
|
2674
|
+
if (started)
|
|
2675
|
+
this.announceCancelled('polite');
|
|
2676
|
+
}
|
|
2677
|
+
/** Undo everything `onBlockPointerDown`/`beginEdit` set up. */
|
|
2678
|
+
teardownPointer(swallowClick) {
|
|
2679
|
+
const s = this.pointerSession;
|
|
2680
|
+
if (!s)
|
|
2681
|
+
return;
|
|
2682
|
+
this.pointerSession = null;
|
|
2683
|
+
if (this.editRaf !== null) {
|
|
2684
|
+
this.doc.defaultView?.cancelAnimationFrame(this.editRaf);
|
|
2685
|
+
this.editRaf = null;
|
|
2686
|
+
}
|
|
2687
|
+
const el = s.el;
|
|
2688
|
+
if (s.pointerId !== null) {
|
|
2689
|
+
try {
|
|
2690
|
+
el.releasePointerCapture(s.pointerId);
|
|
2691
|
+
}
|
|
2692
|
+
catch {
|
|
2693
|
+
// Capture may already be gone.
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
el.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2697
|
+
el.removeEventListener('pointerup', this.pointerUpHandler);
|
|
2698
|
+
el.removeEventListener('pointercancel', this.pointerCancelHandler);
|
|
2699
|
+
el.removeEventListener('touchmove', this.touchMoveEater);
|
|
2700
|
+
el.removeEventListener('contextmenu', this.contextMenuHandler);
|
|
2701
|
+
if (s.touchTimer !== null)
|
|
2702
|
+
this.doc.defaultView?.clearTimeout(s.touchTimer);
|
|
2703
|
+
if (s.savedTouchAction !== null)
|
|
2704
|
+
el.style.touchAction = s.savedTouchAction;
|
|
2705
|
+
this.doc.removeEventListener('keydown', this.escHandler, true);
|
|
2706
|
+
if (swallowClick) {
|
|
2707
|
+
// The browser fires a synthetic click right after pointerup, which would
|
|
2708
|
+
// re-fire `eventClick` from a drag (the carousel's click-swallower
|
|
2709
|
+
// pattern, kept as a flag because the Angular listener on the same
|
|
2710
|
+
// element registered first). The timeout covers gestures with no click.
|
|
2711
|
+
this.swallowNextClick = true;
|
|
2712
|
+
this.doc.defaultView?.setTimeout(() => (this.swallowNextClick = false), CLICK_SWALLOW_MS);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
// ===================================================================
|
|
2716
|
+
// Editable time grid — keyboard move mode (WCAG 2.5.7)
|
|
2717
|
+
// ===================================================================
|
|
2718
|
+
onBlockKeydown(event, p, colIndex) {
|
|
2719
|
+
const e = event;
|
|
2720
|
+
if (!this.editable())
|
|
2721
|
+
return;
|
|
2722
|
+
const s = this.kbSession;
|
|
2723
|
+
if (!s || s.event !== p.event) {
|
|
2724
|
+
// Enter/Space arms "move mode" (and, via preventDefault, keeps the
|
|
2725
|
+
// button's synthetic click — and thus `eventClick` — from firing).
|
|
2726
|
+
if ((e.key === 'Enter' || e.key === ' ') && !this.pointerSession) {
|
|
2727
|
+
e.preventDefault();
|
|
2728
|
+
this.armKeyboard(e.currentTarget, p, colIndex);
|
|
2729
|
+
}
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const snap = Math.max(1, this.snapMinutes());
|
|
2733
|
+
const gridStart = this.dayStartHour() * 60;
|
|
2734
|
+
const gridEnd = this.dayEndHour() * 60;
|
|
2735
|
+
switch (e.key) {
|
|
2736
|
+
case 'Enter':
|
|
2737
|
+
case ' ':
|
|
2738
|
+
e.preventDefault();
|
|
2739
|
+
this.commitKeyboard();
|
|
2740
|
+
break;
|
|
2741
|
+
case 'Escape':
|
|
2742
|
+
e.preventDefault();
|
|
2743
|
+
this.cancelKeyboard();
|
|
2744
|
+
break;
|
|
2745
|
+
case 'ArrowUp':
|
|
2746
|
+
case 'ArrowDown': {
|
|
2747
|
+
e.preventDefault();
|
|
2748
|
+
const dir = e.key === 'ArrowUp' ? -1 : 1;
|
|
2749
|
+
if (e.shiftKey) {
|
|
2750
|
+
// Shift+Up/Down resizes the end, never below the snap duration.
|
|
2751
|
+
s.endMin = clampNumber(s.endMin + dir * snap, s.startMin + snap, gridEnd);
|
|
2752
|
+
}
|
|
2753
|
+
else {
|
|
2754
|
+
const duration = s.endMin - s.startMin;
|
|
2755
|
+
s.startMin = clampNumber(s.startMin + dir * snap, gridStart, Math.max(gridStart, gridEnd - duration));
|
|
2756
|
+
s.endMin = s.startMin + duration;
|
|
2757
|
+
}
|
|
2758
|
+
this.updatePreview(s);
|
|
2759
|
+
this.announceStep(s);
|
|
2760
|
+
break;
|
|
2761
|
+
}
|
|
2762
|
+
case 'ArrowLeft':
|
|
2763
|
+
case 'ArrowRight': {
|
|
2764
|
+
e.preventDefault();
|
|
2765
|
+
const dir = e.key === 'ArrowLeft' ? -1 : 1;
|
|
2766
|
+
s.day = clampNumber(s.day + dir, 0, this.gridDays().length - 1);
|
|
2767
|
+
this.updatePreview(s);
|
|
2768
|
+
this.announceStep(s);
|
|
2769
|
+
break;
|
|
2770
|
+
}
|
|
2771
|
+
default:
|
|
2772
|
+
break;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
/** Losing focus mid-move cancels the keyboard edit (no stuck state). */
|
|
2776
|
+
onBlockBlur() {
|
|
2777
|
+
if (this.kbSession)
|
|
2778
|
+
this.cancelKeyboard();
|
|
2779
|
+
}
|
|
2780
|
+
armKeyboard(el, p, colIndex) {
|
|
2781
|
+
const [startMin, endMin] = eventMinutes(p.event);
|
|
2782
|
+
const cols = this.hostEl.querySelectorAll('.mk-event-calendar__col');
|
|
2783
|
+
const colRects = Array.from(cols, (c) => c.getBoundingClientRect());
|
|
2784
|
+
const rangeMin = Math.max((this.dayEndHour() - this.dayStartHour()) * 60, 1);
|
|
2785
|
+
const s = {
|
|
2786
|
+
kind: 'move',
|
|
2787
|
+
event: p.event,
|
|
2788
|
+
el,
|
|
2789
|
+
pointerId: null,
|
|
2790
|
+
started: true,
|
|
2791
|
+
armed: true,
|
|
2792
|
+
startX: 0,
|
|
2793
|
+
startY: 0,
|
|
2794
|
+
touchTimer: null,
|
|
2795
|
+
savedTouchAction: null,
|
|
2796
|
+
colRects,
|
|
2797
|
+
pxPerMin: (colRects[colIndex]?.height || rangeMin) / rangeMin,
|
|
2798
|
+
origDay: colIndex,
|
|
2799
|
+
origStartMin: startMin,
|
|
2800
|
+
origEndMin: endMin,
|
|
2801
|
+
day: colIndex,
|
|
2802
|
+
startMin,
|
|
2803
|
+
endMin,
|
|
2804
|
+
};
|
|
2805
|
+
this.kbSession = s;
|
|
2806
|
+
this.updatePreview(s);
|
|
2807
|
+
this.announcer.announce(this.i18n.eventCalendarGrabbed(p.event.title, minutesLabel(startMin), minutesLabel(endMin)), 'assertive');
|
|
2808
|
+
}
|
|
2809
|
+
commitKeyboard() {
|
|
2810
|
+
const s = this.kbSession;
|
|
2811
|
+
if (!s)
|
|
2812
|
+
return;
|
|
2813
|
+
this.kbSession = null;
|
|
2814
|
+
this.editPreview.set(null);
|
|
2815
|
+
this.commitEdit(s, 'keyboard');
|
|
2816
|
+
}
|
|
2817
|
+
cancelKeyboard() {
|
|
2818
|
+
this.kbSession = null;
|
|
2819
|
+
this.editPreview.set(null);
|
|
2820
|
+
this.announceCancelled('assertive');
|
|
2821
|
+
}
|
|
2822
|
+
// ===================================================================
|
|
2823
|
+
// Editable time grid — shared commit / preview / template helpers
|
|
2824
|
+
// ===================================================================
|
|
2825
|
+
/**
|
|
2826
|
+
* Emit the edited range (if anything actually changed): `eventResize` when
|
|
2827
|
+
* only the end moved, `eventMove` otherwise. The `events` input is never
|
|
2828
|
+
* touched — applying the change is the consumer's job.
|
|
2829
|
+
*/
|
|
2830
|
+
commitEdit(s, source) {
|
|
2831
|
+
const dayChanged = s.day !== s.origDay;
|
|
2832
|
+
const startChanged = s.startMin !== s.origStartMin;
|
|
2833
|
+
const endChanged = s.endMin !== s.origEndMin;
|
|
2834
|
+
if (!dayChanged && !startChanged && !endChanged)
|
|
2835
|
+
return;
|
|
2836
|
+
const day = this.gridDays()[s.day] ?? this.gridDays()[s.origDay];
|
|
2837
|
+
const start = new Date(day);
|
|
2838
|
+
start.setHours(0, s.startMin, 0, 0);
|
|
2839
|
+
const end = new Date(day);
|
|
2840
|
+
end.setHours(0, s.endMin, 0, 0);
|
|
2841
|
+
const detail = { event: s.event, start, end };
|
|
2842
|
+
const resize = !dayChanged && !startChanged;
|
|
2843
|
+
if (resize)
|
|
2844
|
+
this.eventResize.emit(detail);
|
|
2845
|
+
else
|
|
2846
|
+
this.eventMove.emit(detail);
|
|
2847
|
+
this.announcer.announce(resize
|
|
2848
|
+
? this.i18n.eventCalendarResized(s.event.title, minutesLabel(s.endMin))
|
|
2849
|
+
: this.i18n.eventCalendarMoved(s.event.title, formatDate(day, 'MMMM d, yyyy', this.i18n.dateNames), minutesLabel(s.startMin), minutesLabel(s.endMin)), source === 'keyboard' ? 'assertive' : 'polite');
|
|
2850
|
+
}
|
|
2851
|
+
/** Publish the session's current target as template-facing preview state. */
|
|
2852
|
+
updatePreview(s) {
|
|
2853
|
+
const gridStart = this.dayStartHour() * 60;
|
|
2854
|
+
const gridEnd = this.dayEndHour() * 60;
|
|
2855
|
+
const rangeMin = Math.max(gridEnd - gridStart, 1);
|
|
2856
|
+
const clampedStart = Math.max(s.startMin, gridStart);
|
|
2857
|
+
const clampedEnd = Math.min(s.endMin, gridEnd);
|
|
2858
|
+
const dx = (s.colRects[s.day]?.left ?? 0) - (s.colRects[s.origDay]?.left ?? 0);
|
|
2859
|
+
const dy = (s.startMin - s.origStartMin) * s.pxPerMin;
|
|
2860
|
+
const durationChanged = s.endMin - s.startMin !== s.origEndMin - s.origStartMin;
|
|
2861
|
+
this.editPreview.set({
|
|
2862
|
+
event: s.event,
|
|
2863
|
+
day: s.day,
|
|
2864
|
+
transform: `translate(${dx}px, ${dy}px)`,
|
|
2865
|
+
heightPct: durationChanged
|
|
2866
|
+
? Math.max(((clampedEnd - clampedStart) / rangeMin) * 100, 2.5)
|
|
2867
|
+
: null,
|
|
2868
|
+
label: `${minutesLabel(s.startMin)} – ${minutesLabel(s.endMin)}`,
|
|
2869
|
+
top: ((clampedStart - gridStart) / rangeMin) * 100,
|
|
2870
|
+
outlineHeight: Math.max(((clampedEnd - clampedStart) / rangeMin) * 100, 1),
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
announceCancelled(politeness) {
|
|
2874
|
+
this.announcer.announce(this.i18n.eventCalendarEditCancelled, politeness);
|
|
2875
|
+
}
|
|
2876
|
+
/** Position update after each keyboard step. */
|
|
2877
|
+
announceStep(s) {
|
|
2878
|
+
const day = this.gridDays()[s.day];
|
|
2879
|
+
this.announcer.announce(this.i18n.eventCalendarPosition(s.event.title, formatDate(day, 'MMMM d, yyyy', this.i18n.dateNames), minutesLabel(s.startMin), minutesLabel(s.endMin)), 'assertive');
|
|
2880
|
+
}
|
|
2881
|
+
/** Is this placement the pill currently being edited? */
|
|
2882
|
+
isEditing(p) {
|
|
2883
|
+
return this.editPreview()?.event === p.event;
|
|
2884
|
+
}
|
|
2885
|
+
/** Lift transform for the dragged pill (`null` when idle). */
|
|
2886
|
+
blockTransform(p) {
|
|
2887
|
+
const d = this.editPreview();
|
|
2888
|
+
return d && d.event === p.event ? d.transform : null;
|
|
2889
|
+
}
|
|
2890
|
+
/** Height (%) — the resize preview overrides the layout's height. */
|
|
2891
|
+
blockHeight(p) {
|
|
2892
|
+
const d = this.editPreview();
|
|
2893
|
+
return d && d.event === p.event && d.heightPct !== null ? d.heightPct : p.height;
|
|
2894
|
+
}
|
|
2895
|
+
/** Would-be time range shown on the pill while it is being edited. */
|
|
2896
|
+
dragLabelFor(p) {
|
|
2897
|
+
const d = this.editPreview();
|
|
2898
|
+
return d && d.event === p.event ? d.label : null;
|
|
2899
|
+
}
|
|
2900
|
+
/** Drop-outline geometry when the edit currently targets column `i`. */
|
|
2901
|
+
dropOutlineFor(i) {
|
|
2902
|
+
const d = this.editPreview();
|
|
2903
|
+
return d && d.day === i ? d : null;
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Accessible name for an editable pill — the title plus its time range
|
|
2907
|
+
* (composed with, not replacing, the visible title text and tooltip).
|
|
2908
|
+
*/
|
|
2909
|
+
blockAriaLabel(p) {
|
|
2910
|
+
const [startMin, endMin] = eventMinutes(p.event);
|
|
2911
|
+
return `${p.event.title}, ${minutesLabel(startMin)} – ${minutesLabel(endMin)}`;
|
|
2912
|
+
}
|
|
2913
|
+
/** `aria-roledescription` for editable pills. */
|
|
2914
|
+
movableRoledescription = this.i18n.eventCalendarMovableEvent;
|
|
2915
|
+
ngOnDestroy() {
|
|
2916
|
+
if (this.pointerSession)
|
|
2917
|
+
this.teardownPointer(false);
|
|
2918
|
+
this.kbSession = null;
|
|
2919
|
+
}
|
|
2920
|
+
/** Raw 6×7 grid of dates for the visible month. */
|
|
2921
|
+
weeks = computed(() => buildMonthMatrix(this.viewDate(), this.firstDayOfWeek()), /* @ts-ignore */
|
|
2922
|
+
...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
|
|
2923
|
+
/** Weekday column headers ordered from `firstDayOfWeek`. */
|
|
2924
|
+
weekdays = computed(() => this.weeks()[0].map((d) => ({
|
|
2925
|
+
short: formatDate(d, 'ddd', this.i18n.dateNames),
|
|
2926
|
+
full: getWeekdayFullName(d, this.i18n.dateNames),
|
|
2927
|
+
})), /* @ts-ignore */
|
|
2928
|
+
...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
|
|
2929
|
+
/** Events grouped by local calendar day, keyed by `startOfDay` timestamp. */
|
|
2930
|
+
eventsByDay = computed(() => {
|
|
2931
|
+
const map = new Map();
|
|
2932
|
+
for (const event of this.events()) {
|
|
2933
|
+
const key = startOfDay(event.date).getTime();
|
|
2934
|
+
const bucket = map.get(key);
|
|
2935
|
+
if (bucket)
|
|
2936
|
+
bucket.push(event);
|
|
2937
|
+
else
|
|
2938
|
+
map.set(key, [event]);
|
|
2939
|
+
}
|
|
2940
|
+
return map;
|
|
2941
|
+
}, /* @ts-ignore */
|
|
2942
|
+
...(ngDevMode ? [{ debugName: "eventsByDay" }] : /* istanbul ignore next */ []));
|
|
2943
|
+
/** 6×7 grid of day cells with their (capped) events resolved. */
|
|
2944
|
+
weekCells = computed(() => {
|
|
2945
|
+
const month = this.viewMonth();
|
|
2946
|
+
const today = new Date();
|
|
2947
|
+
const max = this.maxPerDay();
|
|
2948
|
+
const byDay = this.eventsByDay();
|
|
2949
|
+
return this.weeks().map((week) => week.map((date) => {
|
|
2950
|
+
const dayEvents = byDay.get(startOfDay(date).getTime()) ?? [];
|
|
2951
|
+
const cell = {
|
|
2952
|
+
date,
|
|
2953
|
+
outside: !isSameMonth(date, month),
|
|
2954
|
+
today: isSameDay(date, today),
|
|
2955
|
+
visible: dayEvents.slice(0, max),
|
|
2956
|
+
overflow: Math.max(0, dayEvents.length - max),
|
|
2957
|
+
};
|
|
2958
|
+
// Label formatted once per rebuild, not on every CD pass (42 cells).
|
|
2959
|
+
return { ...cell, label: this.dayLabel(cell) };
|
|
2960
|
+
}));
|
|
2961
|
+
}, /* @ts-ignore */
|
|
2962
|
+
...(ngDevMode ? [{ debugName: "weekCells" }] : /* istanbul ignore next */ []));
|
|
2963
|
+
/**
|
|
2964
|
+
* Screen-reader label for a day cell. The button's `aria-label` replaces its
|
|
2965
|
+
* visual content, so the label also announces the day's events — count plus
|
|
2966
|
+
* up to three titles — e.g. `July 9, 2026, 2 events: Standup, Demo`.
|
|
2967
|
+
* Precomputed into {@link MkDayCell#label} by `weekCells`.
|
|
2968
|
+
*/
|
|
2969
|
+
dayLabel(cell) {
|
|
2970
|
+
const formatted = formatDate(cell.date, 'MMMM d, yyyy', this.i18n.dateNames);
|
|
2971
|
+
const count = cell.visible.length + cell.overflow;
|
|
2972
|
+
if (count === 0)
|
|
2973
|
+
return formatted;
|
|
2974
|
+
const titles = cell.visible
|
|
2975
|
+
.slice(0, 3)
|
|
2976
|
+
.map((e) => e.title)
|
|
2977
|
+
.join(', ');
|
|
2978
|
+
return `${formatted}, ${this.i18n.dayEvents(count, titles)}`;
|
|
2979
|
+
}
|
|
2980
|
+
prevMonth() {
|
|
2981
|
+
this.shiftMonth(-1);
|
|
2982
|
+
}
|
|
2983
|
+
nextMonth() {
|
|
2984
|
+
this.shiftMonth(1);
|
|
2985
|
+
}
|
|
2986
|
+
shiftMonth(delta) {
|
|
2987
|
+
const view = this.view();
|
|
2988
|
+
const next = view === 'month'
|
|
2989
|
+
? addMonths(this.viewDate(), delta)
|
|
2990
|
+
: addDays(this.viewDate(), delta * (view === 'week' ? 7 : 1));
|
|
2991
|
+
this.viewDate.set(next);
|
|
2992
|
+
this.monthChange.emit(next);
|
|
2993
|
+
}
|
|
2994
|
+
onDayClick(d) {
|
|
2995
|
+
this.dayClick.emit(d);
|
|
2996
|
+
}
|
|
2997
|
+
onEventClick(event, calendarEvent) {
|
|
2998
|
+
event.stopPropagation();
|
|
2999
|
+
// A real drag just ended — the browser's trailing synthetic click must not
|
|
3000
|
+
// read as an event activation.
|
|
3001
|
+
if (this.swallowNextClick) {
|
|
3002
|
+
this.swallowNextClick = false;
|
|
3003
|
+
event.preventDefault();
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
this.eventClick.emit(calendarEvent);
|
|
3007
|
+
}
|
|
3008
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEventCalendar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3009
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkEventCalendar, isStandalone: true, selector: "mk-event-calendar", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, viewDate: { classPropertyName: "viewDate", publicName: "viewDate", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, maxPerDay: { classPropertyName: "maxPerDay", publicName: "maxPerDay", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, dayStartHour: { classPropertyName: "dayStartHour", publicName: "dayStartHour", isSignal: true, isRequired: false, transformFunction: null }, dayEndHour: { classPropertyName: "dayEndHour", publicName: "dayEndHour", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, snapMinutes: { classPropertyName: "snapMinutes", publicName: "snapMinutes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewDate: "viewDateChange", view: "viewChange", dayClick: "dayClick", eventClick: "eventClick", monthChange: "monthChange", slotClick: "slotClick", eventMove: "eventMove", eventResize: "eventResize" }, host: { classAttribute: "mk-event-calendar" }, ngImport: i0, template: "<div class=\"mk-event-calendar__header\">\n <button\n type=\"button\"\n class=\"mk-event-calendar__nav\"\n [attr.aria-label]=\"i18n.previousMonth\"\n (click)=\"prevMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M15 5l-7 7 7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span class=\"mk-event-calendar__label\" [id]=\"labelId\" aria-live=\"polite\">{{\n monthLabel()\n }}</span>\n\n <button\n type=\"button\"\n class=\"mk-event-calendar__nav\"\n [attr.aria-label]=\"i18n.nextMonth\"\n (click)=\"nextMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M9 5l7 7-7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n</div>\n\n@if (view() === 'month') {\n<!--\n The days are a plain list of buttons (role=\"group\"), not an ARIA grid \u2014 there\n is no roving-tabindex arrow navigation here, so grid semantics would be wrong.\n-->\n<div class=\"mk-event-calendar__grid\" role=\"group\" [attr.aria-labelledby]=\"labelId\">\n <div class=\"mk-event-calendar__weekdays\" aria-hidden=\"true\">\n @for (wd of weekdays(); track $index) {\n <span class=\"mk-event-calendar__weekday\">\n <abbr [title]=\"wd.full\">{{ wd.short }}</abbr>\n </span>\n }\n </div>\n\n @for (week of weekCells(); track $index) {\n <div class=\"mk-event-calendar__week\">\n @for (cell of week; track cell.date.getTime()) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__cell\"\n [class.mk-event-calendar__cell--outside]=\"cell.outside\"\n [class.mk-event-calendar__cell--today]=\"cell.today\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onDayClick(cell.date)\"\n >\n <span class=\"mk-event-calendar__day-num\">{{\n cell.date.getDate()\n }}</span>\n\n <!--\n Presentational for assistive tech: the day button's aria-label\n already announces the events, and nested interactive content inside\n a button is invalid. The pill (click) is a mouse-only shortcut to\n `eventClick`; keyboard users activate the day (`dayClick`) instead.\n -->\n <span class=\"mk-event-calendar__events\" aria-hidden=\"true\">\n @for (event of cell.visible; track $index) {\n <span\n class=\"mk-event-calendar__pill\"\n [style.background-color]=\"event.color || 'var(--mk-primary)'\"\n [title]=\"event.title\"\n (click)=\"onEventClick($event, event)\"\n >\n {{ event.title }}\n </span>\n }\n @if (cell.overflow > 0) {\n <span class=\"mk-event-calendar__more\">{{\n i18n.moreEvents(cell.overflow)\n }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n</div>\n} @else {\n <div\n class=\"mk-event-calendar__timegrid\"\n role=\"group\"\n [attr.aria-labelledby]=\"labelId\"\n >\n @if (view() === 'week') {\n <div class=\"mk-event-calendar__timegrid-head\" aria-hidden=\"true\">\n <span class=\"mk-event-calendar__gutter-spacer\"></span>\n @for (col of timedColumns(); track col.day.getTime()) {\n <span\n class=\"mk-event-calendar__col-head\"\n [class.mk-event-calendar__col-head--today]=\"col.today\"\n >\n {{ col.head }}\n </span>\n }\n </div>\n }\n\n <div class=\"mk-event-calendar__timegrid-body\">\n <div class=\"mk-event-calendar__gutter\" aria-hidden=\"true\">\n @for (h of hours(); track h) {\n <span class=\"mk-event-calendar__hour-label\">{{ hourLabel(h) }}</span>\n }\n </div>\n\n @for (col of timedColumns(); track col.day.getTime(); let ci = $index) {\n <div\n class=\"mk-event-calendar__col\"\n [class.mk-event-calendar__col--today]=\"col.today\"\n >\n @for (h of hours(); track h) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__slot\"\n [attr.aria-label]=\"col.head + ' ' + hourLabel(h)\"\n (click)=\"onSlotClick(col.day, h)\"\n ></button>\n }\n\n @for (p of col.placements; track $index) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__block\"\n [class.mk-event-calendar__block--editable]=\"editable()\"\n [class.mk-event-calendar__block--dragging]=\"isEditing(p)\"\n [style.top.%]=\"p.top\"\n [style.height.%]=\"blockHeight(p)\"\n [style.left.%]=\"(100 / p.lanes) * p.lane\"\n [style.width.%]=\"100 / p.lanes\"\n [style.transform]=\"blockTransform(p)\"\n [style.background-color]=\"p.event.color || 'var(--mk-primary)'\"\n [title]=\"placementTitle(p)\"\n [attr.aria-label]=\"editable() ? blockAriaLabel(p) : null\"\n [attr.aria-roledescription]=\"editable() ? movableRoledescription : null\"\n (click)=\"onEventClick($event, p.event)\"\n (pointerdown)=\"onBlockPointerDown($event, p, ci)\"\n (keydown)=\"onBlockKeydown($event, p, ci)\"\n (blur)=\"onBlockBlur()\"\n >\n @if (dragLabelFor(p); as lbl) {\n <span class=\"mk-event-calendar__drag-time\" aria-hidden=\"true\">{{\n lbl\n }}</span>\n }\n {{ p.event.title }}\n @if (editable()) {\n <span\n class=\"mk-event-calendar__resize-handle\"\n aria-hidden=\"true\"\n ></span>\n }\n </button>\n }\n\n @if (dropOutlineFor(ci); as outline) {\n <div\n class=\"mk-event-calendar__drop-outline\"\n aria-hidden=\"true\"\n [style.top.%]=\"outline.top\"\n [style.height.%]=\"outline.outlineHeight\"\n ></div>\n }\n\n @if (col.allDay.length) {\n <span class=\"mk-event-calendar__all-day\" aria-hidden=\"true\">\n @for (event of col.allDay; track $index) {\n <span\n class=\"mk-event-calendar__pill\"\n [style.background-color]=\"event.color || 'var(--mk-primary)'\"\n [title]=\"event.title\"\n (click)=\"onEventClick($event, event)\"\n >\n {{ event.title }}\n </span>\n }\n </span>\n }\n </div>\n }\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;padding:var(--mk-space-4);font-family:var(--mk-font-sans);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md)}.mk-event-calendar__header{display:flex;align-items:center;gap:var(--mk-space-1);margin-bottom:var(--mk-space-3)}.mk-event-calendar__label{flex:1 1 auto;text-align:center;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-md);white-space:nowrap}.mk-event-calendar__nav{display:inline-flex;align-items:center;justify-content:center;width:var(--mk-control-height-sm);height:var(--mk-control-height-sm);flex:none;color:var(--mk-text-muted);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-event-calendar__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-event-calendar__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-event-calendar__nav svg{display:block}.mk-event-calendar__weekdays,.mk-event-calendar__week{display:grid;grid-template-columns:repeat(7,1fr)}.mk-event-calendar__weekday{padding:0 0 var(--mk-space-2);text-align:center;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);letter-spacing:var(--mk-letter-spacing-wide);text-transform:uppercase;color:var(--mk-text-subtle)}.mk-event-calendar__weekday abbr{text-decoration:none;cursor:default}.mk-event-calendar__cell{display:flex;flex-direction:column;align-items:stretch;gap:var(--mk-space-1);min-height:5.5rem;padding:var(--mk-space-1);font-family:inherit;text-align:start;color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid var(--mk-border-subtle);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-event-calendar__cell:hover{background-color:var(--mk-hover-overlay)}.mk-event-calendar__cell:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1)}.mk-event-calendar__cell--outside{color:var(--mk-text-muted);background-color:var(--mk-surface-2)}.mk-event-calendar__day-num{align-self:flex-start;display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 .25rem;font-size:var(--mk-font-size-sm);line-height:1;border-radius:var(--mk-radius-md)}.mk-event-calendar__cell--today .mk-event-calendar__day-num{color:var(--mk-primary-contrast);background-color:var(--mk-primary);font-weight:var(--mk-font-weight-semibold)}.mk-event-calendar__events{display:flex;flex-direction:column;gap:2px;min-width:0}.mk-event-calendar__pill{display:block;overflow:hidden;padding:1px var(--mk-space-1);font-size:var(--mk-font-size-xs);line-height:1.4;color:var(--mk-primary-contrast);white-space:nowrap;text-overflow:ellipsis;border-radius:var(--mk-radius-md);cursor:pointer}.mk-event-calendar__more{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}@media(prefers-reduced-motion:reduce){.mk-event-calendar__nav,.mk-event-calendar__cell{transition:none}}.mk-event-calendar__timegrid{border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);overflow:hidden}.mk-event-calendar__timegrid-head{display:flex;border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-event-calendar__gutter-spacer{flex:0 0 3.25rem}.mk-event-calendar__col-head{flex:1;padding:var(--mk-space-2);text-align:center;font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-event-calendar__col-head--today{color:var(--mk-primary);font-weight:var(--mk-font-weight-semibold)}.mk-event-calendar__timegrid-body{display:flex}.mk-event-calendar__gutter{flex:0 0 3.25rem;display:flex;flex-direction:column}.mk-event-calendar__hour-label{flex:1 0 3rem;padding:2px var(--mk-space-2) 0 0;text-align:right;font-size:var(--mk-font-size-xs);color:var(--mk-text-subtle)}.mk-event-calendar__col{position:relative;flex:1;display:flex;flex-direction:column;border-inline-start:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-event-calendar__col--today{background:color-mix(in srgb,var(--mk-primary) 4%,transparent)}.mk-event-calendar__slot{flex:1 0 3rem;border:0;border-top:var(--mk-border-width) solid var(--mk-border-subtle);background:transparent;cursor:pointer;padding:0}.mk-event-calendar__slot:hover{background:var(--mk-hover-overlay)}.mk-event-calendar__slot:focus-visible{outline:2px solid var(--mk-focus-ring);outline-offset:-2px}.mk-event-calendar__block{position:absolute;z-index:1;overflow:hidden;margin:1px;padding:2px var(--mk-space-1);border:0;border-radius:var(--mk-radius-sm);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs);text-align:start;cursor:pointer;min-height:1rem}.mk-event-calendar__block:focus-visible{outline:2px solid var(--mk-focus-ring);outline-offset:1px}.mk-event-calendar__block--editable{touch-action:pan-y;-webkit-user-select:none;user-select:none}.mk-event-calendar__block--dragging{z-index:3;opacity:.85;box-shadow:var(--mk-shadow-lg);will-change:transform}.mk-event-calendar__drag-time{display:block;font-weight:var(--mk-font-weight-semibold);white-space:nowrap}.mk-event-calendar__resize-handle{position:absolute;inset-inline:0;bottom:0;height:6px;cursor:ns-resize}@media(pointer:coarse){.mk-event-calendar__resize-handle:before{content:\"\";position:absolute;inset:min(0px,(100% - 24px) / 2)}}.mk-event-calendar__drop-outline{position:absolute;inset-inline:1px;z-index:2;pointer-events:none;border:var(--mk-border-width-strong) dashed var(--mk-primary);border-radius:var(--mk-radius-sm);background:color-mix(in srgb,var(--mk-primary) 8%,transparent)}.mk-event-calendar__all-day{position:absolute;inset-inline:2px;top:2px;z-index:2;display:flex;gap:2px;flex-wrap:wrap}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3010
|
+
}
|
|
3011
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEventCalendar, decorators: [{
|
|
3012
|
+
type: Component,
|
|
3013
|
+
args: [{ selector: 'mk-event-calendar', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
3014
|
+
class: 'mk-event-calendar',
|
|
3015
|
+
}, template: "<div class=\"mk-event-calendar__header\">\n <button\n type=\"button\"\n class=\"mk-event-calendar__nav\"\n [attr.aria-label]=\"i18n.previousMonth\"\n (click)=\"prevMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M15 5l-7 7 7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span class=\"mk-event-calendar__label\" [id]=\"labelId\" aria-live=\"polite\">{{\n monthLabel()\n }}</span>\n\n <button\n type=\"button\"\n class=\"mk-event-calendar__nav\"\n [attr.aria-label]=\"i18n.nextMonth\"\n (click)=\"nextMonth()\"\n >\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n <path\n d=\"M9 5l7 7-7 7\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n</div>\n\n@if (view() === 'month') {\n<!--\n The days are a plain list of buttons (role=\"group\"), not an ARIA grid \u2014 there\n is no roving-tabindex arrow navigation here, so grid semantics would be wrong.\n-->\n<div class=\"mk-event-calendar__grid\" role=\"group\" [attr.aria-labelledby]=\"labelId\">\n <div class=\"mk-event-calendar__weekdays\" aria-hidden=\"true\">\n @for (wd of weekdays(); track $index) {\n <span class=\"mk-event-calendar__weekday\">\n <abbr [title]=\"wd.full\">{{ wd.short }}</abbr>\n </span>\n }\n </div>\n\n @for (week of weekCells(); track $index) {\n <div class=\"mk-event-calendar__week\">\n @for (cell of week; track cell.date.getTime()) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__cell\"\n [class.mk-event-calendar__cell--outside]=\"cell.outside\"\n [class.mk-event-calendar__cell--today]=\"cell.today\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onDayClick(cell.date)\"\n >\n <span class=\"mk-event-calendar__day-num\">{{\n cell.date.getDate()\n }}</span>\n\n <!--\n Presentational for assistive tech: the day button's aria-label\n already announces the events, and nested interactive content inside\n a button is invalid. The pill (click) is a mouse-only shortcut to\n `eventClick`; keyboard users activate the day (`dayClick`) instead.\n -->\n <span class=\"mk-event-calendar__events\" aria-hidden=\"true\">\n @for (event of cell.visible; track $index) {\n <span\n class=\"mk-event-calendar__pill\"\n [style.background-color]=\"event.color || 'var(--mk-primary)'\"\n [title]=\"event.title\"\n (click)=\"onEventClick($event, event)\"\n >\n {{ event.title }}\n </span>\n }\n @if (cell.overflow > 0) {\n <span class=\"mk-event-calendar__more\">{{\n i18n.moreEvents(cell.overflow)\n }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n</div>\n} @else {\n <div\n class=\"mk-event-calendar__timegrid\"\n role=\"group\"\n [attr.aria-labelledby]=\"labelId\"\n >\n @if (view() === 'week') {\n <div class=\"mk-event-calendar__timegrid-head\" aria-hidden=\"true\">\n <span class=\"mk-event-calendar__gutter-spacer\"></span>\n @for (col of timedColumns(); track col.day.getTime()) {\n <span\n class=\"mk-event-calendar__col-head\"\n [class.mk-event-calendar__col-head--today]=\"col.today\"\n >\n {{ col.head }}\n </span>\n }\n </div>\n }\n\n <div class=\"mk-event-calendar__timegrid-body\">\n <div class=\"mk-event-calendar__gutter\" aria-hidden=\"true\">\n @for (h of hours(); track h) {\n <span class=\"mk-event-calendar__hour-label\">{{ hourLabel(h) }}</span>\n }\n </div>\n\n @for (col of timedColumns(); track col.day.getTime(); let ci = $index) {\n <div\n class=\"mk-event-calendar__col\"\n [class.mk-event-calendar__col--today]=\"col.today\"\n >\n @for (h of hours(); track h) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__slot\"\n [attr.aria-label]=\"col.head + ' ' + hourLabel(h)\"\n (click)=\"onSlotClick(col.day, h)\"\n ></button>\n }\n\n @for (p of col.placements; track $index) {\n <button\n type=\"button\"\n class=\"mk-event-calendar__block\"\n [class.mk-event-calendar__block--editable]=\"editable()\"\n [class.mk-event-calendar__block--dragging]=\"isEditing(p)\"\n [style.top.%]=\"p.top\"\n [style.height.%]=\"blockHeight(p)\"\n [style.left.%]=\"(100 / p.lanes) * p.lane\"\n [style.width.%]=\"100 / p.lanes\"\n [style.transform]=\"blockTransform(p)\"\n [style.background-color]=\"p.event.color || 'var(--mk-primary)'\"\n [title]=\"placementTitle(p)\"\n [attr.aria-label]=\"editable() ? blockAriaLabel(p) : null\"\n [attr.aria-roledescription]=\"editable() ? movableRoledescription : null\"\n (click)=\"onEventClick($event, p.event)\"\n (pointerdown)=\"onBlockPointerDown($event, p, ci)\"\n (keydown)=\"onBlockKeydown($event, p, ci)\"\n (blur)=\"onBlockBlur()\"\n >\n @if (dragLabelFor(p); as lbl) {\n <span class=\"mk-event-calendar__drag-time\" aria-hidden=\"true\">{{\n lbl\n }}</span>\n }\n {{ p.event.title }}\n @if (editable()) {\n <span\n class=\"mk-event-calendar__resize-handle\"\n aria-hidden=\"true\"\n ></span>\n }\n </button>\n }\n\n @if (dropOutlineFor(ci); as outline) {\n <div\n class=\"mk-event-calendar__drop-outline\"\n aria-hidden=\"true\"\n [style.top.%]=\"outline.top\"\n [style.height.%]=\"outline.outlineHeight\"\n ></div>\n }\n\n @if (col.allDay.length) {\n <span class=\"mk-event-calendar__all-day\" aria-hidden=\"true\">\n @for (event of col.allDay; track $index) {\n <span\n class=\"mk-event-calendar__pill\"\n [style.background-color]=\"event.color || 'var(--mk-primary)'\"\n [title]=\"event.title\"\n (click)=\"onEventClick($event, event)\"\n >\n {{ event.title }}\n </span>\n }\n </span>\n }\n </div>\n }\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;padding:var(--mk-space-4);font-family:var(--mk-font-sans);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md)}.mk-event-calendar__header{display:flex;align-items:center;gap:var(--mk-space-1);margin-bottom:var(--mk-space-3)}.mk-event-calendar__label{flex:1 1 auto;text-align:center;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-md);white-space:nowrap}.mk-event-calendar__nav{display:inline-flex;align-items:center;justify-content:center;width:var(--mk-control-height-sm);height:var(--mk-control-height-sm);flex:none;color:var(--mk-text-muted);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-event-calendar__nav:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-event-calendar__nav:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-event-calendar__nav svg{display:block}.mk-event-calendar__weekdays,.mk-event-calendar__week{display:grid;grid-template-columns:repeat(7,1fr)}.mk-event-calendar__weekday{padding:0 0 var(--mk-space-2);text-align:center;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);letter-spacing:var(--mk-letter-spacing-wide);text-transform:uppercase;color:var(--mk-text-subtle)}.mk-event-calendar__weekday abbr{text-decoration:none;cursor:default}.mk-event-calendar__cell{display:flex;flex-direction:column;align-items:stretch;gap:var(--mk-space-1);min-height:5.5rem;padding:var(--mk-space-1);font-family:inherit;text-align:start;color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid var(--mk-border-subtle);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-event-calendar__cell:hover{background-color:var(--mk-hover-overlay)}.mk-event-calendar__cell:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(var(--mk-focus-ring-offset) * -1)}.mk-event-calendar__cell--outside{color:var(--mk-text-muted);background-color:var(--mk-surface-2)}.mk-event-calendar__day-num{align-self:flex-start;display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 .25rem;font-size:var(--mk-font-size-sm);line-height:1;border-radius:var(--mk-radius-md)}.mk-event-calendar__cell--today .mk-event-calendar__day-num{color:var(--mk-primary-contrast);background-color:var(--mk-primary);font-weight:var(--mk-font-weight-semibold)}.mk-event-calendar__events{display:flex;flex-direction:column;gap:2px;min-width:0}.mk-event-calendar__pill{display:block;overflow:hidden;padding:1px var(--mk-space-1);font-size:var(--mk-font-size-xs);line-height:1.4;color:var(--mk-primary-contrast);white-space:nowrap;text-overflow:ellipsis;border-radius:var(--mk-radius-md);cursor:pointer}.mk-event-calendar__more{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}@media(prefers-reduced-motion:reduce){.mk-event-calendar__nav,.mk-event-calendar__cell{transition:none}}.mk-event-calendar__timegrid{border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);overflow:hidden}.mk-event-calendar__timegrid-head{display:flex;border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-event-calendar__gutter-spacer{flex:0 0 3.25rem}.mk-event-calendar__col-head{flex:1;padding:var(--mk-space-2);text-align:center;font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-event-calendar__col-head--today{color:var(--mk-primary);font-weight:var(--mk-font-weight-semibold)}.mk-event-calendar__timegrid-body{display:flex}.mk-event-calendar__gutter{flex:0 0 3.25rem;display:flex;flex-direction:column}.mk-event-calendar__hour-label{flex:1 0 3rem;padding:2px var(--mk-space-2) 0 0;text-align:right;font-size:var(--mk-font-size-xs);color:var(--mk-text-subtle)}.mk-event-calendar__col{position:relative;flex:1;display:flex;flex-direction:column;border-inline-start:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-event-calendar__col--today{background:color-mix(in srgb,var(--mk-primary) 4%,transparent)}.mk-event-calendar__slot{flex:1 0 3rem;border:0;border-top:var(--mk-border-width) solid var(--mk-border-subtle);background:transparent;cursor:pointer;padding:0}.mk-event-calendar__slot:hover{background:var(--mk-hover-overlay)}.mk-event-calendar__slot:focus-visible{outline:2px solid var(--mk-focus-ring);outline-offset:-2px}.mk-event-calendar__block{position:absolute;z-index:1;overflow:hidden;margin:1px;padding:2px var(--mk-space-1);border:0;border-radius:var(--mk-radius-sm);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs);text-align:start;cursor:pointer;min-height:1rem}.mk-event-calendar__block:focus-visible{outline:2px solid var(--mk-focus-ring);outline-offset:1px}.mk-event-calendar__block--editable{touch-action:pan-y;-webkit-user-select:none;user-select:none}.mk-event-calendar__block--dragging{z-index:3;opacity:.85;box-shadow:var(--mk-shadow-lg);will-change:transform}.mk-event-calendar__drag-time{display:block;font-weight:var(--mk-font-weight-semibold);white-space:nowrap}.mk-event-calendar__resize-handle{position:absolute;inset-inline:0;bottom:0;height:6px;cursor:ns-resize}@media(pointer:coarse){.mk-event-calendar__resize-handle:before{content:\"\";position:absolute;inset:min(0px,(100% - 24px) / 2)}}.mk-event-calendar__drop-outline{position:absolute;inset-inline:1px;z-index:2;pointer-events:none;border:var(--mk-border-width-strong) dashed var(--mk-primary);border-radius:var(--mk-radius-sm);background:color-mix(in srgb,var(--mk-primary) 8%,transparent)}.mk-event-calendar__all-day{position:absolute;inset-inline:2px;top:2px;z-index:2;display:flex;gap:2px;flex-wrap:wrap}\n"] }]
|
|
3016
|
+
}], propDecorators: { events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], viewDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewDate", required: false }] }, { type: i0.Output, args: ["viewDateChange"] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], maxPerDay: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPerDay", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], dayStartHour: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayStartHour", required: false }] }], dayEndHour: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayEndHour", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], snapMinutes: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapMinutes", required: false }] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }], monthChange: [{ type: i0.Output, args: ["monthChange"] }], slotClick: [{ type: i0.Output, args: ["slotClick"] }], eventMove: [{ type: i0.Output, args: ["eventMove"] }], eventResize: [{ type: i0.Output, args: ["eventResize"] }] } });
|
|
3017
|
+
|
|
3018
|
+
/** Number of days in month `m` (1–12) of `year`. */
|
|
3019
|
+
function daysInMonth(m, year) {
|
|
3020
|
+
return new Date(year, m, 0).getDate();
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* MiniDate — a compact, popover-free inline date editor with segmented
|
|
3024
|
+
* day / month / year spin fields (like a native `<input type="date">` but
|
|
3025
|
+
* themed and signal-driven). Each segment is an ARIA `spinbutton`: Arrow
|
|
3026
|
+
* Up/Down step the value (day wraps within the month), Home/End jump to the
|
|
3027
|
+
* segment's minimum/maximum, Arrow Left/Right move between segments, and
|
|
3028
|
+
* typing digits fills the focused segment and auto-advances (OTP-style). Implements `ControlValueAccessor` and a two-way
|
|
3029
|
+
* `value` model over a real `Date | null`.
|
|
3030
|
+
*
|
|
3031
|
+
* A non-null `Date` is emitted only when all three segments are set and valid;
|
|
3032
|
+
* the day is clamped to the month's length (e.g. Feb 31 → 28/29) and the
|
|
3033
|
+
* result is clamped to `[min, max]`.
|
|
3034
|
+
*
|
|
3035
|
+
* ```html
|
|
3036
|
+
* <mk-mini-date [(value)]="date" order="dmy" [min]="minDate" />
|
|
3037
|
+
* ```
|
|
3038
|
+
*/
|
|
3039
|
+
class MkMiniDate {
|
|
3040
|
+
field = inject(MkFormField, { optional: true });
|
|
3041
|
+
i18n = inject(MK_I18N);
|
|
3042
|
+
segEls = viewChildren('seg', /* @ts-ignore */
|
|
3043
|
+
...(ngDevMode ? [{ debugName: "segEls" }] : /* istanbul ignore next */ []));
|
|
3044
|
+
/** Minimum selectable date (inclusive). */
|
|
3045
|
+
min = input(null, /* @ts-ignore */
|
|
3046
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
3047
|
+
/** Maximum selectable date (inclusive). */
|
|
3048
|
+
max = input(null, /* @ts-ignore */
|
|
3049
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
3050
|
+
/** Two-way value — a real `Date`, or `null` while incomplete. */
|
|
3051
|
+
value = model(null, /* @ts-ignore */
|
|
3052
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
3053
|
+
/** Disable the control. */
|
|
3054
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
3055
|
+
/** Force invalid styling when standalone. */
|
|
3056
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
3057
|
+
/** Control size. Ignored inside an `mk-form-field`. */
|
|
3058
|
+
size = input('md', /* @ts-ignore */
|
|
3059
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
3060
|
+
/** Segment order. */
|
|
3061
|
+
order = input('dmy', /* @ts-ignore */
|
|
3062
|
+
...(ngDevMode ? [{ debugName: "order" }] : /* istanbul ignore next */ []));
|
|
3063
|
+
/** Lowest selectable year (for the year spinbutton range). */
|
|
3064
|
+
yearMin = input(1900, { ...(ngDevMode ? { debugName: "yearMin" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
3065
|
+
/** Highest selectable year (for the year spinbutton range). */
|
|
3066
|
+
yearMax = input(2100, { ...(ngDevMode ? { debugName: "yearMax" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
3067
|
+
segId = mkUniqueId('mk-mini-date');
|
|
3068
|
+
// --- Segment state --------------------------------------------------------
|
|
3069
|
+
daySeg = signal(null, /* @ts-ignore */
|
|
3070
|
+
...(ngDevMode ? [{ debugName: "daySeg" }] : /* istanbul ignore next */ []));
|
|
3071
|
+
monthSeg = signal(null, /* @ts-ignore */
|
|
3072
|
+
...(ngDevMode ? [{ debugName: "monthSeg" }] : /* istanbul ignore next */ []));
|
|
3073
|
+
yearSeg = signal(null, /* @ts-ignore */
|
|
3074
|
+
...(ngDevMode ? [{ debugName: "yearSeg" }] : /* istanbul ignore next */ []));
|
|
3075
|
+
/** Per-segment digit buffer used for OTP-style typing/auto-advance. */
|
|
3076
|
+
buffer = '';
|
|
3077
|
+
bufferKey = null;
|
|
3078
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
3079
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
3080
|
+
onChange = () => { };
|
|
3081
|
+
onTouched = () => { };
|
|
3082
|
+
effectiveSize = computed(() => this.field ? this.field.size() : this.size(), /* @ts-ignore */
|
|
3083
|
+
...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
|
|
3084
|
+
isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
3085
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
3086
|
+
isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
|
|
3087
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
3088
|
+
isRequired = computed(() => this.field?.isRequired() ?? false, /* @ts-ignore */
|
|
3089
|
+
...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
|
|
3090
|
+
labelledBy = computed(() => this.field?.labelId ?? null, /* @ts-ignore */
|
|
3091
|
+
...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
3092
|
+
describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
|
|
3093
|
+
...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
3094
|
+
/** Days available in the current month/year (defaults to a leap year). */
|
|
3095
|
+
monthLength = computed(() => daysInMonth(this.monthSeg() ?? 1, this.yearSeg() ?? 2000), /* @ts-ignore */
|
|
3096
|
+
...(ngDevMode ? [{ debugName: "monthLength" }] : /* istanbul ignore next */ []));
|
|
3097
|
+
/** Ordered, rendered segment descriptors. */
|
|
3098
|
+
parts = computed(() => {
|
|
3099
|
+
const names = this.i18n.dateNames;
|
|
3100
|
+
const byKey = {
|
|
3101
|
+
day: {
|
|
3102
|
+
key: 'day',
|
|
3103
|
+
display: this.pad(this.daySeg(), 2, 'dd'),
|
|
3104
|
+
filled: this.daySeg() != null,
|
|
3105
|
+
valueNow: this.daySeg(),
|
|
3106
|
+
valueMin: 1,
|
|
3107
|
+
valueMax: this.monthLength(),
|
|
3108
|
+
valueText: this.daySeg() != null ? String(this.daySeg()) : this.i18n.empty,
|
|
3109
|
+
label: this.i18n.daySegment,
|
|
3110
|
+
},
|
|
3111
|
+
month: {
|
|
3112
|
+
key: 'month',
|
|
3113
|
+
display: this.monthSeg() != null
|
|
3114
|
+
? names.monthsShort[this.monthSeg() - 1]
|
|
3115
|
+
: 'mmm',
|
|
3116
|
+
filled: this.monthSeg() != null,
|
|
3117
|
+
valueNow: this.monthSeg(),
|
|
3118
|
+
valueMin: 1,
|
|
3119
|
+
valueMax: 12,
|
|
3120
|
+
valueText: this.monthSeg() != null
|
|
3121
|
+
? names.months[this.monthSeg() - 1]
|
|
3122
|
+
: this.i18n.empty,
|
|
3123
|
+
label: this.i18n.monthSegment,
|
|
3124
|
+
},
|
|
3125
|
+
year: {
|
|
3126
|
+
key: 'year',
|
|
3127
|
+
display: this.pad(this.yearSeg(), 4, 'yyyy'),
|
|
3128
|
+
filled: this.yearSeg() != null,
|
|
3129
|
+
valueNow: this.yearSeg(),
|
|
3130
|
+
valueMin: this.yearMin(),
|
|
3131
|
+
valueMax: this.yearMax(),
|
|
3132
|
+
valueText: this.yearSeg() != null ? String(this.yearSeg()) : this.i18n.empty,
|
|
3133
|
+
label: this.i18n.yearSegment,
|
|
3134
|
+
},
|
|
3135
|
+
};
|
|
3136
|
+
return this.order()
|
|
3137
|
+
.split('')
|
|
3138
|
+
.map((c) => c === 'd' ? byKey.day : c === 'm' ? byKey.month : byKey.year);
|
|
3139
|
+
}, /* @ts-ignore */
|
|
3140
|
+
...(ngDevMode ? [{ debugName: "parts" }] : /* istanbul ignore next */ []));
|
|
3141
|
+
// --- Interaction ----------------------------------------------------------
|
|
3142
|
+
onSegKeydown(key, index, e) {
|
|
3143
|
+
if (this.isDisabled())
|
|
3144
|
+
return;
|
|
3145
|
+
switch (e.key) {
|
|
3146
|
+
case 'ArrowUp':
|
|
3147
|
+
e.preventDefault();
|
|
3148
|
+
this.step(key, 1);
|
|
3149
|
+
break;
|
|
3150
|
+
case 'ArrowDown':
|
|
3151
|
+
e.preventDefault();
|
|
3152
|
+
this.step(key, -1);
|
|
3153
|
+
break;
|
|
3154
|
+
case 'ArrowLeft':
|
|
3155
|
+
e.preventDefault();
|
|
3156
|
+
this.focusSeg(index - 1);
|
|
3157
|
+
break;
|
|
3158
|
+
case 'ArrowRight':
|
|
3159
|
+
e.preventDefault();
|
|
3160
|
+
this.focusSeg(index + 1);
|
|
3161
|
+
break;
|
|
3162
|
+
case 'Home':
|
|
3163
|
+
e.preventDefault();
|
|
3164
|
+
this.setSegment(key, this.segMin(key));
|
|
3165
|
+
this.resetBuffer();
|
|
3166
|
+
break;
|
|
3167
|
+
case 'End':
|
|
3168
|
+
e.preventDefault();
|
|
3169
|
+
this.setSegment(key, this.segMax(key));
|
|
3170
|
+
this.resetBuffer();
|
|
3171
|
+
break;
|
|
3172
|
+
case 'Backspace':
|
|
3173
|
+
case 'Delete':
|
|
3174
|
+
e.preventDefault();
|
|
3175
|
+
this.setSegment(key, null);
|
|
3176
|
+
this.resetBuffer();
|
|
3177
|
+
if (e.key === 'Backspace')
|
|
3178
|
+
this.focusSeg(index - 1);
|
|
3179
|
+
break;
|
|
3180
|
+
default:
|
|
3181
|
+
if (/^[0-9]$/.test(e.key)) {
|
|
3182
|
+
e.preventDefault();
|
|
3183
|
+
this.typeDigit(key, index, e.key);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
onSegFocus(key) {
|
|
3188
|
+
// A fresh focus starts a new typing buffer for that segment.
|
|
3189
|
+
if (this.bufferKey !== key)
|
|
3190
|
+
this.resetBuffer();
|
|
3191
|
+
}
|
|
3192
|
+
onBlur() {
|
|
3193
|
+
this.onTouched();
|
|
3194
|
+
}
|
|
3195
|
+
/** A segment's minimum value (Home key). */
|
|
3196
|
+
segMin(key) {
|
|
3197
|
+
return key === 'year' ? this.yearMin() : 1;
|
|
3198
|
+
}
|
|
3199
|
+
/** A segment's maximum value (End key). */
|
|
3200
|
+
segMax(key) {
|
|
3201
|
+
if (key === 'day')
|
|
3202
|
+
return this.monthLength();
|
|
3203
|
+
if (key === 'month')
|
|
3204
|
+
return 12;
|
|
3205
|
+
return this.yearMax();
|
|
3206
|
+
}
|
|
3207
|
+
/** Step a segment by `delta`; the day wraps within the current month. */
|
|
3208
|
+
step(key, delta) {
|
|
3209
|
+
if (key === 'day') {
|
|
3210
|
+
const len = this.monthLength();
|
|
3211
|
+
const cur = this.daySeg() ?? (delta > 0 ? 0 : len + 1);
|
|
3212
|
+
const next = ((cur - 1 + delta) % len + len) % len + 1;
|
|
3213
|
+
this.setSegment('day', next);
|
|
3214
|
+
}
|
|
3215
|
+
else if (key === 'month') {
|
|
3216
|
+
const cur = this.monthSeg() ?? (delta > 0 ? 0 : 13);
|
|
3217
|
+
const next = ((cur - 1 + delta) % 12 + 12) % 12 + 1;
|
|
3218
|
+
this.setSegment('month', next);
|
|
3219
|
+
}
|
|
3220
|
+
else {
|
|
3221
|
+
const base = this.yearSeg() ?? new Date().getFullYear() - delta;
|
|
3222
|
+
const next = Math.min(this.yearMax(), Math.max(this.yearMin(), base + delta));
|
|
3223
|
+
this.setSegment('year', next);
|
|
3224
|
+
}
|
|
3225
|
+
this.resetBuffer();
|
|
3226
|
+
}
|
|
3227
|
+
/** OTP-style digit entry with auto-advance to the next segment when full. */
|
|
3228
|
+
typeDigit(key, index, digit) {
|
|
3229
|
+
if (this.bufferKey !== key)
|
|
3230
|
+
this.buffer = '';
|
|
3231
|
+
this.bufferKey = key;
|
|
3232
|
+
const maxDigits = key === 'year' ? 4 : 2;
|
|
3233
|
+
const max = key === 'day'
|
|
3234
|
+
? this.monthLength()
|
|
3235
|
+
: key === 'month'
|
|
3236
|
+
? 12
|
|
3237
|
+
: this.yearMax();
|
|
3238
|
+
let buf = this.buffer + digit;
|
|
3239
|
+
let num = Number(buf);
|
|
3240
|
+
// If the running value overflows the max, restart the buffer with this digit.
|
|
3241
|
+
if (num > max || buf.length > maxDigits) {
|
|
3242
|
+
buf = digit;
|
|
3243
|
+
num = Number(buf);
|
|
3244
|
+
}
|
|
3245
|
+
this.buffer = buf;
|
|
3246
|
+
this.setSegment(key, num === 0 ? null : num);
|
|
3247
|
+
// Advance when the segment is saturated (full width, or another digit
|
|
3248
|
+
// could only overflow the max).
|
|
3249
|
+
const full = buf.length >= maxDigits || num * 10 > max;
|
|
3250
|
+
if (full) {
|
|
3251
|
+
this.resetBuffer();
|
|
3252
|
+
this.focusSeg(index + 1);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
/** Set a segment, then rebuild + emit the composite date. */
|
|
3256
|
+
setSegment(key, value) {
|
|
3257
|
+
if (key === 'day')
|
|
3258
|
+
this.daySeg.set(value);
|
|
3259
|
+
else if (key === 'month')
|
|
3260
|
+
this.monthSeg.set(value);
|
|
3261
|
+
else
|
|
3262
|
+
this.yearSeg.set(value);
|
|
3263
|
+
this.recompute();
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Rebuild the `Date` from the three segments and emit it. Emits `null` unless
|
|
3267
|
+
* all three are set; clamps the day to the month length and the whole date to
|
|
3268
|
+
* `[min, max]`, writing any clamp back into the segments.
|
|
3269
|
+
*/
|
|
3270
|
+
recompute() {
|
|
3271
|
+
const d = this.daySeg();
|
|
3272
|
+
const m = this.monthSeg();
|
|
3273
|
+
const y = this.yearSeg();
|
|
3274
|
+
if (d == null || m == null || y == null) {
|
|
3275
|
+
this.emit(null);
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
const len = daysInMonth(m, y);
|
|
3279
|
+
const clampedDay = Math.min(d, len);
|
|
3280
|
+
if (clampedDay !== d)
|
|
3281
|
+
this.daySeg.set(clampedDay);
|
|
3282
|
+
const raw = new Date(y, m - 1, clampedDay);
|
|
3283
|
+
const bounded = clampDate(startOfDay(raw), this.min(), this.max());
|
|
3284
|
+
if (!isSameDay(bounded, raw))
|
|
3285
|
+
this.split(bounded);
|
|
3286
|
+
this.emit(startOfDay(bounded));
|
|
3287
|
+
}
|
|
3288
|
+
emit(v) {
|
|
3289
|
+
this.value.set(v);
|
|
3290
|
+
this.onChange(v);
|
|
3291
|
+
}
|
|
3292
|
+
/** Populate the three segments from a `Date`. */
|
|
3293
|
+
split(date) {
|
|
3294
|
+
this.daySeg.set(date.getDate());
|
|
3295
|
+
this.monthSeg.set(date.getMonth() + 1);
|
|
3296
|
+
this.yearSeg.set(date.getFullYear());
|
|
3297
|
+
}
|
|
3298
|
+
resetBuffer() {
|
|
3299
|
+
this.buffer = '';
|
|
3300
|
+
this.bufferKey = null;
|
|
3301
|
+
}
|
|
3302
|
+
focusSeg(index) {
|
|
3303
|
+
const els = this.segEls();
|
|
3304
|
+
const el = els[Math.max(0, Math.min(els.length - 1, index))];
|
|
3305
|
+
el?.nativeElement.focus();
|
|
3306
|
+
this.resetBuffer();
|
|
3307
|
+
}
|
|
3308
|
+
pad(v, width, placeholder) {
|
|
3309
|
+
return v == null ? placeholder : String(v).padStart(width, '0');
|
|
3310
|
+
}
|
|
3311
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
3312
|
+
writeValue(value) {
|
|
3313
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
3314
|
+
this.split(value);
|
|
3315
|
+
}
|
|
3316
|
+
else {
|
|
3317
|
+
this.daySeg.set(null);
|
|
3318
|
+
this.monthSeg.set(null);
|
|
3319
|
+
this.yearSeg.set(null);
|
|
3320
|
+
}
|
|
3321
|
+
this.value.set(value ?? null);
|
|
3322
|
+
this.resetBuffer();
|
|
3323
|
+
}
|
|
3324
|
+
registerOnChange(fn) {
|
|
3325
|
+
this.onChange = fn;
|
|
3326
|
+
}
|
|
3327
|
+
registerOnTouched(fn) {
|
|
3328
|
+
this.onTouched = fn;
|
|
3329
|
+
}
|
|
3330
|
+
setDisabledState(isDisabled) {
|
|
3331
|
+
this.cvaDisabled.set(isDisabled);
|
|
3332
|
+
}
|
|
3333
|
+
// --- Validator ------------------------------------------------------------
|
|
3334
|
+
validatorChange = mkValidatorChange(() => {
|
|
3335
|
+
this.min();
|
|
3336
|
+
this.max();
|
|
3337
|
+
});
|
|
3338
|
+
/**
|
|
3339
|
+
* Reports `mkMinDate` / `mkMaxDate` against the `[min]` and `[max]` inputs.
|
|
3340
|
+
*/
|
|
3341
|
+
validate(control) {
|
|
3342
|
+
const v = control.value;
|
|
3343
|
+
if (!(v instanceof Date) || Number.isNaN(v.getTime()))
|
|
3344
|
+
return null;
|
|
3345
|
+
const day = startOfDay(v);
|
|
3346
|
+
const min = this.min();
|
|
3347
|
+
if (min && isBefore(day, startOfDay(min)))
|
|
3348
|
+
return { mkMinDate: { min, actual: v } };
|
|
3349
|
+
const max = this.max();
|
|
3350
|
+
if (max && isAfter(day, startOfDay(max)))
|
|
3351
|
+
return { mkMaxDate: { max, actual: v } };
|
|
3352
|
+
return null;
|
|
3353
|
+
}
|
|
3354
|
+
registerOnValidatorChange(fn) {
|
|
3355
|
+
this.validatorChange.register(fn);
|
|
3356
|
+
}
|
|
3357
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMiniDate, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3358
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkMiniDate, isStandalone: true, selector: "mk-mini-date", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: false, transformFunction: null }, yearMin: { classPropertyName: "yearMin", publicName: "yearMin", isSignal: true, isRequired: false, transformFunction: null }, yearMax: { classPropertyName: "yearMax", publicName: "yearMax", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelledBy()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "isInvalid() || null", "attr.aria-required": "isRequired() || null", "class.mk-mini-date--sm": "effectiveSize() === 'sm'", "class.mk-mini-date--lg": "effectiveSize() === 'lg'", "class.mk-mini-date--invalid": "isInvalid()", "class.mk-mini-date--disabled": "isDisabled()" }, classAttribute: "mk-mini-date" }, providers: [
|
|
3359
|
+
{
|
|
3360
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3361
|
+
useExisting: forwardRef(() => MkMiniDate),
|
|
3362
|
+
multi: true,
|
|
3363
|
+
},
|
|
3364
|
+
{
|
|
3365
|
+
provide: NG_VALIDATORS,
|
|
3366
|
+
useExisting: forwardRef(() => MkMiniDate),
|
|
3367
|
+
multi: true,
|
|
3368
|
+
},
|
|
3369
|
+
], viewQueries: [{ propertyName: "segEls", predicate: ["seg"], descendants: true, isSignal: true }], exportAs: ["mkMiniDate"], ngImport: i0, template: "@for (part of parts(); track part.key; let last = $last) {\n <span\n #seg\n class=\"mk-mini-date__seg\"\n [class.mk-mini-date__seg--empty]=\"!part.filled\"\n [id]=\"segId + '-' + part.key\"\n role=\"spinbutton\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-label]=\"part.label\"\n [attr.aria-valuenow]=\"part.valueNow\"\n [attr.aria-valuemin]=\"part.valueMin\"\n [attr.aria-valuemax]=\"part.valueMax\"\n [attr.aria-valuetext]=\"part.valueText\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (keydown)=\"onSegKeydown(part.key, $index, $event)\"\n (focus)=\"onSegFocus(part.key)\"\n (blur)=\"onBlur()\"\n >{{ part.display }}</span>\n @if (!last) {\n <span class=\"mk-mini-date__sep\" aria-hidden=\"true\">/</span>\n }\n}\n", styles: [":host{display:inline-flex;align-items:center;gap:.125rem;height:var(--mk-control-height-md);padding:0 var(--mk-space-2);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);color:var(--mk-text);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-mini-date--sm){height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm);font-size:var(--mk-font-size-sm)}:host(.mk-mini-date--lg){height:var(--mk-control-height-lg);font-size:var(--mk-font-size-lg)}:host(:focus-within){border-color:var(--mk-primary);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}:host(.mk-mini-date--invalid){border-color:var(--mk-danger)}:host(.mk-mini-date--disabled){background-color:var(--mk-surface-2);opacity:.7;pointer-events:none}.mk-mini-date__seg{padding:0 var(--mk-space-1);border-radius:var(--mk-radius-sm);font-variant-numeric:tabular-nums;text-align:center;cursor:default;outline:none;-webkit-user-select:none;user-select:none}.mk-mini-date__seg--empty{color:var(--mk-text-subtle)}.mk-mini-date__seg:focus-visible,.mk-mini-date__seg:focus{background-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-mini-date__sep{color:var(--mk-text-muted);-webkit-user-select:none;user-select:none}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3370
|
+
}
|
|
3371
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMiniDate, decorators: [{
|
|
3372
|
+
type: Component,
|
|
3373
|
+
args: [{ selector: 'mk-mini-date', changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'mkMiniDate', host: {
|
|
3374
|
+
class: 'mk-mini-date',
|
|
3375
|
+
role: 'group',
|
|
3376
|
+
'[attr.aria-labelledby]': 'labelledBy()',
|
|
3377
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
3378
|
+
'[attr.aria-invalid]': 'isInvalid() || null',
|
|
3379
|
+
'[attr.aria-required]': 'isRequired() || null',
|
|
3380
|
+
'[class.mk-mini-date--sm]': "effectiveSize() === 'sm'",
|
|
3381
|
+
'[class.mk-mini-date--lg]': "effectiveSize() === 'lg'",
|
|
3382
|
+
'[class.mk-mini-date--invalid]': 'isInvalid()',
|
|
3383
|
+
'[class.mk-mini-date--disabled]': 'isDisabled()',
|
|
3384
|
+
}, providers: [
|
|
3385
|
+
{
|
|
3386
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3387
|
+
useExisting: forwardRef(() => MkMiniDate),
|
|
3388
|
+
multi: true,
|
|
3389
|
+
},
|
|
3390
|
+
{
|
|
3391
|
+
provide: NG_VALIDATORS,
|
|
3392
|
+
useExisting: forwardRef(() => MkMiniDate),
|
|
3393
|
+
multi: true,
|
|
3394
|
+
},
|
|
3395
|
+
], template: "@for (part of parts(); track part.key; let last = $last) {\n <span\n #seg\n class=\"mk-mini-date__seg\"\n [class.mk-mini-date__seg--empty]=\"!part.filled\"\n [id]=\"segId + '-' + part.key\"\n role=\"spinbutton\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-label]=\"part.label\"\n [attr.aria-valuenow]=\"part.valueNow\"\n [attr.aria-valuemin]=\"part.valueMin\"\n [attr.aria-valuemax]=\"part.valueMax\"\n [attr.aria-valuetext]=\"part.valueText\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (keydown)=\"onSegKeydown(part.key, $index, $event)\"\n (focus)=\"onSegFocus(part.key)\"\n (blur)=\"onBlur()\"\n >{{ part.display }}</span>\n @if (!last) {\n <span class=\"mk-mini-date__sep\" aria-hidden=\"true\">/</span>\n }\n}\n", styles: [":host{display:inline-flex;align-items:center;gap:.125rem;height:var(--mk-control-height-md);padding:0 var(--mk-space-2);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);color:var(--mk-text);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-mini-date--sm){height:var(--mk-control-height-sm);border-radius:var(--mk-radius-sm);font-size:var(--mk-font-size-sm)}:host(.mk-mini-date--lg){height:var(--mk-control-height-lg);font-size:var(--mk-font-size-lg)}:host(:focus-within){border-color:var(--mk-primary);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}:host(.mk-mini-date--invalid){border-color:var(--mk-danger)}:host(.mk-mini-date--disabled){background-color:var(--mk-surface-2);opacity:.7;pointer-events:none}.mk-mini-date__seg{padding:0 var(--mk-space-1);border-radius:var(--mk-radius-sm);font-variant-numeric:tabular-nums;text-align:center;cursor:default;outline:none;-webkit-user-select:none;user-select:none}.mk-mini-date__seg--empty{color:var(--mk-text-subtle)}.mk-mini-date__seg:focus-visible,.mk-mini-date__seg:focus{background-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-mini-date__sep{color:var(--mk-text-muted);-webkit-user-select:none;user-select:none}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"] }]
|
|
3396
|
+
}], propDecorators: { segEls: [{ type: i0.ViewChildren, args: ['seg', { isSignal: true }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], order: [{ type: i0.Input, args: [{ isSignal: true, alias: "order", required: false }] }], yearMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "yearMin", required: false }] }], yearMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "yearMax", required: false }] }] } });
|
|
3397
|
+
|
|
3398
|
+
/**
|
|
3399
|
+
* DATE & TIME components group barrel for @mk-kit/ui.
|
|
3400
|
+
* Re-exports every public component, the shared date helpers, and the
|
|
3401
|
+
* `MkDateRange` type.
|
|
3402
|
+
*/
|
|
3403
|
+
|
|
3404
|
+
/**
|
|
3405
|
+
* Generated bundle index. Do not edit.
|
|
3406
|
+
*/
|
|
3407
|
+
|
|
3408
|
+
export { MkCalendar, MkDatePicker, MkDateRangePicker, MkEventCalendar, MkMiniDate, MkMonthPicker, MkTimePicker, MkWeekPicker, addDays, addMonths, buildMonthMatrix, clampDate, endOfMonth, endOfWeek, formatDate, formatISODate, getISOWeek, getMonthNames, getWeekdayFullName, getWeekdayNames, isAfter, isBefore, isSameDay, isSameMonth, parseISODate, startOfDay, startOfMonth, startOfWeek };
|
|
3409
|
+
//# sourceMappingURL=mk-kit-ui-datetime.mjs.map
|