@besaitech/ng-design-system 0.0.77 → 0.0.79
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/calendar/index.d.ts +277 -0
- package/fesm2022/besaitech-ng-design-system-calendar.mjs +1184 -0
- package/fesm2022/besaitech-ng-design-system-calendar.mjs.map +1 -0
- package/fesm2022/besaitech-ng-design-system.mjs +946 -43
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +228 -26
- package/package.json +5 -1
- package/themes/base.css +53 -1
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
import { startOfDay, addDays, isSameDay, SdsIcon, SdsSegmented } from '@besaitech/ng-design-system';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { inject, DestroyRef, ElementRef, input, model, numberAttribute, booleanAttribute, output, signal, effect, untracked, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
const MS_PER_MINUTE = 60_000;
|
|
6
|
+
function toDate(value) {
|
|
7
|
+
return value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
8
|
+
}
|
|
9
|
+
/** Minutes since local midnight. Read off the clock, never from an epoch difference — see below. */
|
|
10
|
+
function minutesOfDay(d) {
|
|
11
|
+
return d.getHours() * 60 + d.getMinutes();
|
|
12
|
+
}
|
|
13
|
+
function startOfWeek(d, firstDayOfWeek) {
|
|
14
|
+
const day = startOfDay(d);
|
|
15
|
+
const shift = (day.getDay() - firstDayOfWeek + 7) % 7;
|
|
16
|
+
return addDays(day, -shift);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The days a view shows.
|
|
20
|
+
*
|
|
21
|
+
* <p>`month` is always a 6×7 block, never 4–6 ragged rows: a grid that changes height as you page
|
|
22
|
+
* through the year makes everything below it jump.
|
|
23
|
+
*/
|
|
24
|
+
function visibleDays(view, date, firstDayOfWeek) {
|
|
25
|
+
if (view === 'day') {
|
|
26
|
+
return [startOfDay(date)];
|
|
27
|
+
}
|
|
28
|
+
if (view === '3day') {
|
|
29
|
+
const from = startOfDay(date);
|
|
30
|
+
return [0, 1, 2].map((i) => addDays(from, i));
|
|
31
|
+
}
|
|
32
|
+
if (view === 'week') {
|
|
33
|
+
const from = startOfWeek(date, firstDayOfWeek);
|
|
34
|
+
return Array.from({ length: 7 }, (_, i) => addDays(from, i));
|
|
35
|
+
}
|
|
36
|
+
const first = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
37
|
+
const from = startOfWeek(first, firstDayOfWeek);
|
|
38
|
+
return Array.from({ length: 42 }, (_, i) => addDays(from, i));
|
|
39
|
+
}
|
|
40
|
+
/** The half-open range a view covers — what a host should fetch for. */
|
|
41
|
+
function visibleRange(view, date, firstDayOfWeek) {
|
|
42
|
+
const days = visibleDays(view, date, firstDayOfWeek);
|
|
43
|
+
return { start: days[0], end: addDays(days[days.length - 1], 1) };
|
|
44
|
+
}
|
|
45
|
+
/** Paging: one month, one week, three days or one day at a time, in the view's own unit. */
|
|
46
|
+
function step(view, date, direction) {
|
|
47
|
+
switch (view) {
|
|
48
|
+
case 'month': {
|
|
49
|
+
const d = new Date(date.getFullYear(), date.getMonth() + direction, 1);
|
|
50
|
+
return d;
|
|
51
|
+
}
|
|
52
|
+
case 'week':
|
|
53
|
+
return addDays(date, 7 * direction);
|
|
54
|
+
case '3day':
|
|
55
|
+
return addDays(date, 3 * direction);
|
|
56
|
+
default:
|
|
57
|
+
return addDays(date, direction);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Whether an event touches a given day at all. Half-open: an event ending at 00:00 is not there. */
|
|
61
|
+
function occursOn(start, end, day) {
|
|
62
|
+
const from = startOfDay(day).getTime();
|
|
63
|
+
const to = addDays(startOfDay(day), 1).getTime();
|
|
64
|
+
return start.getTime() < to && end.getTime() > from;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Place one day's timed events in the grid, side by side where they overlap.
|
|
68
|
+
*
|
|
69
|
+
* <p>The classic column packing: events are clustered into runs that transitively overlap, each
|
|
70
|
+
* takes the leftmost column that is free at its time, and the whole cluster is then divided by the
|
|
71
|
+
* widest point. So two appointments at 09:00 each take half the width and a third takes a third —
|
|
72
|
+
* and a 14:00 appointment with nothing beside it is full width, because it is in its own cluster.
|
|
73
|
+
*
|
|
74
|
+
* <p><b>Vertical position is read off the clock, not from an epoch difference.</b> On the two days a
|
|
75
|
+
* year when the clocks move, a 09:00–10:00 appointment is still drawn at 09:00 and one hour tall;
|
|
76
|
+
* computing it from elapsed milliseconds would slide every event after the change by an hour.
|
|
77
|
+
*
|
|
78
|
+
* <p>Events are clipped to the visible window rather than dropped: a night shift starting at 06:00
|
|
79
|
+
* on a grid that opens at 08:00 should still show, flush to the top, not vanish.
|
|
80
|
+
*/
|
|
81
|
+
function placeDay(events, day, startHour, endHour, defaultMinutes) {
|
|
82
|
+
const windowStart = startHour * 60;
|
|
83
|
+
const windowEnd = endHour * 60;
|
|
84
|
+
const span = Math.max(1, windowEnd - windowStart);
|
|
85
|
+
const dayStart = startOfDay(day);
|
|
86
|
+
const nextDay = addDays(dayStart, 1);
|
|
87
|
+
const resolved = events
|
|
88
|
+
.map((event) => {
|
|
89
|
+
const start = toDate(event.start);
|
|
90
|
+
const end = event.end
|
|
91
|
+
? toDate(event.end)
|
|
92
|
+
: new Date(start.getTime() + defaultMinutes * MS_PER_MINUTE);
|
|
93
|
+
return { event, start, end };
|
|
94
|
+
})
|
|
95
|
+
.filter((e) => !e.event.allDay && occursOn(e.start, e.end, day))
|
|
96
|
+
.map((e) => {
|
|
97
|
+
// Clip to this day first, so a block running past midnight is drawn on both days correctly.
|
|
98
|
+
const from = e.start < dayStart ? dayStart : e.start;
|
|
99
|
+
const to = e.end > nextDay ? nextDay : e.end;
|
|
100
|
+
const fromMin = from.getTime() === dayStart.getTime() ? 0 : minutesOfDay(from);
|
|
101
|
+
const toMin = to.getTime() === nextDay.getTime() ? 24 * 60 : minutesOfDay(to);
|
|
102
|
+
return { ...e, fromMin, toMin: Math.max(toMin, fromMin + 1) };
|
|
103
|
+
})
|
|
104
|
+
.filter((e) => e.toMin > windowStart && e.fromMin < windowEnd)
|
|
105
|
+
.sort((a, b) => a.fromMin - b.fromMin || b.toMin - a.toMin);
|
|
106
|
+
// Cluster transitively overlapping events; each cluster is divided independently.
|
|
107
|
+
const placed = [];
|
|
108
|
+
let cluster = [];
|
|
109
|
+
let clusterEnd = -1;
|
|
110
|
+
const flush = () => {
|
|
111
|
+
if (!cluster.length)
|
|
112
|
+
return;
|
|
113
|
+
const columnEnds = [];
|
|
114
|
+
const columnOf = new Map();
|
|
115
|
+
for (const item of cluster) {
|
|
116
|
+
let column = columnEnds.findIndex((endMin) => endMin <= item.fromMin);
|
|
117
|
+
if (column === -1) {
|
|
118
|
+
column = columnEnds.length;
|
|
119
|
+
}
|
|
120
|
+
columnEnds[column] = item.toMin;
|
|
121
|
+
columnOf.set(item, column);
|
|
122
|
+
}
|
|
123
|
+
const columns = columnEnds.length;
|
|
124
|
+
for (const item of cluster) {
|
|
125
|
+
const top = (Math.max(item.fromMin, windowStart) - windowStart) / span;
|
|
126
|
+
const bottom = (Math.min(item.toMin, windowEnd) - windowStart) / span;
|
|
127
|
+
const column = columnOf.get(item) ?? 0;
|
|
128
|
+
placed.push({
|
|
129
|
+
event: item.event,
|
|
130
|
+
start: item.start,
|
|
131
|
+
end: item.end,
|
|
132
|
+
top,
|
|
133
|
+
height: Math.max(bottom - top, 0.012),
|
|
134
|
+
left: column / columns,
|
|
135
|
+
width: 1 / columns,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
cluster = [];
|
|
139
|
+
clusterEnd = -1;
|
|
140
|
+
};
|
|
141
|
+
for (const item of resolved) {
|
|
142
|
+
if (cluster.length && item.fromMin >= clusterEnd) {
|
|
143
|
+
flush();
|
|
144
|
+
}
|
|
145
|
+
cluster.push(item);
|
|
146
|
+
clusterEnd = Math.max(clusterEnd, item.toMin);
|
|
147
|
+
}
|
|
148
|
+
flush();
|
|
149
|
+
return placed;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A dragged minute, snapped to the grid and held inside the visible window.
|
|
153
|
+
*
|
|
154
|
+
* <p>Clamping is not cosmetic: without it a drag that leaves the top of the grid proposes a time
|
|
155
|
+
* before the practice opens, and the host either books it or has to invent the same rule itself.
|
|
156
|
+
*/
|
|
157
|
+
function snapMinute(minute, stepMinutes, startHour, endHour) {
|
|
158
|
+
const step = Math.max(1, stepMinutes);
|
|
159
|
+
return Math.min(endHour * 60, Math.max(startHour * 60, Math.round(minute / step) * step));
|
|
160
|
+
}
|
|
161
|
+
/** The all-day (and multi-day) events touching a given day, in a stable order. */
|
|
162
|
+
function allDayOn(events, day, defaultMinutes) {
|
|
163
|
+
return events
|
|
164
|
+
.map((event) => {
|
|
165
|
+
const start = toDate(event.start);
|
|
166
|
+
const end = event.end
|
|
167
|
+
? toDate(event.end)
|
|
168
|
+
: new Date(start.getTime() + defaultMinutes * MS_PER_MINUTE);
|
|
169
|
+
return { event, start, end };
|
|
170
|
+
})
|
|
171
|
+
.filter((e) => e.event.allDay && occursOn(e.start, e.end, day))
|
|
172
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
|
173
|
+
.map((e) => e.event);
|
|
174
|
+
}
|
|
175
|
+
/** Every event touching a day, all-day first then by start — what a month cell lists. */
|
|
176
|
+
function eventsOn(events, day, defaultMinutes) {
|
|
177
|
+
return events
|
|
178
|
+
.map((event) => {
|
|
179
|
+
const start = toDate(event.start);
|
|
180
|
+
const end = event.end
|
|
181
|
+
? toDate(event.end)
|
|
182
|
+
: new Date(start.getTime() + defaultMinutes * MS_PER_MINUTE);
|
|
183
|
+
return { event, start, end };
|
|
184
|
+
})
|
|
185
|
+
.filter((e) => occursOn(e.start, e.end, day))
|
|
186
|
+
.sort((a, b) => Number(!!b.event.allDay) - Number(!!a.event.allDay) ||
|
|
187
|
+
a.start.getTime() - b.start.getTime())
|
|
188
|
+
.map((e) => e.event);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Written out in full — a composed `bg-accent-${c}-soft` would produce no CSS. See sds-severity. */
|
|
192
|
+
const CHIP = {
|
|
193
|
+
brand: 'bg-brand-500/15 text-brand-700 dark:text-brand-200 border-brand-500/40',
|
|
194
|
+
blue: 'bg-accent-blue-soft text-accent-blue border-accent-blue/40',
|
|
195
|
+
green: 'bg-accent-green-soft text-accent-green border-accent-green/40',
|
|
196
|
+
amber: 'bg-accent-amber-soft text-accent-amber border-accent-amber/40',
|
|
197
|
+
red: 'bg-accent-red-soft text-accent-red border-accent-red/40',
|
|
198
|
+
indigo: 'bg-accent-indigo-soft text-accent-indigo border-accent-indigo/40',
|
|
199
|
+
neutral: 'bg-muted text-ink-soft border-border',
|
|
200
|
+
};
|
|
201
|
+
const DOT = {
|
|
202
|
+
brand: 'bg-brand-500',
|
|
203
|
+
blue: 'bg-accent-blue',
|
|
204
|
+
green: 'bg-accent-green',
|
|
205
|
+
amber: 'bg-accent-amber',
|
|
206
|
+
red: 'bg-accent-red',
|
|
207
|
+
indigo: 'bg-accent-indigo',
|
|
208
|
+
neutral: 'bg-ink-faint',
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* A calendar of events — month, week, three days or one day.
|
|
212
|
+
*
|
|
213
|
+
* <p><b>Not {@link SdsCalendar}</b>, which is the month grid inside the date picker and exists to
|
|
214
|
+
* pick a date. This one shows what is ON the days, and it owns no data: the host passes `events`
|
|
215
|
+
* and listens to `rangeChange` to fetch the next lot.
|
|
216
|
+
*
|
|
217
|
+
* <p><b>It stores nothing and decides nothing.</b> `view` and `date` are two-way models, so the
|
|
218
|
+
* host can deep-link a view or restore one; every interaction is an output. That is what lets the
|
|
219
|
+
* same component serve a doctor's diary, a room's bookings or a delivery schedule without knowing
|
|
220
|
+
* what any of them are.
|
|
221
|
+
*
|
|
222
|
+
* <p><b>Dates come from `Intl`, control words come from you.</b> Day and month names follow
|
|
223
|
+
* `[locale]` automatically, so the part that is genuinely per-language needs no wiring — but
|
|
224
|
+
* "Today", "All day" and "+2 more" are inputs with English defaults, and a non-English app MUST
|
|
225
|
+
* pass them. An icon-only control's accessible name is the only name it has.
|
|
226
|
+
*
|
|
227
|
+
* <p><b>Mobile.</b> The week view keeps a readable minimum column width and scrolls sideways under
|
|
228
|
+
* its own header rather than squeezing seven columns onto a phone; the month view drops from chips
|
|
229
|
+
* to dots below `sm`. Both are CSS, so there is no resize listener to get wrong and no layout that
|
|
230
|
+
* depends on JavaScript having run.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* <sds-event-calendar
|
|
234
|
+
* [events]="appointments()"
|
|
235
|
+
* [(view)]="view"
|
|
236
|
+
* [(date)]="focused"
|
|
237
|
+
* [todayLabel]="'calendar.today' | translate"
|
|
238
|
+
* (eventClick)="open($event)"
|
|
239
|
+
* (rangeChange)="load($event)" />
|
|
240
|
+
*/
|
|
241
|
+
class SdsEventCalendar {
|
|
242
|
+
destroyRef = inject(DestroyRef);
|
|
243
|
+
host = inject(ElementRef);
|
|
244
|
+
events = input([], ...(ngDevMode ? [{ debugName: "events" }] : []));
|
|
245
|
+
/** Two-way: the host can deep-link or restore a view. */
|
|
246
|
+
view = model('week', ...(ngDevMode ? [{ debugName: "view" }] : []));
|
|
247
|
+
/** Two-way: any day inside the span being shown. */
|
|
248
|
+
date = model(new Date(), ...(ngDevMode ? [{ debugName: "date" }] : []));
|
|
249
|
+
/** 1 = Monday (the European week), 0 = Sunday. */
|
|
250
|
+
firstDayOfWeek = input(1, ...(ngDevMode ? [{ debugName: "firstDayOfWeek", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
251
|
+
/** The hours the time grid shows. Events outside are clipped to the edge, never dropped. */
|
|
252
|
+
startHour = input(7, ...(ngDevMode ? [{ debugName: "startHour", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
253
|
+
endHour = input(20, ...(ngDevMode ? [{ debugName: "endHour", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
254
|
+
hourHeight = input(48, ...(ngDevMode ? [{ debugName: "hourHeight", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
255
|
+
/** How long an event with no `end` is assumed to last. */
|
|
256
|
+
defaultMinutes = input(30, ...(ngDevMode ? [{ debugName: "defaultMinutes", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
257
|
+
/** CSS height of the scrolling body (month: of the whole grid). */
|
|
258
|
+
height = input('32rem', ...(ngDevMode ? [{ debugName: "height" }] : []));
|
|
259
|
+
/** BCP-47 tag for day and month names. Empty follows the browser. */
|
|
260
|
+
locale = input('', ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
261
|
+
nowIndicator = input(true, ...(ngDevMode ? [{ debugName: "nowIndicator", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
262
|
+
showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
263
|
+
showViewSwitcher = input(true, ...(ngDevMode ? [{ debugName: "showViewSwitcher", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
264
|
+
/** Chips per month cell before it collapses to "+N". */
|
|
265
|
+
maxPerCell = input(3, ...(ngDevMode ? [{ debugName: "maxPerCell", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
266
|
+
/** Control words. English defaults — a non-English app must pass every one of these. */
|
|
267
|
+
todayLabel = input('Today', ...(ngDevMode ? [{ debugName: "todayLabel" }] : []));
|
|
268
|
+
previousLabel = input('Previous', ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
|
|
269
|
+
nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
|
|
270
|
+
allDayLabel = input('All day', ...(ngDevMode ? [{ debugName: "allDayLabel" }] : []));
|
|
271
|
+
/** `{count}` is substituted. */
|
|
272
|
+
moreLabel = input('+{count} more', ...(ngDevMode ? [{ debugName: "moreLabel" }] : []));
|
|
273
|
+
viewSwitcherLabel = input('View', ...(ngDevMode ? [{ debugName: "viewSwitcherLabel" }] : []));
|
|
274
|
+
monthLabel = input('Month', ...(ngDevMode ? [{ debugName: "monthLabel" }] : []));
|
|
275
|
+
weekLabel = input('Week', ...(ngDevMode ? [{ debugName: "weekLabel" }] : []));
|
|
276
|
+
threeDayLabel = input('3 days', ...(ngDevMode ? [{ debugName: "threeDayLabel" }] : []));
|
|
277
|
+
dayLabel = input('Day', ...(ngDevMode ? [{ debugName: "dayLabel" }] : []));
|
|
278
|
+
/**
|
|
279
|
+
* Let an event be dragged to another time or day. <b>Opt-in</b>: a calendar whose host does not
|
|
280
|
+
* persist the move would show an appointment settle somewhere it has not been booked, which is
|
|
281
|
+
* worse than not offering the gesture.
|
|
282
|
+
*/
|
|
283
|
+
draggable = input(false, ...(ngDevMode ? [{ debugName: "draggable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
284
|
+
/** Drag across empty time to propose a span. The plain click keeps emitting `slotClick`. */
|
|
285
|
+
selectable = input(true, ...(ngDevMode ? [{ debugName: "selectable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
286
|
+
/** Both gestures snap to this. 15 minutes is the finest grid anybody reads off a week view. */
|
|
287
|
+
snapMinutes = input(15, ...(ngDevMode ? [{ debugName: "snapMinutes", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
288
|
+
eventClick = output();
|
|
289
|
+
/**
|
|
290
|
+
* An event was dragged somewhere else. The component does NOT move it: it reports where it was
|
|
291
|
+
* dropped and redraws from `events` as before, so the diary only changes once the host says it
|
|
292
|
+
* did. A drag that moved the chip locally would show a booking that does not exist whenever the
|
|
293
|
+
* write is refused.
|
|
294
|
+
*/
|
|
295
|
+
eventDrop = output();
|
|
296
|
+
/** A span was dragged out across empty time — "book something from here to here". */
|
|
297
|
+
rangeSelect = output();
|
|
298
|
+
/** A click on empty time, snapped to the hour that was clicked. */
|
|
299
|
+
slotClick = output();
|
|
300
|
+
dayClick = output();
|
|
301
|
+
/** The span now on screen — fetch for this. Fires whenever the view or the date moves. */
|
|
302
|
+
rangeChange = output();
|
|
303
|
+
/** Ticks the now-indicator. One timer, cleared with the component. */
|
|
304
|
+
now = signal(new Date(), ...(ngDevMode ? [{ debugName: "now" }] : []));
|
|
305
|
+
/** The gesture in flight, if any. Null between gestures — there is never more than one. */
|
|
306
|
+
drag = signal(null, ...(ngDevMode ? [{ debugName: "drag" }] : []));
|
|
307
|
+
/** Set once the pointer has moved far enough to be a drag rather than a click. */
|
|
308
|
+
dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
|
|
309
|
+
/**
|
|
310
|
+
* A finished drag still produces a click, and it arrives AFTER pointerup — so checking
|
|
311
|
+
* "am I dragging" inside the click handler is always false by then. Measured: dropping an event
|
|
312
|
+
* emitted `eventDrop` AND `slotClick`, so a host both moved the appointment and opened a
|
|
313
|
+
* "new appointment" panel for where the finger lifted. This survives until the click it has to
|
|
314
|
+
* swallow, and is cleared either way.
|
|
315
|
+
*/
|
|
316
|
+
suppressClick = false;
|
|
317
|
+
constructor() {
|
|
318
|
+
const timer = setInterval(() => this.now.set(new Date()), 60_000);
|
|
319
|
+
this.destroyRef.onDestroy(() => clearInterval(timer));
|
|
320
|
+
/*
|
|
321
|
+
* Announce the visible span whenever it changes, HOWEVER it changed. Emitting from the toolbar
|
|
322
|
+
* handlers instead would look identical until a host moved `date` itself — a deep link, a
|
|
323
|
+
* "jump to this appointment", restoring a remembered day — and then the calendar would show a
|
|
324
|
+
* week nobody fetched. The models are two-way, so the host is a first-class navigator.
|
|
325
|
+
*/
|
|
326
|
+
effect(() => {
|
|
327
|
+
const view = this.view();
|
|
328
|
+
const { start, end } = visibleRange(view, this.date(), this.firstDayOfWeek());
|
|
329
|
+
untracked(() => this.rangeChange.emit({ start, end, view }));
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
days = computed(() => visibleDays(this.view(), this.date(), this.firstDayOfWeek()), ...(ngDevMode ? [{ debugName: "days" }] : []));
|
|
333
|
+
hours = computed(() => {
|
|
334
|
+
const out = [];
|
|
335
|
+
for (let h = this.startHour(); h <= this.endHour(); h++)
|
|
336
|
+
out.push(h);
|
|
337
|
+
return out;
|
|
338
|
+
}, ...(ngDevMode ? [{ debugName: "hours" }] : []));
|
|
339
|
+
bodyHeight = computed(() => Math.max(1, this.endHour() - this.startHour()) * this.hourHeight(), ...(ngDevMode ? [{ debugName: "bodyHeight" }] : []));
|
|
340
|
+
columns = computed(() => `3.25rem repeat(${this.days().length}, minmax(0, 1fr))`, ...(ngDevMode ? [{ debugName: "columns" }] : []));
|
|
341
|
+
/** Keeps a day column readable on a phone; the grid scrolls sideways instead of squeezing. */
|
|
342
|
+
minWidth = computed(() => {
|
|
343
|
+
const perDay = this.days().length > 3 ? 92 : 120;
|
|
344
|
+
return 52 + this.days().length * perDay;
|
|
345
|
+
}, ...(ngDevMode ? [{ debugName: "minWidth" }] : []));
|
|
346
|
+
intl = computed(() => this.locale() || undefined, ...(ngDevMode ? [{ debugName: "intl" }] : []));
|
|
347
|
+
weekdayNames = computed(() => {
|
|
348
|
+
const fmt = new Intl.DateTimeFormat(this.intl(), { weekday: 'short' });
|
|
349
|
+
const base = startOfDay(new Date(2024, 0, 7)); // a Sunday
|
|
350
|
+
return Array.from({ length: 7 }, (_, i) => fmt.format(addDays(base, (this.firstDayOfWeek() + i) % 7)));
|
|
351
|
+
}, ...(ngDevMode ? [{ debugName: "weekdayNames" }] : []));
|
|
352
|
+
title = computed(() => {
|
|
353
|
+
const days = this.days();
|
|
354
|
+
const view = this.view();
|
|
355
|
+
if (view === 'day') {
|
|
356
|
+
return new Intl.DateTimeFormat(this.intl(), {
|
|
357
|
+
weekday: 'long',
|
|
358
|
+
day: 'numeric',
|
|
359
|
+
month: 'long',
|
|
360
|
+
year: 'numeric',
|
|
361
|
+
}).format(days[0]);
|
|
362
|
+
}
|
|
363
|
+
if (view === 'month') {
|
|
364
|
+
return new Intl.DateTimeFormat(this.intl(), { month: 'long', year: 'numeric' }).format(this.date());
|
|
365
|
+
}
|
|
366
|
+
const first = days[0];
|
|
367
|
+
const last = days[days.length - 1];
|
|
368
|
+
const sameMonth = first.getMonth() === last.getMonth();
|
|
369
|
+
const short = new Intl.DateTimeFormat(this.intl(), { day: 'numeric', month: 'short' });
|
|
370
|
+
const long = new Intl.DateTimeFormat(this.intl(), {
|
|
371
|
+
day: 'numeric',
|
|
372
|
+
month: 'short',
|
|
373
|
+
year: 'numeric',
|
|
374
|
+
});
|
|
375
|
+
return sameMonth
|
|
376
|
+
? `${first.getDate()} – ${long.format(last)}`
|
|
377
|
+
: `${short.format(first)} – ${long.format(last)}`;
|
|
378
|
+
}, ...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
379
|
+
viewOptions = computed(() => [
|
|
380
|
+
{ value: 'month', label: this.monthLabel() },
|
|
381
|
+
{ value: 'week', label: this.weekLabel() },
|
|
382
|
+
{ value: '3day', label: this.threeDayLabel() },
|
|
383
|
+
{ value: 'day', label: this.dayLabel() },
|
|
384
|
+
], ...(ngDevMode ? [{ debugName: "viewOptions" }] : []));
|
|
385
|
+
hasAllDay = computed(() => this.days().some((d) => allDayOn(this.events(), d, this.defaultMinutes()).length > 0), ...(ngDevMode ? [{ debugName: "hasAllDay" }] : []));
|
|
386
|
+
nowTop = computed(() => {
|
|
387
|
+
const minutes = minutesOfDay(this.now());
|
|
388
|
+
const from = this.startHour() * 60;
|
|
389
|
+
const to = this.endHour() * 60;
|
|
390
|
+
if (minutes < from || minutes > to)
|
|
391
|
+
return null;
|
|
392
|
+
return ((minutes - from) / 60) * this.hourHeight();
|
|
393
|
+
}, ...(ngDevMode ? [{ debugName: "nowTop" }] : []));
|
|
394
|
+
placed(day) {
|
|
395
|
+
return placeDay(this.events(), day, this.startHour(), this.endHour(), this.defaultMinutes());
|
|
396
|
+
}
|
|
397
|
+
allDayEvents(day) {
|
|
398
|
+
return allDayOn(this.events(), day, this.defaultMinutes());
|
|
399
|
+
}
|
|
400
|
+
cellEvents(day) {
|
|
401
|
+
return eventsOn(this.events(), day, this.defaultMinutes());
|
|
402
|
+
}
|
|
403
|
+
isToday(day) {
|
|
404
|
+
return isSameDay(day, this.now());
|
|
405
|
+
}
|
|
406
|
+
inFocusedMonth(day) {
|
|
407
|
+
return day.getMonth() === this.date().getMonth();
|
|
408
|
+
}
|
|
409
|
+
shortWeekday(day) {
|
|
410
|
+
return new Intl.DateTimeFormat(this.intl(), { weekday: 'short' }).format(day);
|
|
411
|
+
}
|
|
412
|
+
hourLabel(hour) {
|
|
413
|
+
return new Intl.DateTimeFormat(this.intl(), { hour: 'numeric', minute: '2-digit' }).format(new Date(2024, 0, 1, hour, 0));
|
|
414
|
+
}
|
|
415
|
+
clock(value) {
|
|
416
|
+
return new Intl.DateTimeFormat(this.intl(), { hour: '2-digit', minute: '2-digit' }).format(toDate(value));
|
|
417
|
+
}
|
|
418
|
+
hourTop(hour) {
|
|
419
|
+
return (hour - this.startHour()) * this.hourHeight();
|
|
420
|
+
}
|
|
421
|
+
chipClass(e) {
|
|
422
|
+
return CHIP[e.color ?? 'brand'];
|
|
423
|
+
}
|
|
424
|
+
dotClass(e) {
|
|
425
|
+
return DOT[e.color ?? 'brand'];
|
|
426
|
+
}
|
|
427
|
+
setView(next) {
|
|
428
|
+
this.view.set(next);
|
|
429
|
+
}
|
|
430
|
+
move(direction) {
|
|
431
|
+
this.date.set(step(this.view(), this.date(), direction));
|
|
432
|
+
}
|
|
433
|
+
goToday() {
|
|
434
|
+
this.date.set(new Date());
|
|
435
|
+
}
|
|
436
|
+
/** A click on empty grid becomes a slot of `defaultMinutes`, snapped to the hour clicked. */
|
|
437
|
+
onGridClick(event, day) {
|
|
438
|
+
if (this.swallowed()) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const host = event.currentTarget;
|
|
442
|
+
const y = event.clientY - host.getBoundingClientRect().top;
|
|
443
|
+
const minutes = this.startHour() * 60 + (y / this.hourHeight()) * 60;
|
|
444
|
+
const snapped = Math.floor(minutes / this.defaultMinutes()) * this.defaultMinutes();
|
|
445
|
+
const start = new Date(startOfDay(day).getTime() + snapped * 60_000);
|
|
446
|
+
this.slotClick.emit({
|
|
447
|
+
start,
|
|
448
|
+
end: new Date(start.getTime() + this.defaultMinutes() * 60_000),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// ------------------------------------------------------------------ dragging
|
|
452
|
+
canDrag(placed) {
|
|
453
|
+
return this.draggable() && !placed.event.cancelled;
|
|
454
|
+
}
|
|
455
|
+
isDragging(placed) {
|
|
456
|
+
const drag = this.drag();
|
|
457
|
+
return this.dragging() && drag?.kind === 'move' && drag.placed.event.id === placed.event.id;
|
|
458
|
+
}
|
|
459
|
+
/** The dashed block: where this drag would land on this column, or null. */
|
|
460
|
+
ghostFor(dayIndex) {
|
|
461
|
+
const drag = this.drag();
|
|
462
|
+
if (!drag || !this.dragging() || drag.dayIndex !== dayIndex) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const perMinute = this.hourHeight() / 60;
|
|
466
|
+
const from = this.startHour() * 60;
|
|
467
|
+
const [a, b] = drag.kind === 'move'
|
|
468
|
+
? [drag.atMin, drag.atMin + drag.durationMin]
|
|
469
|
+
: [Math.min(drag.fromMin, drag.atMin), Math.max(drag.fromMin, drag.atMin)];
|
|
470
|
+
const top = (a - from) * perMinute;
|
|
471
|
+
return {
|
|
472
|
+
top,
|
|
473
|
+
height: Math.max((b - a) * perMinute, 2),
|
|
474
|
+
label: `${this.hhmm(a)} – ${this.hhmm(b)}`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
startSelect(event, dayIndex) {
|
|
478
|
+
if (!this.selectable() || event.button !== 0) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const column = event.currentTarget;
|
|
482
|
+
const minute = this.snap(this.minuteAt(event.clientY, column));
|
|
483
|
+
this.begin(event, { kind: 'select', dayIndex, fromMin: minute, atMin: minute });
|
|
484
|
+
}
|
|
485
|
+
startMove(event, placed, dayIndex) {
|
|
486
|
+
if (!this.canDrag(placed) || event.button !== 0) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Stop the column beneath from also starting a range-select under the same finger.
|
|
490
|
+
event.stopPropagation();
|
|
491
|
+
const column = event.currentTarget.closest('[data-sds-day]');
|
|
492
|
+
if (!column) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const startMin = minutesOfDay(placed.start);
|
|
496
|
+
const durationMin = Math.max(this.snapMinutes(), Math.round((placed.end.getTime() - placed.start.getTime()) / 60_000));
|
|
497
|
+
this.begin(event, {
|
|
498
|
+
kind: 'move',
|
|
499
|
+
dayIndex,
|
|
500
|
+
placed,
|
|
501
|
+
durationMin,
|
|
502
|
+
// Where in the block the finger went down, so it does not jump to the top on the first move.
|
|
503
|
+
grabOffsetMin: this.minuteAt(event.clientY, column) - startMin,
|
|
504
|
+
atMin: startMin,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Listeners are attached for the gesture and removed on release, rather than living on the host
|
|
509
|
+
* for the component's whole life. A calendar sitting idle on a dashboard should not be handling
|
|
510
|
+
* every pointer move on the page.
|
|
511
|
+
*/
|
|
512
|
+
begin(event, state) {
|
|
513
|
+
this.drag.set(state);
|
|
514
|
+
this.dragging.set(false);
|
|
515
|
+
const startY = event.clientY;
|
|
516
|
+
const startX = event.clientX;
|
|
517
|
+
const move = (moved) => {
|
|
518
|
+
const current = this.drag();
|
|
519
|
+
if (!current) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// A few pixels of slop, so a click with a shaky hand is still a click.
|
|
523
|
+
if (!this.dragging() &&
|
|
524
|
+
Math.abs(moved.clientY - startY) < 4 &&
|
|
525
|
+
Math.abs(moved.clientX - startX) < 4) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.dragging.set(true);
|
|
529
|
+
const column = this.columnAt(moved.clientX, moved.clientY);
|
|
530
|
+
const dayIndex = column ? Number(column.dataset['sdsDay']) : current.dayIndex;
|
|
531
|
+
const target = column ?? this.columnFor(current.dayIndex);
|
|
532
|
+
if (!target) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const raw = this.minuteAt(moved.clientY, target);
|
|
536
|
+
const atMin = current.kind === 'move' ? this.snap(raw - current.grabOffsetMin) : this.snap(raw);
|
|
537
|
+
this.drag.set({ ...current, dayIndex, atMin });
|
|
538
|
+
};
|
|
539
|
+
const up = () => {
|
|
540
|
+
window.removeEventListener('pointermove', move);
|
|
541
|
+
window.removeEventListener('pointerup', up);
|
|
542
|
+
window.removeEventListener('pointercancel', up);
|
|
543
|
+
const finished = this.drag();
|
|
544
|
+
const moved = this.dragging();
|
|
545
|
+
this.drag.set(null);
|
|
546
|
+
this.dragging.set(false);
|
|
547
|
+
if (finished && moved) {
|
|
548
|
+
this.suppressClick = true;
|
|
549
|
+
// If no click follows (the pointer left the grid, a touch cancel), do not swallow the
|
|
550
|
+
// NEXT genuine one — a flag that outlives its gesture is a dead control.
|
|
551
|
+
setTimeout(() => {
|
|
552
|
+
this.suppressClick = false;
|
|
553
|
+
});
|
|
554
|
+
this.finish(finished);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
window.addEventListener('pointermove', move);
|
|
558
|
+
window.addEventListener('pointerup', up);
|
|
559
|
+
window.addEventListener('pointercancel', up);
|
|
560
|
+
}
|
|
561
|
+
finish(drag) {
|
|
562
|
+
const day = this.days()[drag.dayIndex];
|
|
563
|
+
if (!day) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (drag.kind === 'move') {
|
|
567
|
+
const start = this.atMinute(day, drag.atMin);
|
|
568
|
+
this.eventDrop.emit({
|
|
569
|
+
event: drag.placed.event,
|
|
570
|
+
start,
|
|
571
|
+
end: new Date(start.getTime() + drag.durationMin * 60_000),
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const from = Math.min(drag.fromMin, drag.atMin);
|
|
576
|
+
const to = Math.max(drag.fromMin, drag.atMin);
|
|
577
|
+
if (to - from < this.snapMinutes()) {
|
|
578
|
+
// Not a span — the click handler will treat it as a slot click.
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.rangeSelect.emit({ start: this.atMinute(day, from), end: this.atMinute(day, to) });
|
|
582
|
+
}
|
|
583
|
+
snap(minute) {
|
|
584
|
+
return snapMinute(minute, this.snapMinutes(), this.startHour(), this.endHour());
|
|
585
|
+
}
|
|
586
|
+
minuteAt(clientY, column) {
|
|
587
|
+
const y = clientY - column.getBoundingClientRect().top;
|
|
588
|
+
return this.startHour() * 60 + (y / this.hourHeight()) * 60;
|
|
589
|
+
}
|
|
590
|
+
atMinute(day, minute) {
|
|
591
|
+
return new Date(startOfDay(day).getTime() + minute * 60_000);
|
|
592
|
+
}
|
|
593
|
+
columnAt(x, y) {
|
|
594
|
+
return (document
|
|
595
|
+
.elementsFromPoint(x, y)
|
|
596
|
+
.find((el) => el instanceof HTMLElement && !!el.dataset['sdsDay']) ??
|
|
597
|
+
null);
|
|
598
|
+
}
|
|
599
|
+
columnFor(dayIndex) {
|
|
600
|
+
return this.host.nativeElement.querySelector(`[data-sds-day="${dayIndex}"]`);
|
|
601
|
+
}
|
|
602
|
+
/** True once, for the click that a just-finished drag is about to produce. */
|
|
603
|
+
swallowed() {
|
|
604
|
+
if (!this.suppressClick) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
this.suppressClick = false;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
hhmm(minute) {
|
|
611
|
+
const h = Math.floor(minute / 60);
|
|
612
|
+
const m = Math.round(minute % 60);
|
|
613
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
614
|
+
}
|
|
615
|
+
onEventClick(event, placed) {
|
|
616
|
+
// Without this the click falls through to the grid and also opens "new appointment".
|
|
617
|
+
event.stopPropagation();
|
|
618
|
+
if (this.swallowed()) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.eventClick.emit(placed.event);
|
|
622
|
+
}
|
|
623
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsEventCalendar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
624
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsEventCalendar, isStandalone: true, selector: "sds-event-calendar", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, startHour: { classPropertyName: "startHour", publicName: "startHour", isSignal: true, isRequired: false, transformFunction: null }, endHour: { classPropertyName: "endHour", publicName: "endHour", isSignal: true, isRequired: false, transformFunction: null }, hourHeight: { classPropertyName: "hourHeight", publicName: "hourHeight", isSignal: true, isRequired: false, transformFunction: null }, defaultMinutes: { classPropertyName: "defaultMinutes", publicName: "defaultMinutes", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, nowIndicator: { classPropertyName: "nowIndicator", publicName: "nowIndicator", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showViewSwitcher: { classPropertyName: "showViewSwitcher", publicName: "showViewSwitcher", isSignal: true, isRequired: false, transformFunction: null }, maxPerCell: { classPropertyName: "maxPerCell", publicName: "maxPerCell", isSignal: true, isRequired: false, transformFunction: null }, todayLabel: { classPropertyName: "todayLabel", publicName: "todayLabel", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, allDayLabel: { classPropertyName: "allDayLabel", publicName: "allDayLabel", isSignal: true, isRequired: false, transformFunction: null }, moreLabel: { classPropertyName: "moreLabel", publicName: "moreLabel", isSignal: true, isRequired: false, transformFunction: null }, viewSwitcherLabel: { classPropertyName: "viewSwitcherLabel", publicName: "viewSwitcherLabel", isSignal: true, isRequired: false, transformFunction: null }, monthLabel: { classPropertyName: "monthLabel", publicName: "monthLabel", isSignal: true, isRequired: false, transformFunction: null }, weekLabel: { classPropertyName: "weekLabel", publicName: "weekLabel", isSignal: true, isRequired: false, transformFunction: null }, threeDayLabel: { classPropertyName: "threeDayLabel", publicName: "threeDayLabel", isSignal: true, isRequired: false, transformFunction: null }, dayLabel: { classPropertyName: "dayLabel", publicName: "dayLabel", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, snapMinutes: { classPropertyName: "snapMinutes", publicName: "snapMinutes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { view: "viewChange", date: "dateChange", eventClick: "eventClick", eventDrop: "eventDrop", rangeSelect: "rangeSelect", slotClick: "slotClick", dayClick: "dayClick", rangeChange: "rangeChange" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
625
|
+
<div class="flex flex-col overflow-hidden rounded-card border border-border bg-surface">
|
|
626
|
+
@if (showToolbar()) {
|
|
627
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-border px-3 py-2.5">
|
|
628
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
629
|
+
<button
|
|
630
|
+
type="button"
|
|
631
|
+
[attr.aria-label]="previousLabel()"
|
|
632
|
+
[title]="previousLabel()"
|
|
633
|
+
class="inline-flex size-8 items-center justify-center rounded-control text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
634
|
+
(click)="move(-1)"
|
|
635
|
+
>
|
|
636
|
+
<sds-icon name="chevron-left" [size]="18" />
|
|
637
|
+
</button>
|
|
638
|
+
<button
|
|
639
|
+
type="button"
|
|
640
|
+
class="inline-flex h-8 items-center rounded-control border border-border px-3 text-sm font-medium text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
641
|
+
(click)="goToday()"
|
|
642
|
+
>
|
|
643
|
+
{{ todayLabel() }}
|
|
644
|
+
</button>
|
|
645
|
+
<button
|
|
646
|
+
type="button"
|
|
647
|
+
[attr.aria-label]="nextLabel()"
|
|
648
|
+
[title]="nextLabel()"
|
|
649
|
+
class="inline-flex size-8 items-center justify-center rounded-control text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
650
|
+
(click)="move(1)"
|
|
651
|
+
>
|
|
652
|
+
<sds-icon name="chevron-right" [size]="18" />
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<h2 class="min-w-0 flex-1 truncate text-sm font-semibold text-ink sm:text-base">
|
|
657
|
+
{{ title() }}
|
|
658
|
+
</h2>
|
|
659
|
+
|
|
660
|
+
@if (showViewSwitcher()) {
|
|
661
|
+
<sds-segmented
|
|
662
|
+
size="sm"
|
|
663
|
+
[ariaLabel]="viewSwitcherLabel()"
|
|
664
|
+
[options]="viewOptions()"
|
|
665
|
+
[value]="view()"
|
|
666
|
+
(valueChange)="setView($event)"
|
|
667
|
+
/>
|
|
668
|
+
}
|
|
669
|
+
</div>
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@if (view() === 'month') {
|
|
673
|
+
<!-- ------------------------------------------------------------------ month -->
|
|
674
|
+
<div class="grid grid-cols-7 border-b border-border bg-muted/50">
|
|
675
|
+
@for (d of weekdayNames(); track $index) {
|
|
676
|
+
<div class="px-1 py-1.5 text-center text-[11px] font-medium text-ink-soft">
|
|
677
|
+
{{ d }}
|
|
678
|
+
</div>
|
|
679
|
+
}
|
|
680
|
+
</div>
|
|
681
|
+
<div class="grid grid-cols-7 grid-rows-6" [style.height]="height()">
|
|
682
|
+
@for (day of days(); track day.getTime()) {
|
|
683
|
+
<div
|
|
684
|
+
class="flex min-w-0 flex-col gap-0.5 border-t border-l border-border p-1 first:border-l-0 [&:nth-child(7n+1)]:border-l-0"
|
|
685
|
+
[class.bg-muted]="!inFocusedMonth(day)"
|
|
686
|
+
[class.opacity-60]="!inFocusedMonth(day)"
|
|
687
|
+
>
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
class="self-start rounded-full px-1.5 text-[11px] font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
691
|
+
[class]="
|
|
692
|
+
isToday(day) ? 'bg-brand-500 text-white hover:bg-brand-600' : 'text-ink-soft'
|
|
693
|
+
"
|
|
694
|
+
(click)="dayClick.emit(day)"
|
|
695
|
+
>
|
|
696
|
+
{{ day.getDate() }}
|
|
697
|
+
</button>
|
|
698
|
+
|
|
699
|
+
<!-- Phones get dots: four chips in a 48px-wide cell is four unreadable smears. -->
|
|
700
|
+
<div class="flex flex-wrap items-center gap-0.5 px-0.5 sm:hidden">
|
|
701
|
+
@for (e of cellEvents(day).slice(0, 4); track e.id) {
|
|
702
|
+
<span [class]="dotClass(e) + ' size-1.5 rounded-full'" aria-hidden="true"></span>
|
|
703
|
+
}
|
|
704
|
+
@if (cellEvents(day).length) {
|
|
705
|
+
<span class="sr-only">{{ cellEvents(day).length }}</span>
|
|
706
|
+
}
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
<div class="hidden min-h-0 flex-col gap-0.5 overflow-hidden sm:flex">
|
|
710
|
+
@for (e of cellEvents(day).slice(0, maxPerCell()); track e.id) {
|
|
711
|
+
<button
|
|
712
|
+
type="button"
|
|
713
|
+
[class]="
|
|
714
|
+
chipClass(e) + ' truncate rounded border px-1 py-px text-left text-[11px]'
|
|
715
|
+
"
|
|
716
|
+
[class.line-through]="e.cancelled"
|
|
717
|
+
[class.opacity-60]="e.cancelled"
|
|
718
|
+
(click)="eventClick.emit(e)"
|
|
719
|
+
>
|
|
720
|
+
@if (!e.allDay) {
|
|
721
|
+
<span class="tabular-nums opacity-80">{{ clock(e.start) }}</span>
|
|
722
|
+
}
|
|
723
|
+
{{ e.title }}
|
|
724
|
+
</button>
|
|
725
|
+
}
|
|
726
|
+
@if (cellEvents(day).length > maxPerCell()) {
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
class="px-1 text-left text-[11px] font-medium text-ink-soft hover:text-ink"
|
|
730
|
+
(click)="dayClick.emit(day)"
|
|
731
|
+
>
|
|
732
|
+
{{
|
|
733
|
+
moreLabel().replace('{count}', '' + (cellEvents(day).length - maxPerCell()))
|
|
734
|
+
}}
|
|
735
|
+
</button>
|
|
736
|
+
}
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
}
|
|
740
|
+
</div>
|
|
741
|
+
} @else {
|
|
742
|
+
<!-- ------------------------------------------------- week / 3 days / one day -->
|
|
743
|
+
<div class="overflow-x-auto">
|
|
744
|
+
<div [style.min-width.px]="minWidth()">
|
|
745
|
+
<div class="grid border-b border-border" [style.grid-template-columns]="columns()">
|
|
746
|
+
<div class="border-r border-border"></div>
|
|
747
|
+
@for (day of days(); track day.getTime()) {
|
|
748
|
+
<button
|
|
749
|
+
type="button"
|
|
750
|
+
class="flex flex-col items-center gap-0.5 px-1 py-1.5 transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500"
|
|
751
|
+
(click)="dayClick.emit(day)"
|
|
752
|
+
>
|
|
753
|
+
<span class="text-[11px] uppercase text-ink-faint">{{ shortWeekday(day) }}</span>
|
|
754
|
+
<span
|
|
755
|
+
class="inline-flex size-6 items-center justify-center rounded-full text-sm font-semibold"
|
|
756
|
+
[class]="isToday(day) ? 'bg-brand-500 text-white' : 'text-ink'"
|
|
757
|
+
>
|
|
758
|
+
{{ day.getDate() }}
|
|
759
|
+
</span>
|
|
760
|
+
</button>
|
|
761
|
+
}
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
@if (hasAllDay()) {
|
|
765
|
+
<div class="grid border-b border-border" [style.grid-template-columns]="columns()">
|
|
766
|
+
<div
|
|
767
|
+
class="border-r border-border px-1 py-1 text-right text-[10px] leading-tight text-ink-faint"
|
|
768
|
+
>
|
|
769
|
+
{{ allDayLabel() }}
|
|
770
|
+
</div>
|
|
771
|
+
@for (day of days(); track day.getTime()) {
|
|
772
|
+
<div class="flex min-w-0 flex-col gap-0.5 p-1">
|
|
773
|
+
@for (e of allDayEvents(day); track e.id) {
|
|
774
|
+
<button
|
|
775
|
+
type="button"
|
|
776
|
+
[class]="
|
|
777
|
+
chipClass(e) + ' truncate rounded border px-1 py-px text-left text-[11px]'
|
|
778
|
+
"
|
|
779
|
+
[class.line-through]="e.cancelled"
|
|
780
|
+
(click)="eventClick.emit(e)"
|
|
781
|
+
>
|
|
782
|
+
{{ e.title }}
|
|
783
|
+
</button>
|
|
784
|
+
}
|
|
785
|
+
</div>
|
|
786
|
+
}
|
|
787
|
+
</div>
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
<div class="overflow-y-auto" [style.height]="height()">
|
|
791
|
+
<div class="grid" [style.grid-template-columns]="columns()">
|
|
792
|
+
<div class="relative border-r border-border" [style.height.px]="bodyHeight()">
|
|
793
|
+
@for (h of hours(); track h) {
|
|
794
|
+
<span
|
|
795
|
+
class="absolute right-1.5 -translate-y-1/2 text-[10px] tabular-nums text-ink-faint"
|
|
796
|
+
[style.top.px]="hourTop(h)"
|
|
797
|
+
>
|
|
798
|
+
{{ hourLabel(h) }}
|
|
799
|
+
</span>
|
|
800
|
+
}
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
@for (day of days(); track day.getTime(); let dayIndex = $index) {
|
|
804
|
+
<div
|
|
805
|
+
class="relative touch-none border-r border-border last:border-r-0"
|
|
806
|
+
[attr.data-sds-day]="dayIndex"
|
|
807
|
+
[style.height.px]="bodyHeight()"
|
|
808
|
+
(click)="onGridClick($event, day)"
|
|
809
|
+
(pointerdown)="startSelect($event, dayIndex)"
|
|
810
|
+
>
|
|
811
|
+
@for (h of hours(); track h) {
|
|
812
|
+
<div
|
|
813
|
+
class="pointer-events-none absolute inset-x-0 border-t border-border/70"
|
|
814
|
+
[style.top.px]="hourTop(h)"
|
|
815
|
+
></div>
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@if (ghostFor(dayIndex); as ghost) {
|
|
819
|
+
<!--
|
|
820
|
+
What the drag will produce, drawn where it will land. A drag with no preview
|
|
821
|
+
is a guess: you find out what you did once the server has been told.
|
|
822
|
+
-->
|
|
823
|
+
<div
|
|
824
|
+
class="pointer-events-none absolute inset-x-0.5 z-30 rounded border-2 border-dashed border-brand-500 bg-brand-500/15"
|
|
825
|
+
[style.top.px]="ghost.top"
|
|
826
|
+
[style.height.px]="ghost.height"
|
|
827
|
+
aria-hidden="true"
|
|
828
|
+
>
|
|
829
|
+
<span
|
|
830
|
+
class="block px-1 pt-0.5 text-[10px] font-medium text-brand-700 dark:text-brand-200"
|
|
831
|
+
>
|
|
832
|
+
{{ ghost.label }}
|
|
833
|
+
</span>
|
|
834
|
+
</div>
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
@if (nowIndicator() && isToday(day) && nowTop() !== null) {
|
|
838
|
+
<div
|
|
839
|
+
class="pointer-events-none absolute inset-x-0 z-20 flex items-center"
|
|
840
|
+
[style.top.px]="nowTop()"
|
|
841
|
+
aria-hidden="true"
|
|
842
|
+
>
|
|
843
|
+
<span class="size-2 shrink-0 rounded-full bg-danger"></span>
|
|
844
|
+
<span class="h-px flex-1 bg-danger"></span>
|
|
845
|
+
</div>
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
@for (p of placed(day); track p.event.id) {
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
[class]="
|
|
852
|
+
chipClass(p.event) +
|
|
853
|
+
' absolute z-10 overflow-hidden rounded border px-1 py-0.5 text-left transition-shadow hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500'
|
|
854
|
+
"
|
|
855
|
+
[class.line-through]="p.event.cancelled"
|
|
856
|
+
[class.opacity-60]="p.event.cancelled"
|
|
857
|
+
[style.top.%]="p.top * 100"
|
|
858
|
+
[style.height.%]="p.height * 100"
|
|
859
|
+
[style.left.%]="p.left * 100"
|
|
860
|
+
[style.width.%]="p.width * 100"
|
|
861
|
+
[attr.aria-label]="
|
|
862
|
+
p.event.title + ', ' + clock(p.start) + ' – ' + clock(p.end)
|
|
863
|
+
"
|
|
864
|
+
[class.cursor-grab]="canDrag(p)"
|
|
865
|
+
[class.opacity-40]="isDragging(p)"
|
|
866
|
+
(click)="onEventClick($event, p)"
|
|
867
|
+
(pointerdown)="startMove($event, p, dayIndex)"
|
|
868
|
+
>
|
|
869
|
+
<span class="block truncate text-[11px] font-medium leading-tight">
|
|
870
|
+
{{ p.event.title }}
|
|
871
|
+
</span>
|
|
872
|
+
@if (p.height > 0.045) {
|
|
873
|
+
<span class="block truncate text-[10px] leading-tight opacity-80">
|
|
874
|
+
{{ clock(p.start) }}
|
|
875
|
+
@if (p.event.subtitle) {
|
|
876
|
+
· {{ p.event.subtitle }}
|
|
877
|
+
}
|
|
878
|
+
</span>
|
|
879
|
+
}
|
|
880
|
+
</button>
|
|
881
|
+
}
|
|
882
|
+
</div>
|
|
883
|
+
}
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
}
|
|
889
|
+
</div>
|
|
890
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "component", type: SdsSegmented, selector: "sds-segmented", inputs: ["options", "value", "size", "ariaLabel", "ariaLabelledby", "disabled", "fullWidth", "iconOnly"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
891
|
+
}
|
|
892
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsEventCalendar, decorators: [{
|
|
893
|
+
type: Component,
|
|
894
|
+
args: [{
|
|
895
|
+
selector: 'sds-event-calendar',
|
|
896
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
897
|
+
imports: [SdsIcon, SdsSegmented],
|
|
898
|
+
host: { class: 'block' },
|
|
899
|
+
template: `
|
|
900
|
+
<div class="flex flex-col overflow-hidden rounded-card border border-border bg-surface">
|
|
901
|
+
@if (showToolbar()) {
|
|
902
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-border px-3 py-2.5">
|
|
903
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
904
|
+
<button
|
|
905
|
+
type="button"
|
|
906
|
+
[attr.aria-label]="previousLabel()"
|
|
907
|
+
[title]="previousLabel()"
|
|
908
|
+
class="inline-flex size-8 items-center justify-center rounded-control text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
909
|
+
(click)="move(-1)"
|
|
910
|
+
>
|
|
911
|
+
<sds-icon name="chevron-left" [size]="18" />
|
|
912
|
+
</button>
|
|
913
|
+
<button
|
|
914
|
+
type="button"
|
|
915
|
+
class="inline-flex h-8 items-center rounded-control border border-border px-3 text-sm font-medium text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
916
|
+
(click)="goToday()"
|
|
917
|
+
>
|
|
918
|
+
{{ todayLabel() }}
|
|
919
|
+
</button>
|
|
920
|
+
<button
|
|
921
|
+
type="button"
|
|
922
|
+
[attr.aria-label]="nextLabel()"
|
|
923
|
+
[title]="nextLabel()"
|
|
924
|
+
class="inline-flex size-8 items-center justify-center rounded-control text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
925
|
+
(click)="move(1)"
|
|
926
|
+
>
|
|
927
|
+
<sds-icon name="chevron-right" [size]="18" />
|
|
928
|
+
</button>
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
<h2 class="min-w-0 flex-1 truncate text-sm font-semibold text-ink sm:text-base">
|
|
932
|
+
{{ title() }}
|
|
933
|
+
</h2>
|
|
934
|
+
|
|
935
|
+
@if (showViewSwitcher()) {
|
|
936
|
+
<sds-segmented
|
|
937
|
+
size="sm"
|
|
938
|
+
[ariaLabel]="viewSwitcherLabel()"
|
|
939
|
+
[options]="viewOptions()"
|
|
940
|
+
[value]="view()"
|
|
941
|
+
(valueChange)="setView($event)"
|
|
942
|
+
/>
|
|
943
|
+
}
|
|
944
|
+
</div>
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
@if (view() === 'month') {
|
|
948
|
+
<!-- ------------------------------------------------------------------ month -->
|
|
949
|
+
<div class="grid grid-cols-7 border-b border-border bg-muted/50">
|
|
950
|
+
@for (d of weekdayNames(); track $index) {
|
|
951
|
+
<div class="px-1 py-1.5 text-center text-[11px] font-medium text-ink-soft">
|
|
952
|
+
{{ d }}
|
|
953
|
+
</div>
|
|
954
|
+
}
|
|
955
|
+
</div>
|
|
956
|
+
<div class="grid grid-cols-7 grid-rows-6" [style.height]="height()">
|
|
957
|
+
@for (day of days(); track day.getTime()) {
|
|
958
|
+
<div
|
|
959
|
+
class="flex min-w-0 flex-col gap-0.5 border-t border-l border-border p-1 first:border-l-0 [&:nth-child(7n+1)]:border-l-0"
|
|
960
|
+
[class.bg-muted]="!inFocusedMonth(day)"
|
|
961
|
+
[class.opacity-60]="!inFocusedMonth(day)"
|
|
962
|
+
>
|
|
963
|
+
<button
|
|
964
|
+
type="button"
|
|
965
|
+
class="self-start rounded-full px-1.5 text-[11px] font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
966
|
+
[class]="
|
|
967
|
+
isToday(day) ? 'bg-brand-500 text-white hover:bg-brand-600' : 'text-ink-soft'
|
|
968
|
+
"
|
|
969
|
+
(click)="dayClick.emit(day)"
|
|
970
|
+
>
|
|
971
|
+
{{ day.getDate() }}
|
|
972
|
+
</button>
|
|
973
|
+
|
|
974
|
+
<!-- Phones get dots: four chips in a 48px-wide cell is four unreadable smears. -->
|
|
975
|
+
<div class="flex flex-wrap items-center gap-0.5 px-0.5 sm:hidden">
|
|
976
|
+
@for (e of cellEvents(day).slice(0, 4); track e.id) {
|
|
977
|
+
<span [class]="dotClass(e) + ' size-1.5 rounded-full'" aria-hidden="true"></span>
|
|
978
|
+
}
|
|
979
|
+
@if (cellEvents(day).length) {
|
|
980
|
+
<span class="sr-only">{{ cellEvents(day).length }}</span>
|
|
981
|
+
}
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
<div class="hidden min-h-0 flex-col gap-0.5 overflow-hidden sm:flex">
|
|
985
|
+
@for (e of cellEvents(day).slice(0, maxPerCell()); track e.id) {
|
|
986
|
+
<button
|
|
987
|
+
type="button"
|
|
988
|
+
[class]="
|
|
989
|
+
chipClass(e) + ' truncate rounded border px-1 py-px text-left text-[11px]'
|
|
990
|
+
"
|
|
991
|
+
[class.line-through]="e.cancelled"
|
|
992
|
+
[class.opacity-60]="e.cancelled"
|
|
993
|
+
(click)="eventClick.emit(e)"
|
|
994
|
+
>
|
|
995
|
+
@if (!e.allDay) {
|
|
996
|
+
<span class="tabular-nums opacity-80">{{ clock(e.start) }}</span>
|
|
997
|
+
}
|
|
998
|
+
{{ e.title }}
|
|
999
|
+
</button>
|
|
1000
|
+
}
|
|
1001
|
+
@if (cellEvents(day).length > maxPerCell()) {
|
|
1002
|
+
<button
|
|
1003
|
+
type="button"
|
|
1004
|
+
class="px-1 text-left text-[11px] font-medium text-ink-soft hover:text-ink"
|
|
1005
|
+
(click)="dayClick.emit(day)"
|
|
1006
|
+
>
|
|
1007
|
+
{{
|
|
1008
|
+
moreLabel().replace('{count}', '' + (cellEvents(day).length - maxPerCell()))
|
|
1009
|
+
}}
|
|
1010
|
+
</button>
|
|
1011
|
+
}
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
}
|
|
1015
|
+
</div>
|
|
1016
|
+
} @else {
|
|
1017
|
+
<!-- ------------------------------------------------- week / 3 days / one day -->
|
|
1018
|
+
<div class="overflow-x-auto">
|
|
1019
|
+
<div [style.min-width.px]="minWidth()">
|
|
1020
|
+
<div class="grid border-b border-border" [style.grid-template-columns]="columns()">
|
|
1021
|
+
<div class="border-r border-border"></div>
|
|
1022
|
+
@for (day of days(); track day.getTime()) {
|
|
1023
|
+
<button
|
|
1024
|
+
type="button"
|
|
1025
|
+
class="flex flex-col items-center gap-0.5 px-1 py-1.5 transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500"
|
|
1026
|
+
(click)="dayClick.emit(day)"
|
|
1027
|
+
>
|
|
1028
|
+
<span class="text-[11px] uppercase text-ink-faint">{{ shortWeekday(day) }}</span>
|
|
1029
|
+
<span
|
|
1030
|
+
class="inline-flex size-6 items-center justify-center rounded-full text-sm font-semibold"
|
|
1031
|
+
[class]="isToday(day) ? 'bg-brand-500 text-white' : 'text-ink'"
|
|
1032
|
+
>
|
|
1033
|
+
{{ day.getDate() }}
|
|
1034
|
+
</span>
|
|
1035
|
+
</button>
|
|
1036
|
+
}
|
|
1037
|
+
</div>
|
|
1038
|
+
|
|
1039
|
+
@if (hasAllDay()) {
|
|
1040
|
+
<div class="grid border-b border-border" [style.grid-template-columns]="columns()">
|
|
1041
|
+
<div
|
|
1042
|
+
class="border-r border-border px-1 py-1 text-right text-[10px] leading-tight text-ink-faint"
|
|
1043
|
+
>
|
|
1044
|
+
{{ allDayLabel() }}
|
|
1045
|
+
</div>
|
|
1046
|
+
@for (day of days(); track day.getTime()) {
|
|
1047
|
+
<div class="flex min-w-0 flex-col gap-0.5 p-1">
|
|
1048
|
+
@for (e of allDayEvents(day); track e.id) {
|
|
1049
|
+
<button
|
|
1050
|
+
type="button"
|
|
1051
|
+
[class]="
|
|
1052
|
+
chipClass(e) + ' truncate rounded border px-1 py-px text-left text-[11px]'
|
|
1053
|
+
"
|
|
1054
|
+
[class.line-through]="e.cancelled"
|
|
1055
|
+
(click)="eventClick.emit(e)"
|
|
1056
|
+
>
|
|
1057
|
+
{{ e.title }}
|
|
1058
|
+
</button>
|
|
1059
|
+
}
|
|
1060
|
+
</div>
|
|
1061
|
+
}
|
|
1062
|
+
</div>
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
<div class="overflow-y-auto" [style.height]="height()">
|
|
1066
|
+
<div class="grid" [style.grid-template-columns]="columns()">
|
|
1067
|
+
<div class="relative border-r border-border" [style.height.px]="bodyHeight()">
|
|
1068
|
+
@for (h of hours(); track h) {
|
|
1069
|
+
<span
|
|
1070
|
+
class="absolute right-1.5 -translate-y-1/2 text-[10px] tabular-nums text-ink-faint"
|
|
1071
|
+
[style.top.px]="hourTop(h)"
|
|
1072
|
+
>
|
|
1073
|
+
{{ hourLabel(h) }}
|
|
1074
|
+
</span>
|
|
1075
|
+
}
|
|
1076
|
+
</div>
|
|
1077
|
+
|
|
1078
|
+
@for (day of days(); track day.getTime(); let dayIndex = $index) {
|
|
1079
|
+
<div
|
|
1080
|
+
class="relative touch-none border-r border-border last:border-r-0"
|
|
1081
|
+
[attr.data-sds-day]="dayIndex"
|
|
1082
|
+
[style.height.px]="bodyHeight()"
|
|
1083
|
+
(click)="onGridClick($event, day)"
|
|
1084
|
+
(pointerdown)="startSelect($event, dayIndex)"
|
|
1085
|
+
>
|
|
1086
|
+
@for (h of hours(); track h) {
|
|
1087
|
+
<div
|
|
1088
|
+
class="pointer-events-none absolute inset-x-0 border-t border-border/70"
|
|
1089
|
+
[style.top.px]="hourTop(h)"
|
|
1090
|
+
></div>
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
@if (ghostFor(dayIndex); as ghost) {
|
|
1094
|
+
<!--
|
|
1095
|
+
What the drag will produce, drawn where it will land. A drag with no preview
|
|
1096
|
+
is a guess: you find out what you did once the server has been told.
|
|
1097
|
+
-->
|
|
1098
|
+
<div
|
|
1099
|
+
class="pointer-events-none absolute inset-x-0.5 z-30 rounded border-2 border-dashed border-brand-500 bg-brand-500/15"
|
|
1100
|
+
[style.top.px]="ghost.top"
|
|
1101
|
+
[style.height.px]="ghost.height"
|
|
1102
|
+
aria-hidden="true"
|
|
1103
|
+
>
|
|
1104
|
+
<span
|
|
1105
|
+
class="block px-1 pt-0.5 text-[10px] font-medium text-brand-700 dark:text-brand-200"
|
|
1106
|
+
>
|
|
1107
|
+
{{ ghost.label }}
|
|
1108
|
+
</span>
|
|
1109
|
+
</div>
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
@if (nowIndicator() && isToday(day) && nowTop() !== null) {
|
|
1113
|
+
<div
|
|
1114
|
+
class="pointer-events-none absolute inset-x-0 z-20 flex items-center"
|
|
1115
|
+
[style.top.px]="nowTop()"
|
|
1116
|
+
aria-hidden="true"
|
|
1117
|
+
>
|
|
1118
|
+
<span class="size-2 shrink-0 rounded-full bg-danger"></span>
|
|
1119
|
+
<span class="h-px flex-1 bg-danger"></span>
|
|
1120
|
+
</div>
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
@for (p of placed(day); track p.event.id) {
|
|
1124
|
+
<button
|
|
1125
|
+
type="button"
|
|
1126
|
+
[class]="
|
|
1127
|
+
chipClass(p.event) +
|
|
1128
|
+
' absolute z-10 overflow-hidden rounded border px-1 py-0.5 text-left transition-shadow hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500'
|
|
1129
|
+
"
|
|
1130
|
+
[class.line-through]="p.event.cancelled"
|
|
1131
|
+
[class.opacity-60]="p.event.cancelled"
|
|
1132
|
+
[style.top.%]="p.top * 100"
|
|
1133
|
+
[style.height.%]="p.height * 100"
|
|
1134
|
+
[style.left.%]="p.left * 100"
|
|
1135
|
+
[style.width.%]="p.width * 100"
|
|
1136
|
+
[attr.aria-label]="
|
|
1137
|
+
p.event.title + ', ' + clock(p.start) + ' – ' + clock(p.end)
|
|
1138
|
+
"
|
|
1139
|
+
[class.cursor-grab]="canDrag(p)"
|
|
1140
|
+
[class.opacity-40]="isDragging(p)"
|
|
1141
|
+
(click)="onEventClick($event, p)"
|
|
1142
|
+
(pointerdown)="startMove($event, p, dayIndex)"
|
|
1143
|
+
>
|
|
1144
|
+
<span class="block truncate text-[11px] font-medium leading-tight">
|
|
1145
|
+
{{ p.event.title }}
|
|
1146
|
+
</span>
|
|
1147
|
+
@if (p.height > 0.045) {
|
|
1148
|
+
<span class="block truncate text-[10px] leading-tight opacity-80">
|
|
1149
|
+
{{ clock(p.start) }}
|
|
1150
|
+
@if (p.event.subtitle) {
|
|
1151
|
+
· {{ p.event.subtitle }}
|
|
1152
|
+
}
|
|
1153
|
+
</span>
|
|
1154
|
+
}
|
|
1155
|
+
</button>
|
|
1156
|
+
}
|
|
1157
|
+
</div>
|
|
1158
|
+
}
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
}
|
|
1164
|
+
</div>
|
|
1165
|
+
`,
|
|
1166
|
+
}]
|
|
1167
|
+
}], ctorParameters: () => [], propDecorators: { events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: false }] }, { type: i0.Output, args: ["dateChange"] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], startHour: [{ type: i0.Input, args: [{ isSignal: true, alias: "startHour", required: false }] }], endHour: [{ type: i0.Input, args: [{ isSignal: true, alias: "endHour", required: false }] }], hourHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourHeight", required: false }] }], defaultMinutes: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultMinutes", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], nowIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "nowIndicator", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showViewSwitcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "showViewSwitcher", required: false }] }], maxPerCell: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPerCell", required: false }] }], todayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], allDayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "allDayLabel", required: false }] }], moreLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "moreLabel", required: false }] }], viewSwitcherLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewSwitcherLabel", required: false }] }], monthLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "monthLabel", required: false }] }], weekLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekLabel", required: false }] }], threeDayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "threeDayLabel", required: false }] }], dayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayLabel", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], snapMinutes: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapMinutes", required: false }] }], eventClick: [{ type: i0.Output, args: ["eventClick"] }], eventDrop: [{ type: i0.Output, args: ["eventDrop"] }], rangeSelect: [{ type: i0.Output, args: ["rangeSelect"] }], slotClick: [{ type: i0.Output, args: ["slotClick"] }], dayClick: [{ type: i0.Output, args: ["dayClick"] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }] } });
|
|
1168
|
+
|
|
1169
|
+
/*
|
|
1170
|
+
* @besaitech/ng-design-system/calendar — the event calendar, kept OUT of the main entry point.
|
|
1171
|
+
*
|
|
1172
|
+
* Same reason the chart family has its own: a consuming app that never shows a calendar should not
|
|
1173
|
+
* carry one. It is reachable from a lazy route, but the library is one ES module, so anything the
|
|
1174
|
+
* primary entry point exports is linked into whatever bundle already imports from it — measured at
|
|
1175
|
+
* roughly 28 kB landing in medicware-clinic's INITIAL bundle, past its budget, for a screen behind
|
|
1176
|
+
* a lazy route. A separate entry point is what actually makes it lazy.
|
|
1177
|
+
*/
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Generated bundle index. Do not edit.
|
|
1181
|
+
*/
|
|
1182
|
+
|
|
1183
|
+
export { SdsEventCalendar, allDayOn, eventsOn, minutesOfDay, occursOn, placeDay, snapMinute, startOfWeek, step, toDate, visibleDays, visibleRange };
|
|
1184
|
+
//# sourceMappingURL=besaitech-ng-design-system-calendar.mjs.map
|