@luckydye/calendar 1.3.2 → 1.4.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/dist/calendar.js +2344 -2061
- package/package.json +7 -1
- package/src/ActiveCalendarStore.ts +88 -88
- package/src/CalDAVConfig.ts +611 -514
- package/src/CalDAVSource.ts +561 -466
- package/src/CalendarIntegration.ts +64 -47
- package/src/CalendarInternal.ts +645 -614
- package/src/CalendarLayer.ts +1 -0
- package/src/CalendarStorage.ts +51 -48
- package/src/CalendarView.ts +883 -507
- package/src/Color.ts +48 -54
- package/src/GoogleCalendarSource.ts +758 -662
- package/src/ICal.ts +420 -348
- package/src/InMemorySource.ts +56 -48
- package/src/IndexedDBStorage.ts +444 -398
- package/src/InhouseBookingSource.ts +614 -523
- package/src/Keybinds.ts +6 -1
- package/src/NotificationScheduler.ts +11 -8
- package/src/StatusBar.ts +12 -8
- package/src/StatusMessage.ts +2 -2
- package/src/Theme.ts +21 -7
- package/src/TimeseriesJson.ts +98 -98
- package/src/app.ts +153 -78
- package/src/layers/EventsLayer.ts +530 -400
- package/src/layers/GridLayer.ts +45 -125
- package/src/layers/TimeseriesHeatmapLayer.ts +123 -120
- package/src/service-worker.js +3 -2
|
@@ -3,10 +3,16 @@ import type {
|
|
|
3
3
|
EventSegment,
|
|
4
4
|
WeekInfo,
|
|
5
5
|
} from "../CalendarInternal.js";
|
|
6
|
-
import {
|
|
6
|
+
import type { CalendarLayer, LayerContext } from "../CalendarLayer.js";
|
|
7
7
|
import { hexToRgb, rgbToHsl } from "../Color.js";
|
|
8
8
|
|
|
9
9
|
const MIN_EVENT_HEIGHT = 20;
|
|
10
|
+
const COMPACT_EVENT_HEIGHT = 6;
|
|
11
|
+
const COMPACT_EVENT_THRESHOLD = 110;
|
|
12
|
+
const TIME_SCALE_EVENT_THRESHOLD = 400;
|
|
13
|
+
const MONTH_LABEL_HEIGHT = 40;
|
|
14
|
+
const MONTH_LABEL_GAP = 4;
|
|
15
|
+
const DAY_CELL_EVENT_INSET = 6;
|
|
10
16
|
|
|
11
17
|
export interface EventRect {
|
|
12
18
|
event: CalendarEvent;
|
|
@@ -16,8 +22,22 @@ export interface EventRect {
|
|
|
16
22
|
height: number;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
export interface PreviewEventData {
|
|
26
|
+
id: string;
|
|
27
|
+
start: Date;
|
|
28
|
+
end: Date;
|
|
29
|
+
color: {
|
|
30
|
+
fill: string;
|
|
31
|
+
stroke: string;
|
|
32
|
+
text: string;
|
|
33
|
+
dashed?: boolean;
|
|
34
|
+
};
|
|
35
|
+
excludeEventId?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export interface EventsState {
|
|
20
39
|
events: CalendarEvent[];
|
|
40
|
+
previewEvents: PreviewEventData[];
|
|
21
41
|
hoveredEventId: string | null;
|
|
22
42
|
isEventSelected: (event: CalendarEvent) => boolean;
|
|
23
43
|
shouldRenderEventWithStripes: (event: CalendarEvent) => boolean;
|
|
@@ -37,6 +57,7 @@ export function createEventsLayer(
|
|
|
37
57
|
width,
|
|
38
58
|
height,
|
|
39
59
|
scrollTop,
|
|
60
|
+
scrollLeft,
|
|
40
61
|
dayWidth,
|
|
41
62
|
dayHeight,
|
|
42
63
|
leftGutterWidth,
|
|
@@ -47,25 +68,42 @@ export function createEventsLayer(
|
|
|
47
68
|
getDayVisualPosition,
|
|
48
69
|
} = lc;
|
|
49
70
|
|
|
50
|
-
const
|
|
71
|
+
const previewEvents = state.previewEvents;
|
|
72
|
+
const excludedEventIds = new Set(
|
|
73
|
+
previewEvents
|
|
74
|
+
.map((preview) => preview.excludeEventId)
|
|
75
|
+
.filter((id): id is string => Boolean(id)),
|
|
76
|
+
);
|
|
77
|
+
const events = state.events.filter(
|
|
78
|
+
(event) => !excludedEventIds.has(event.id),
|
|
79
|
+
);
|
|
51
80
|
const viewportBottom = scrollTop + height;
|
|
52
|
-
const showTimeScale = dayHeight >=
|
|
81
|
+
const showTimeScale = dayHeight >= TIME_SCALE_EVENT_THRESHOLD;
|
|
82
|
+
const showCompactEvents =
|
|
83
|
+
!showTimeScale && dayHeight < COMPACT_EVENT_THRESHOLD;
|
|
53
84
|
|
|
54
85
|
layer.eventRects = [];
|
|
55
86
|
|
|
56
87
|
if (visibleWeeks.length === 0) return;
|
|
57
88
|
|
|
58
89
|
const firstVisibleWeek = visibleWeeks[0]!;
|
|
59
|
-
const lastVisibleWeek =
|
|
60
|
-
visibleWeeks[visibleWeeks.length - 1]!;
|
|
90
|
+
const lastVisibleWeek = visibleWeeks[visibleWeeks.length - 1]!;
|
|
61
91
|
const firstVisibleDay = firstVisibleWeek.days[0]!;
|
|
62
92
|
const lastVisibleDay = lastVisibleWeek.days[6]!;
|
|
63
93
|
const visibleStartTime = firstVisibleDay.getTime();
|
|
64
|
-
const visibleEndTime =
|
|
65
|
-
lastVisibleDay.getTime() + 86400000 - 1;
|
|
94
|
+
const visibleEndTime = lastVisibleDay.getTime() + 86400000 - 1;
|
|
66
95
|
|
|
67
96
|
const dayOccupiedRows = new Map<string, Set<number>>();
|
|
68
97
|
const eventRowIndex = new Map<string, number>();
|
|
98
|
+
const overflowedDays = new Map<
|
|
99
|
+
string,
|
|
100
|
+
{
|
|
101
|
+
weekIndex: number;
|
|
102
|
+
dayIndex: number;
|
|
103
|
+
topInset: number;
|
|
104
|
+
maxEventsInRow: number;
|
|
105
|
+
}
|
|
106
|
+
>();
|
|
69
107
|
|
|
70
108
|
const segments: EventSegment[] = [];
|
|
71
109
|
const allDaySegments: EventSegment[] = [];
|
|
@@ -76,10 +114,7 @@ export function createEventsLayer(
|
|
|
76
114
|
const eventStartTime = event.start.getTime();
|
|
77
115
|
const eventEndTime = event.end.getTime();
|
|
78
116
|
|
|
79
|
-
if (
|
|
80
|
-
eventEndTime < visibleStartTime ||
|
|
81
|
-
eventStartTime > visibleEndTime
|
|
82
|
-
)
|
|
117
|
+
if (eventEndTime < visibleStartTime || eventStartTime > visibleEndTime)
|
|
83
118
|
continue;
|
|
84
119
|
|
|
85
120
|
const eventWeeks: {
|
|
@@ -95,10 +130,7 @@ export function createEventsLayer(
|
|
|
95
130
|
const weekStart = weekStartDay.getTime();
|
|
96
131
|
const weekEnd = weekEndDay.getTime() + 86399999;
|
|
97
132
|
|
|
98
|
-
if (
|
|
99
|
-
eventEndTime >= weekStart &&
|
|
100
|
-
eventStartTime <= weekEnd
|
|
101
|
-
) {
|
|
133
|
+
if (eventEndTime >= weekStart && eventStartTime <= weekEnd) {
|
|
102
134
|
const weekIndex = allWeeks.indexOf(week);
|
|
103
135
|
eventWeeks.push({ weekIndex, week });
|
|
104
136
|
}
|
|
@@ -117,19 +149,13 @@ export function createEventsLayer(
|
|
|
117
149
|
let endDayIndex = 6;
|
|
118
150
|
|
|
119
151
|
if (isStart) {
|
|
120
|
-
startDayIndex = getDayIndexInWeek(
|
|
121
|
-
week,
|
|
122
|
-
event.start,
|
|
123
|
-
);
|
|
152
|
+
startDayIndex = getDayIndexInWeek(week, event.start);
|
|
124
153
|
}
|
|
125
154
|
if (isEnd) {
|
|
126
155
|
const effectiveEnd = isAllDay
|
|
127
156
|
? new Date(event.end.getTime() - 1)
|
|
128
157
|
: event.end;
|
|
129
|
-
endDayIndex = getDayIndexInWeek(
|
|
130
|
-
week,
|
|
131
|
-
effectiveEnd,
|
|
132
|
-
);
|
|
158
|
+
endDayIndex = getDayIndexInWeek(week, effectiveEnd);
|
|
133
159
|
}
|
|
134
160
|
|
|
135
161
|
const segment = {
|
|
@@ -153,30 +179,47 @@ export function createEventsLayer(
|
|
|
153
179
|
|
|
154
180
|
const renderedTimedSegments = showTimeScale
|
|
155
181
|
? timedSegments.flatMap((seg) => {
|
|
156
|
-
if (seg.startDayIndex === seg.endDayIndex)
|
|
157
|
-
return [seg];
|
|
182
|
+
if (seg.startDayIndex === seg.endDayIndex) return [seg];
|
|
158
183
|
const result = [];
|
|
159
|
-
for (
|
|
160
|
-
let d = seg.startDayIndex;
|
|
161
|
-
d <= seg.endDayIndex;
|
|
162
|
-
d++
|
|
163
|
-
) {
|
|
184
|
+
for (let d = seg.startDayIndex; d <= seg.endDayIndex; d++) {
|
|
164
185
|
result.push({
|
|
165
186
|
...seg,
|
|
166
187
|
startDayIndex: d,
|
|
167
188
|
endDayIndex: d,
|
|
168
|
-
isStart:
|
|
169
|
-
d === seg.startDayIndex && seg.isStart,
|
|
189
|
+
isStart: d === seg.startDayIndex && seg.isStart,
|
|
170
190
|
isEnd: d === seg.endDayIndex && seg.isEnd,
|
|
171
191
|
totalWeeks: 1,
|
|
172
192
|
});
|
|
173
193
|
}
|
|
174
194
|
return result;
|
|
175
|
-
|
|
195
|
+
})
|
|
176
196
|
: timedSegments;
|
|
177
197
|
|
|
178
198
|
segments.push(...allDaySegments, ...renderedTimedSegments);
|
|
179
199
|
|
|
200
|
+
const monthHeaderWeeks = new Set<number>();
|
|
201
|
+
const firstVisibleWeekIndex = allWeeks.indexOf(visibleWeeks[0]!);
|
|
202
|
+
let previousMonthKey: string | null = null;
|
|
203
|
+
if (firstVisibleWeekIndex > 0) {
|
|
204
|
+
const previousDay = allWeeks[firstVisibleWeekIndex - 1]?.days[6];
|
|
205
|
+
if (previousDay) {
|
|
206
|
+
previousMonthKey = `${previousDay.getMonth()}-${previousDay.getFullYear()}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const week of visibleWeeks) {
|
|
210
|
+
const weekIndex = allWeeks.indexOf(week);
|
|
211
|
+
if (weekIndex < 0) continue;
|
|
212
|
+
for (const day of week.days) {
|
|
213
|
+
if (!day) continue;
|
|
214
|
+
const monthKey = `${day.getMonth()}-${day.getFullYear()}`;
|
|
215
|
+
if (monthKey !== previousMonthKey) {
|
|
216
|
+
monthHeaderWeeks.add(weekIndex);
|
|
217
|
+
previousMonthKey = monthKey;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
180
223
|
// Calculate columns for overlapping events in time scale mode
|
|
181
224
|
interface EventLayout {
|
|
182
225
|
segment: EventSegment;
|
|
@@ -186,10 +229,7 @@ export function createEventsLayer(
|
|
|
186
229
|
totalColumns: number;
|
|
187
230
|
}
|
|
188
231
|
|
|
189
|
-
const segmentsByWeekDay = new Map<
|
|
190
|
-
string,
|
|
191
|
-
EventSegment[]
|
|
192
|
-
>();
|
|
232
|
+
const segmentsByWeekDay = new Map<string, EventSegment[]>();
|
|
193
233
|
for (const segment of segments) {
|
|
194
234
|
const allDay = segment.event.isAllDay === true;
|
|
195
235
|
if (showTimeScale && !allDay) {
|
|
@@ -202,7 +242,7 @@ export function createEventsLayer(
|
|
|
202
242
|
if (!segmentsByWeekDay.has(key)) {
|
|
203
243
|
segmentsByWeekDay.set(key, []);
|
|
204
244
|
}
|
|
205
|
-
segmentsByWeekDay.get(key)
|
|
245
|
+
segmentsByWeekDay.get(key)?.push(segment);
|
|
206
246
|
}
|
|
207
247
|
}
|
|
208
248
|
}
|
|
@@ -214,8 +254,8 @@ export function createEventsLayer(
|
|
|
214
254
|
|
|
215
255
|
for (const [dayKey, daySegments] of segmentsByWeekDay) {
|
|
216
256
|
const [weekIndexStr, dayIndexStr] = dayKey.split("-");
|
|
217
|
-
const dayIndex = parseInt(dayIndexStr!);
|
|
218
|
-
const weekIndex = parseInt(weekIndexStr!);
|
|
257
|
+
const dayIndex = Number.parseInt(dayIndexStr!);
|
|
258
|
+
const weekIndex = Number.parseInt(weekIndexStr!);
|
|
219
259
|
|
|
220
260
|
interface SegmentTime {
|
|
221
261
|
segment: EventSegment;
|
|
@@ -228,34 +268,31 @@ export function createEventsLayer(
|
|
|
228
268
|
const week = allWeeks[weekIndex];
|
|
229
269
|
if (!week) continue;
|
|
230
270
|
|
|
231
|
-
const dayStartTime = new Date(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
).setHours(23, 59, 59, 999);
|
|
237
|
-
const eventStartTime = seg.event.start.getTime();
|
|
238
|
-
const eventEndTime =
|
|
239
|
-
seg.event.end.getTime() - 60000;
|
|
240
|
-
|
|
241
|
-
const effectiveStartTime = Math.max(
|
|
242
|
-
eventStartTime,
|
|
243
|
-
dayStartTime,
|
|
271
|
+
const dayStartTime = new Date(week.days[dayIndex]!).setHours(
|
|
272
|
+
0,
|
|
273
|
+
0,
|
|
274
|
+
0,
|
|
275
|
+
0,
|
|
244
276
|
);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
const dayEndTime = new Date(week.days[dayIndex]!).setHours(
|
|
278
|
+
23,
|
|
279
|
+
59,
|
|
280
|
+
59,
|
|
281
|
+
999,
|
|
248
282
|
);
|
|
283
|
+
const eventStartTime = seg.event.start.getTime();
|
|
284
|
+
const eventEndTime = seg.event.end.getTime() - 60000;
|
|
285
|
+
|
|
286
|
+
const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
|
|
287
|
+
const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
|
|
249
288
|
|
|
250
289
|
const effectiveStart = new Date(effectiveStartTime);
|
|
251
290
|
const effectiveEnd = new Date(effectiveEndTime);
|
|
252
291
|
|
|
253
292
|
const startMinutes =
|
|
254
|
-
effectiveStart.getHours() * 60 +
|
|
255
|
-
effectiveStart.getMinutes();
|
|
293
|
+
effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
|
|
256
294
|
const endMinutes =
|
|
257
|
-
effectiveEnd.getHours() * 60 +
|
|
258
|
-
effectiveEnd.getMinutes();
|
|
295
|
+
effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
|
|
259
296
|
|
|
260
297
|
segmentTimes.push({
|
|
261
298
|
segment: seg,
|
|
@@ -268,9 +305,7 @@ export function createEventsLayer(
|
|
|
268
305
|
if (a.startMinutes !== b.startMinutes)
|
|
269
306
|
return a.startMinutes - b.startMinutes;
|
|
270
307
|
return (
|
|
271
|
-
b.endMinutes -
|
|
272
|
-
b.startMinutes -
|
|
273
|
-
(a.endMinutes - a.startMinutes)
|
|
308
|
+
b.endMinutes - b.startMinutes - (a.endMinutes - a.startMinutes)
|
|
274
309
|
);
|
|
275
310
|
});
|
|
276
311
|
|
|
@@ -278,15 +313,8 @@ export function createEventsLayer(
|
|
|
278
313
|
|
|
279
314
|
for (const st of segmentTimes) {
|
|
280
315
|
let columnIndex = 0;
|
|
281
|
-
for (
|
|
282
|
-
|
|
283
|
-
columnIndex < columns.length;
|
|
284
|
-
columnIndex++
|
|
285
|
-
) {
|
|
286
|
-
if (
|
|
287
|
-
columns[columnIndex]!.endMinutes <=
|
|
288
|
-
st.startMinutes
|
|
289
|
-
) {
|
|
316
|
+
for (; columnIndex < columns.length; columnIndex++) {
|
|
317
|
+
if (columns[columnIndex]?.endMinutes <= st.startMinutes) {
|
|
290
318
|
break;
|
|
291
319
|
}
|
|
292
320
|
}
|
|
@@ -294,8 +322,7 @@ export function createEventsLayer(
|
|
|
294
322
|
if (columnIndex === columns.length) {
|
|
295
323
|
columns.push({ endMinutes: st.endMinutes });
|
|
296
324
|
} else {
|
|
297
|
-
columns[columnIndex]!.endMinutes =
|
|
298
|
-
st.endMinutes;
|
|
325
|
+
columns[columnIndex]!.endMinutes = st.endMinutes;
|
|
299
326
|
}
|
|
300
327
|
|
|
301
328
|
const segmentKey = `${st.segment.weekIndex}-${st.segment.event.id}-${dayIndex}`;
|
|
@@ -329,55 +356,52 @@ export function createEventsLayer(
|
|
|
329
356
|
const weekYOffset = week.yOffset;
|
|
330
357
|
const allDay = event.isAllDay === true;
|
|
331
358
|
|
|
332
|
-
const startVisualPos =
|
|
333
|
-
getDayVisualPosition(startDayIndex);
|
|
359
|
+
const startVisualPos = getDayVisualPosition(startDayIndex);
|
|
334
360
|
const endVisualPos = getDayVisualPosition(endDayIndex);
|
|
335
361
|
|
|
336
362
|
let yStart: number;
|
|
337
363
|
let yEnd: number;
|
|
338
364
|
|
|
339
365
|
if (showTimeScale && !allDay) {
|
|
340
|
-
const dayStartTime = new Date(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
).setHours(23, 59, 59, 999);
|
|
346
|
-
const eventStartTime = event.start.getTime();
|
|
347
|
-
const eventEndTime =
|
|
348
|
-
event.end.getTime() - 60000;
|
|
349
|
-
|
|
350
|
-
const effectiveStartTime = Math.max(
|
|
351
|
-
eventStartTime,
|
|
352
|
-
dayStartTime,
|
|
366
|
+
const dayStartTime = new Date(week.days[startDayIndex]!).setHours(
|
|
367
|
+
0,
|
|
368
|
+
0,
|
|
369
|
+
0,
|
|
370
|
+
0,
|
|
353
371
|
);
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
372
|
+
const dayEndTime = new Date(week.days[endDayIndex]!).setHours(
|
|
373
|
+
23,
|
|
374
|
+
59,
|
|
375
|
+
59,
|
|
376
|
+
999,
|
|
357
377
|
);
|
|
378
|
+
const eventStartTime = event.start.getTime();
|
|
379
|
+
const eventEndTime = event.end.getTime() - 60000;
|
|
358
380
|
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
381
|
+
const effectiveStartTime = Math.max(eventStartTime, dayStartTime);
|
|
382
|
+
const effectiveEndTime = Math.min(eventEndTime, dayEndTime);
|
|
383
|
+
|
|
384
|
+
const effectiveStart = new Date(effectiveStartTime);
|
|
362
385
|
const effectiveEnd = new Date(effectiveEndTime);
|
|
363
386
|
|
|
364
387
|
const startMinutes =
|
|
365
|
-
effectiveStart.getHours() * 60 +
|
|
366
|
-
effectiveStart.getMinutes();
|
|
388
|
+
effectiveStart.getHours() * 60 + effectiveStart.getMinutes();
|
|
367
389
|
const endMinutes =
|
|
368
|
-
effectiveEnd.getHours() * 60 +
|
|
369
|
-
effectiveEnd.getMinutes();
|
|
390
|
+
effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
|
|
370
391
|
|
|
371
|
-
const visualRowY =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
yStart =
|
|
375
|
-
visualRowY +
|
|
376
|
-
(startMinutes / 1440) * dayHeight;
|
|
377
|
-
yEnd =
|
|
378
|
-
visualRowY +
|
|
379
|
-
(endMinutes / 1440) * dayHeight;
|
|
392
|
+
const visualRowY = weekYOffset + startVisualPos.row * dayHeight;
|
|
393
|
+
yStart = visualRowY + (startMinutes / 1440) * dayHeight;
|
|
394
|
+
yEnd = visualRowY + (endMinutes / 1440) * dayHeight;
|
|
380
395
|
} else {
|
|
396
|
+
const topInset = monthHeaderWeeks.has(weekIndex)
|
|
397
|
+
? MONTH_LABEL_HEIGHT + MONTH_LABEL_GAP + DAY_CELL_EVENT_INSET
|
|
398
|
+
: DAY_CELL_EVENT_INSET;
|
|
399
|
+
const rowHeight = showCompactEvents
|
|
400
|
+
? COMPACT_EVENT_HEIGHT + 2
|
|
401
|
+
: MIN_EVENT_HEIGHT + 2;
|
|
402
|
+
const eventBlockHeight = showCompactEvents
|
|
403
|
+
? COMPACT_EVENT_HEIGHT
|
|
404
|
+
: MIN_EVENT_HEIGHT;
|
|
381
405
|
const eventKey = `${weekIndex}-${event.id}`;
|
|
382
406
|
let rowIndex = eventRowIndex.get(eventKey);
|
|
383
407
|
|
|
@@ -385,14 +409,9 @@ export function createEventsLayer(
|
|
|
385
409
|
rowIndex = 0;
|
|
386
410
|
while (true) {
|
|
387
411
|
let rowFree = true;
|
|
388
|
-
for (
|
|
389
|
-
let d = startDayIndex;
|
|
390
|
-
d <= endDayIndex;
|
|
391
|
-
d++
|
|
392
|
-
) {
|
|
412
|
+
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
393
413
|
const dayKey = `${weekIndex}-${d}`;
|
|
394
|
-
const occupied =
|
|
395
|
-
dayOccupiedRows.get(dayKey);
|
|
414
|
+
const occupied = dayOccupiedRows.get(dayKey);
|
|
396
415
|
if (occupied?.has(rowIndex)) {
|
|
397
416
|
rowFree = false;
|
|
398
417
|
break;
|
|
@@ -404,11 +423,7 @@ export function createEventsLayer(
|
|
|
404
423
|
eventRowIndex.set(eventKey, rowIndex);
|
|
405
424
|
}
|
|
406
425
|
|
|
407
|
-
for (
|
|
408
|
-
let d = startDayIndex;
|
|
409
|
-
d <= endDayIndex;
|
|
410
|
-
d++
|
|
411
|
-
) {
|
|
426
|
+
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
412
427
|
const dayKey = `${weekIndex}-${d}`;
|
|
413
428
|
let occupied = dayOccupiedRows.get(dayKey);
|
|
414
429
|
if (!occupied) {
|
|
@@ -418,23 +433,31 @@ export function createEventsLayer(
|
|
|
418
433
|
occupied.add(rowIndex);
|
|
419
434
|
}
|
|
420
435
|
|
|
421
|
-
const maxEventsInRow = Math.floor(
|
|
422
|
-
|
|
423
|
-
)
|
|
424
|
-
|
|
436
|
+
const maxEventsInRow = Math.floor((dayHeight - topInset) / rowHeight);
|
|
437
|
+
const visibleEventsInRow = Math.max(0, maxEventsInRow - 1);
|
|
438
|
+
if (rowIndex >= visibleEventsInRow) {
|
|
439
|
+
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
440
|
+
overflowedDays.set(`${weekIndex}-${d}`, {
|
|
441
|
+
weekIndex,
|
|
442
|
+
dayIndex: d,
|
|
443
|
+
topInset,
|
|
444
|
+
maxEventsInRow: visibleEventsInRow,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
425
449
|
|
|
426
|
-
const visualRowY =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
yStart =
|
|
430
|
-
visualRowY +
|
|
431
|
-
4 +
|
|
432
|
-
rowIndex * (MIN_EVENT_HEIGHT + 2);
|
|
433
|
-
yEnd = yStart + MIN_EVENT_HEIGHT;
|
|
450
|
+
const visualRowY = weekYOffset + startVisualPos.row * dayHeight;
|
|
451
|
+
yStart = visualRowY + topInset + rowIndex * rowHeight;
|
|
452
|
+
yEnd = yStart + eventBlockHeight;
|
|
434
453
|
}
|
|
435
454
|
|
|
436
455
|
const eventHeight = Math.max(
|
|
437
|
-
showTimeScale
|
|
456
|
+
showTimeScale
|
|
457
|
+
? 4
|
|
458
|
+
: showCompactEvents
|
|
459
|
+
? COMPACT_EVENT_HEIGHT
|
|
460
|
+
: MIN_EVENT_HEIGHT,
|
|
438
461
|
yEnd - yStart,
|
|
439
462
|
);
|
|
440
463
|
|
|
@@ -450,36 +473,28 @@ export function createEventsLayer(
|
|
|
450
473
|
columnLayout &&
|
|
451
474
|
columnLayout.totalColumns > 1
|
|
452
475
|
) {
|
|
453
|
-
const columnWidth =
|
|
454
|
-
dayWidth / columnLayout.totalColumns;
|
|
476
|
+
const columnWidth = dayWidth / columnLayout.totalColumns;
|
|
455
477
|
x =
|
|
456
478
|
leftGutterWidth +
|
|
457
|
-
startVisualPos.col * dayWidth
|
|
479
|
+
startVisualPos.col * dayWidth -
|
|
480
|
+
scrollLeft +
|
|
458
481
|
columnLayout.column * columnWidth;
|
|
459
482
|
spanWidth = columnWidth;
|
|
460
483
|
} else {
|
|
461
|
-
const colSpan =
|
|
462
|
-
|
|
463
|
-
x =
|
|
464
|
-
leftGutterWidth +
|
|
465
|
-
startVisualPos.col * dayWidth;
|
|
484
|
+
const colSpan = endVisualPos.col - startVisualPos.col + 1;
|
|
485
|
+
x = leftGutterWidth + startVisualPos.col * dayWidth - scrollLeft;
|
|
466
486
|
spanWidth = colSpan * dayWidth;
|
|
467
487
|
}
|
|
468
488
|
|
|
469
489
|
const viewportY = yStart - scrollTop;
|
|
470
490
|
|
|
471
|
-
if (
|
|
472
|
-
viewportY + eventHeight < 0 ||
|
|
473
|
-
viewportY > height
|
|
474
|
-
)
|
|
475
|
-
continue;
|
|
491
|
+
if (viewportY + eventHeight < 0 || viewportY > height) continue;
|
|
476
492
|
|
|
477
493
|
const eventColor = event.color || "#888888";
|
|
478
494
|
const isSelected = state.isEventSelected(event);
|
|
479
|
-
const isHovered =
|
|
480
|
-
state.hoveredEventId === event.id;
|
|
495
|
+
const isHovered = state.hoveredEventId === event.id;
|
|
481
496
|
|
|
482
|
-
const padding =
|
|
497
|
+
const padding = DAY_CELL_EVENT_INSET;
|
|
483
498
|
const eventWidth = spanWidth - padding * 2;
|
|
484
499
|
const leftBorderWidth = 3;
|
|
485
500
|
const contentPadding = 6;
|
|
@@ -512,65 +527,52 @@ export function createEventsLayer(
|
|
|
512
527
|
|
|
513
528
|
const hsl = rgbToHsl(hexToRgb(eventColor));
|
|
514
529
|
const textPrimary =
|
|
515
|
-
styles["--text-primary"] ||
|
|
516
|
-
|
|
517
|
-
const textInverse =
|
|
518
|
-
styles["--text-inverse"] || "rgb(0, 0, 0)";
|
|
530
|
+
styles["--text-primary"] || "rgba(255, 255, 255, 0.9)";
|
|
531
|
+
const textInverse = styles["--text-inverse"] || "rgb(0, 0, 0)";
|
|
519
532
|
const backgroundColor = isSelected
|
|
520
533
|
? eventColor
|
|
521
|
-
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
534
|
+
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
535
|
+
hsl[2] + 15,
|
|
536
|
+
40,
|
|
537
|
+
)}%, 0.45)`;
|
|
522
538
|
const borderColor = isSelected
|
|
523
539
|
? eventColor
|
|
524
|
-
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
540
|
+
: `hsla(${hsl[0]}, ${Math.min(hsl[1] * 0.9, 90)}%, ${Math.min(
|
|
541
|
+
hsl[2] + 10,
|
|
542
|
+
70,
|
|
543
|
+
)}%, 1)`;
|
|
525
544
|
|
|
526
|
-
const useStripes =
|
|
527
|
-
state.shouldRenderEventWithStripes(event);
|
|
545
|
+
const useStripes = state.shouldRenderEventWithStripes(event);
|
|
528
546
|
|
|
529
547
|
ctx.beginPath();
|
|
530
|
-
ctx.roundRect(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
radiusTopLeft,
|
|
537
|
-
radiusTopRight,
|
|
538
|
-
radiusBottomRight,
|
|
539
|
-
radiusBottomLeft,
|
|
540
|
-
],
|
|
541
|
-
);
|
|
548
|
+
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
549
|
+
radiusTopLeft,
|
|
550
|
+
radiusTopRight,
|
|
551
|
+
radiusBottomRight,
|
|
552
|
+
radiusBottomLeft,
|
|
553
|
+
]);
|
|
542
554
|
|
|
543
555
|
ctx.fillStyle = backgroundColor;
|
|
544
556
|
ctx.fill();
|
|
545
557
|
|
|
546
558
|
if (useStripes) {
|
|
547
|
-
const patternCanvas =
|
|
548
|
-
state.getStripePatternCanvas();
|
|
559
|
+
const patternCanvas = state.getStripePatternCanvas();
|
|
549
560
|
if (patternCanvas) {
|
|
550
561
|
ctx.save();
|
|
551
562
|
ctx.beginPath();
|
|
552
|
-
ctx.roundRect(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
radiusTopLeft,
|
|
559
|
-
radiusTopRight,
|
|
560
|
-
radiusBottomRight,
|
|
561
|
-
radiusBottomLeft,
|
|
562
|
-
],
|
|
563
|
-
);
|
|
563
|
+
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
564
|
+
radiusTopLeft,
|
|
565
|
+
radiusTopRight,
|
|
566
|
+
radiusBottomRight,
|
|
567
|
+
radiusBottomLeft,
|
|
568
|
+
]);
|
|
564
569
|
ctx.clip();
|
|
565
570
|
|
|
566
571
|
const patternSize = 12;
|
|
567
572
|
const startX =
|
|
568
|
-
Math.floor((x + padding) / patternSize) *
|
|
569
|
-
patternSize;
|
|
573
|
+
Math.floor((x + padding) / patternSize) * patternSize;
|
|
570
574
|
const startY =
|
|
571
|
-
Math.floor(yStart / patternSize) *
|
|
572
|
-
patternSize -
|
|
573
|
-
scrollTop;
|
|
575
|
+
Math.floor(yStart / patternSize) * patternSize - scrollTop;
|
|
574
576
|
|
|
575
577
|
for (
|
|
576
578
|
let py = startY;
|
|
@@ -582,13 +584,7 @@ export function createEventsLayer(
|
|
|
582
584
|
px < x + padding + eventWidth;
|
|
583
585
|
px += patternSize
|
|
584
586
|
) {
|
|
585
|
-
ctx.drawImage(
|
|
586
|
-
patternCanvas,
|
|
587
|
-
px,
|
|
588
|
-
py,
|
|
589
|
-
patternSize,
|
|
590
|
-
patternSize,
|
|
591
|
-
);
|
|
587
|
+
ctx.drawImage(patternCanvas, px, py, patternSize, patternSize);
|
|
592
588
|
}
|
|
593
589
|
}
|
|
594
590
|
|
|
@@ -604,12 +600,7 @@ export function createEventsLayer(
|
|
|
604
600
|
viewportY + 2,
|
|
605
601
|
leftBorderWidth,
|
|
606
602
|
eventHeight - 4,
|
|
607
|
-
[
|
|
608
|
-
radiusTopLeft,
|
|
609
|
-
radiusTopLeft,
|
|
610
|
-
radiusBottomLeft,
|
|
611
|
-
radiusBottomLeft,
|
|
612
|
-
],
|
|
603
|
+
[radiusTopLeft, radiusTopLeft, radiusBottomLeft, radiusBottomLeft],
|
|
613
604
|
);
|
|
614
605
|
ctx.fill();
|
|
615
606
|
}
|
|
@@ -618,43 +609,30 @@ export function createEventsLayer(
|
|
|
618
609
|
ctx.strokeStyle = textInverse;
|
|
619
610
|
ctx.lineWidth = 1;
|
|
620
611
|
ctx.beginPath();
|
|
621
|
-
ctx.roundRect(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
radiusTopLeft,
|
|
628
|
-
radiusTopRight,
|
|
629
|
-
radiusBottomRight,
|
|
630
|
-
radiusBottomLeft,
|
|
631
|
-
],
|
|
632
|
-
);
|
|
612
|
+
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
613
|
+
radiusTopLeft,
|
|
614
|
+
radiusTopRight,
|
|
615
|
+
radiusBottomRight,
|
|
616
|
+
radiusBottomLeft,
|
|
617
|
+
]);
|
|
633
618
|
ctx.stroke();
|
|
634
619
|
}
|
|
635
620
|
|
|
636
621
|
if (isHovered && !isSelected) {
|
|
637
622
|
ctx.strokeStyle =
|
|
638
|
-
styles["--grid-color-hover"] ||
|
|
639
|
-
"rgba(255, 255, 255, 0.2)";
|
|
623
|
+
styles["--grid-color-hover"] || "rgba(255, 255, 255, 0.2)";
|
|
640
624
|
ctx.lineWidth = 1;
|
|
641
625
|
ctx.beginPath();
|
|
642
|
-
ctx.roundRect(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
radiusTopLeft,
|
|
649
|
-
radiusTopRight,
|
|
650
|
-
radiusBottomRight,
|
|
651
|
-
radiusBottomLeft,
|
|
652
|
-
],
|
|
653
|
-
);
|
|
626
|
+
ctx.roundRect(x + padding, viewportY, eventWidth, eventHeight, [
|
|
627
|
+
radiusTopLeft,
|
|
628
|
+
radiusTopRight,
|
|
629
|
+
radiusBottomRight,
|
|
630
|
+
radiusBottomLeft,
|
|
631
|
+
]);
|
|
654
632
|
ctx.stroke();
|
|
655
633
|
}
|
|
656
634
|
|
|
657
|
-
if (isStart && eventHeight >= 16) {
|
|
635
|
+
if (!showCompactEvents && isStart && eventHeight >= 16) {
|
|
658
636
|
const textColor = isSelected
|
|
659
637
|
? textInverse || "white"
|
|
660
638
|
: textPrimary || eventColor;
|
|
@@ -663,18 +641,9 @@ export function createEventsLayer(
|
|
|
663
641
|
ctx.textAlign = "left";
|
|
664
642
|
ctx.textBaseline = "top";
|
|
665
643
|
|
|
666
|
-
const textX =
|
|
667
|
-
x +
|
|
668
|
-
padding +
|
|
669
|
-
leftBorderWidth +
|
|
670
|
-
contentPadding +
|
|
671
|
-
1;
|
|
644
|
+
const textX = x + padding + leftBorderWidth + contentPadding + 1;
|
|
672
645
|
const textY = viewportY + 6;
|
|
673
|
-
let maxTextWidth =
|
|
674
|
-
eventWidth -
|
|
675
|
-
leftBorderWidth -
|
|
676
|
-
contentPadding -
|
|
677
|
-
4;
|
|
646
|
+
let maxTextWidth = eventWidth - leftBorderWidth - contentPadding - 4;
|
|
678
647
|
|
|
679
648
|
ctx.save();
|
|
680
649
|
ctx.beginPath();
|
|
@@ -689,42 +658,29 @@ export function createEventsLayer(
|
|
|
689
658
|
if (event.rrule) {
|
|
690
659
|
ctx.font = `11px ${fontFamily}`;
|
|
691
660
|
const recurIcon = "⟳";
|
|
692
|
-
const iconWidth =
|
|
693
|
-
ctx.measureText(recurIcon).width;
|
|
661
|
+
const iconWidth = ctx.measureText(recurIcon).width;
|
|
694
662
|
ctx.fillText(recurIcon, textX, textY);
|
|
695
663
|
maxTextWidth -= iconWidth + 4;
|
|
696
664
|
}
|
|
697
665
|
|
|
698
666
|
const titleStartX =
|
|
699
|
-
textX +
|
|
700
|
-
(event.rrule
|
|
701
|
-
? ctx.measureText("⟳").width + 4
|
|
702
|
-
: 0);
|
|
667
|
+
textX + (event.rrule ? ctx.measureText("⟳").width + 4 : 0);
|
|
703
668
|
let displayTitle = event.title;
|
|
704
669
|
ctx.font = `11px ${fontFamily}`;
|
|
705
|
-
const titleWidth =
|
|
706
|
-
ctx.measureText(displayTitle).width;
|
|
670
|
+
const titleWidth = ctx.measureText(displayTitle).width;
|
|
707
671
|
|
|
708
672
|
if (titleWidth > maxTextWidth) {
|
|
709
673
|
const ellipsis = "…";
|
|
710
|
-
const ellipsisWidth =
|
|
711
|
-
ctx.measureText(ellipsis).width;
|
|
674
|
+
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
712
675
|
|
|
713
676
|
let left = 0;
|
|
714
677
|
let right = displayTitle.length;
|
|
715
678
|
let bestFit = 0;
|
|
716
679
|
|
|
717
680
|
while (left <= right) {
|
|
718
|
-
const mid = Math.floor(
|
|
719
|
-
|
|
720
|
-
);
|
|
721
|
-
const testText = displayTitle.substring(
|
|
722
|
-
0,
|
|
723
|
-
mid,
|
|
724
|
-
);
|
|
725
|
-
const testWidth =
|
|
726
|
-
ctx.measureText(testText).width +
|
|
727
|
-
ellipsisWidth;
|
|
681
|
+
const mid = Math.floor((left + right) / 2);
|
|
682
|
+
const testText = displayTitle.substring(0, mid);
|
|
683
|
+
const testWidth = ctx.measureText(testText).width + ellipsisWidth;
|
|
728
684
|
|
|
729
685
|
if (testWidth <= maxTextWidth) {
|
|
730
686
|
bestFit = mid;
|
|
@@ -734,31 +690,23 @@ export function createEventsLayer(
|
|
|
734
690
|
}
|
|
735
691
|
}
|
|
736
692
|
|
|
737
|
-
displayTitle =
|
|
738
|
-
displayTitle.substring(0, bestFit) +
|
|
739
|
-
ellipsis;
|
|
693
|
+
displayTitle = displayTitle.substring(0, bestFit) + ellipsis;
|
|
740
694
|
}
|
|
741
695
|
|
|
742
|
-
ctx.fillText(
|
|
743
|
-
displayTitle,
|
|
744
|
-
titleStartX,
|
|
745
|
-
textY,
|
|
746
|
-
);
|
|
696
|
+
ctx.fillText(displayTitle, titleStartX, textY);
|
|
747
697
|
|
|
748
698
|
if (eventHeight >= 32) {
|
|
749
699
|
const formatTime = (date: Date) => {
|
|
750
700
|
const hours = date.getHours();
|
|
751
701
|
const minutes = date.getMinutes();
|
|
752
|
-
const ampm =
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
702
|
+
const ampm = hours >= 12 ? "PM" : "AM";
|
|
703
|
+
const displayHours = hours % 12 || 12;
|
|
704
|
+
return `${displayHours}:${minutes
|
|
705
|
+
.toString()
|
|
706
|
+
.padStart(2, "0")} ${ampm}`;
|
|
757
707
|
};
|
|
758
708
|
|
|
759
|
-
const startTime = formatTime(
|
|
760
|
-
event.start,
|
|
761
|
-
);
|
|
709
|
+
const startTime = formatTime(event.start);
|
|
762
710
|
const endTime = formatTime(event.end);
|
|
763
711
|
const timeText = `${startTime} – ${endTime}`;
|
|
764
712
|
|
|
@@ -769,27 +717,21 @@ export function createEventsLayer(
|
|
|
769
717
|
ctx.font = `10px ${fontFamily}`;
|
|
770
718
|
|
|
771
719
|
let displayTime = timeText;
|
|
772
|
-
|
|
773
|
-
ctx.measureText(displayTime).width;
|
|
720
|
+
const timeWidth = ctx.measureText(displayTime).width;
|
|
774
721
|
|
|
775
722
|
if (timeWidth > maxTextWidth) {
|
|
776
723
|
const ellipsis = "…";
|
|
777
|
-
const ellipsisWidth =
|
|
778
|
-
ctx.measureText(ellipsis).width;
|
|
724
|
+
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
779
725
|
|
|
780
726
|
let left = 0;
|
|
781
727
|
let right = displayTime.length;
|
|
782
728
|
let bestFit = 0;
|
|
783
729
|
|
|
784
730
|
while (left <= right) {
|
|
785
|
-
const mid = Math.floor(
|
|
786
|
-
|
|
787
|
-
);
|
|
788
|
-
const testText =
|
|
789
|
-
displayTime.substring(0, mid);
|
|
731
|
+
const mid = Math.floor((left + right) / 2);
|
|
732
|
+
const testText = displayTime.substring(0, mid);
|
|
790
733
|
const testWidth =
|
|
791
|
-
ctx.measureText(testText)
|
|
792
|
-
.width + ellipsisWidth;
|
|
734
|
+
ctx.measureText(testText).width + ellipsisWidth;
|
|
793
735
|
|
|
794
736
|
if (testWidth <= maxTextWidth) {
|
|
795
737
|
bestFit = mid;
|
|
@@ -799,16 +741,10 @@ export function createEventsLayer(
|
|
|
799
741
|
}
|
|
800
742
|
}
|
|
801
743
|
|
|
802
|
-
displayTime =
|
|
803
|
-
displayTime.substring(0, bestFit) +
|
|
804
|
-
ellipsis;
|
|
744
|
+
displayTime = displayTime.substring(0, bestFit) + ellipsis;
|
|
805
745
|
}
|
|
806
746
|
|
|
807
|
-
ctx.fillText(
|
|
808
|
-
displayTime,
|
|
809
|
-
textX,
|
|
810
|
-
textY + 14,
|
|
811
|
-
);
|
|
747
|
+
ctx.fillText(displayTime, textX, textY + 14);
|
|
812
748
|
}
|
|
813
749
|
|
|
814
750
|
ctx.restore();
|
|
@@ -825,130 +761,324 @@ export function createEventsLayer(
|
|
|
825
761
|
});
|
|
826
762
|
}
|
|
827
763
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
"
|
|
833
|
-
"
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
"November",
|
|
841
|
-
"December",
|
|
842
|
-
];
|
|
843
|
-
|
|
844
|
-
const monthBoundaries: {
|
|
845
|
-
monthKey: string;
|
|
846
|
-
monthName: string;
|
|
847
|
-
year: number;
|
|
848
|
-
yOffset: number;
|
|
849
|
-
}[] = [];
|
|
850
|
-
const seenMonths = new Set<string>();
|
|
764
|
+
if (!showTimeScale) {
|
|
765
|
+
const textMuted = styles["--text-muted"] || "rgba(255, 255, 255, 0.4)";
|
|
766
|
+
ctx.save();
|
|
767
|
+
ctx.font = `600 12px ${fontFamily}`;
|
|
768
|
+
ctx.textAlign = "left";
|
|
769
|
+
ctx.textBaseline = "top";
|
|
770
|
+
ctx.fillStyle = textMuted;
|
|
771
|
+
|
|
772
|
+
for (const overflow of overflowedDays.values()) {
|
|
773
|
+
if (overflow.maxEventsInRow <= 0) continue;
|
|
774
|
+
const week = allWeeks[overflow.weekIndex];
|
|
775
|
+
if (!week || week.height === 0) continue;
|
|
851
776
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
777
|
+
const { col } = getDayVisualPosition(overflow.dayIndex);
|
|
778
|
+
const visualRowY = week.yOffset;
|
|
779
|
+
const dotY =
|
|
780
|
+
visualRowY +
|
|
781
|
+
overflow.topInset +
|
|
782
|
+
overflow.maxEventsInRow *
|
|
783
|
+
(showCompactEvents
|
|
784
|
+
? COMPACT_EVENT_HEIGHT + 2
|
|
785
|
+
: MIN_EVENT_HEIGHT + 2) -
|
|
786
|
+
scrollTop;
|
|
787
|
+
const dotX = leftGutterWidth + col * dayWidth - scrollLeft + 8;
|
|
788
|
+
|
|
789
|
+
if (dotY < 0 || dotY > height) continue;
|
|
790
|
+
if (
|
|
791
|
+
dotX + ctx.measureText("...").width >
|
|
792
|
+
leftGutterWidth + (col + 1) * dayWidth - scrollLeft - 8
|
|
793
|
+
) {
|
|
794
|
+
continue;
|
|
870
795
|
}
|
|
796
|
+
ctx.fillText("...", dotX, dotY);
|
|
871
797
|
}
|
|
798
|
+
|
|
799
|
+
ctx.restore();
|
|
872
800
|
}
|
|
873
801
|
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
802
|
+
if (previewEvents.length > 0) {
|
|
803
|
+
for (const preview of previewEvents) {
|
|
804
|
+
if (showTimeScale) {
|
|
805
|
+
renderTimedPreview(
|
|
806
|
+
ctx,
|
|
807
|
+
preview,
|
|
808
|
+
allWeeks,
|
|
809
|
+
scrollTop,
|
|
810
|
+
scrollLeft,
|
|
811
|
+
dayWidth,
|
|
812
|
+
dayHeight,
|
|
813
|
+
leftGutterWidth,
|
|
814
|
+
fontFamily,
|
|
815
|
+
styles,
|
|
816
|
+
);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
881
819
|
|
|
882
|
-
|
|
883
|
-
|
|
820
|
+
renderStackedPreview(
|
|
821
|
+
ctx,
|
|
822
|
+
preview,
|
|
823
|
+
allWeeks,
|
|
824
|
+
scrollTop,
|
|
825
|
+
scrollLeft,
|
|
826
|
+
dayWidth,
|
|
827
|
+
dayHeight,
|
|
828
|
+
leftGutterWidth,
|
|
829
|
+
showCompactEvents,
|
|
830
|
+
monthHeaderWeeks,
|
|
831
|
+
dayOccupiedRows,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
return layer;
|
|
838
|
+
}
|
|
884
839
|
|
|
885
|
-
|
|
840
|
+
function renderTimedPreview(
|
|
841
|
+
ctx: CanvasRenderingContext2D,
|
|
842
|
+
preview: PreviewEventData,
|
|
843
|
+
allWeeks: WeekInfo[],
|
|
844
|
+
scrollTop: number,
|
|
845
|
+
scrollLeft: number,
|
|
846
|
+
dayWidth: number,
|
|
847
|
+
dayHeight: number,
|
|
848
|
+
leftGutterWidth: number,
|
|
849
|
+
fontFamily: string,
|
|
850
|
+
styles: Record<string, string>,
|
|
851
|
+
): void {
|
|
852
|
+
const drawBlock = (colX: number, top: number, bottom: number) => {
|
|
853
|
+
const bx = colX + DAY_CELL_EVENT_INSET;
|
|
854
|
+
const bw = dayWidth - DAY_CELL_EVENT_INSET * 2;
|
|
855
|
+
const bh = Math.max(4, bottom - top);
|
|
856
|
+
ctx.fillStyle = preview.color.fill;
|
|
857
|
+
ctx.beginPath();
|
|
858
|
+
ctx.roundRect(bx, top, bw, bh, 4);
|
|
859
|
+
ctx.fill();
|
|
860
|
+
ctx.strokeStyle = preview.color.stroke;
|
|
861
|
+
ctx.lineWidth = 1;
|
|
862
|
+
if (preview.color.dashed !== false) ctx.setLineDash([6, 3]);
|
|
863
|
+
ctx.stroke();
|
|
864
|
+
ctx.setLineDash([]);
|
|
865
|
+
};
|
|
886
866
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
);
|
|
896
|
-
const labelTopMargin = 32;
|
|
897
|
-
const finalTop =
|
|
898
|
-
labelY +
|
|
899
|
-
clampedStickyTop -
|
|
900
|
-
scrollTop +
|
|
901
|
-
labelTopMargin;
|
|
867
|
+
const getTimeY = (day: Date, hours: number, minutes: number) => {
|
|
868
|
+
const week = allWeeks.find((w) =>
|
|
869
|
+
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
870
|
+
);
|
|
871
|
+
if (!week) return null;
|
|
872
|
+
const totalMinutes = hours * 60 + minutes;
|
|
873
|
+
return week.yOffset + (totalMinutes / 1440) * dayHeight - scrollTop;
|
|
874
|
+
};
|
|
902
875
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
876
|
+
const getDayColumnX = (day: Date) => {
|
|
877
|
+
const week = allWeeks.find((w) =>
|
|
878
|
+
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
879
|
+
);
|
|
880
|
+
if (!week) return null;
|
|
881
|
+
const dayIndex = week.days.findIndex(
|
|
882
|
+
(d) => d.toDateString() === day.toDateString(),
|
|
883
|
+
);
|
|
884
|
+
if (dayIndex < 0) return null;
|
|
885
|
+
return leftGutterWidth + dayIndex * dayWidth - scrollLeft;
|
|
886
|
+
};
|
|
907
887
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
888
|
+
const sameDay = preview.start.toDateString() === preview.end.toDateString();
|
|
889
|
+
if (sameDay) {
|
|
890
|
+
const colX = getDayColumnX(preview.start);
|
|
891
|
+
const top = getTimeY(
|
|
892
|
+
preview.start,
|
|
893
|
+
preview.start.getHours(),
|
|
894
|
+
preview.start.getMinutes(),
|
|
895
|
+
);
|
|
896
|
+
const bottom = getTimeY(
|
|
897
|
+
preview.end,
|
|
898
|
+
preview.end.getHours(),
|
|
899
|
+
preview.end.getMinutes(),
|
|
900
|
+
);
|
|
901
|
+
if (colX != null && top != null && bottom != null) {
|
|
902
|
+
drawBlock(colX, top, bottom);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
const current = new Date(preview.start);
|
|
906
|
+
current.setHours(0, 0, 0, 0);
|
|
907
|
+
const endDay = new Date(preview.end);
|
|
908
|
+
endDay.setHours(23, 59, 59, 999);
|
|
909
|
+
while (current <= endDay) {
|
|
910
|
+
const colX = getDayColumnX(current);
|
|
911
|
+
if (colX != null) {
|
|
912
|
+
const isFirst = current.toDateString() === preview.start.toDateString();
|
|
913
|
+
const isLast = current.toDateString() === preview.end.toDateString();
|
|
914
|
+
let top: number | null;
|
|
915
|
+
let bottom: number | null;
|
|
916
|
+
if (isFirst) {
|
|
917
|
+
top = getTimeY(
|
|
918
|
+
current,
|
|
919
|
+
preview.start.getHours(),
|
|
920
|
+
preview.start.getMinutes(),
|
|
921
|
+
);
|
|
922
|
+
bottom = getTimeY(current, 23, 59);
|
|
923
|
+
} else if (isLast) {
|
|
924
|
+
top = getTimeY(current, 0, 0);
|
|
925
|
+
bottom = getTimeY(
|
|
926
|
+
current,
|
|
927
|
+
preview.end.getHours(),
|
|
928
|
+
preview.end.getMinutes(),
|
|
929
|
+
);
|
|
930
|
+
} else {
|
|
931
|
+
top = getTimeY(current, 0, 0);
|
|
932
|
+
bottom = getTimeY(current, 23, 59);
|
|
933
|
+
}
|
|
934
|
+
if (top != null && bottom != null) {
|
|
935
|
+
drawBlock(colX, top, bottom);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
current.setDate(current.getDate() + 1);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
931
941
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
942
|
+
const fmtTime = (d: Date) =>
|
|
943
|
+
`${d.getHours().toString().padStart(2, "0")}:${d
|
|
944
|
+
.getMinutes()
|
|
945
|
+
.toString()
|
|
946
|
+
.padStart(2, "0")}`;
|
|
947
|
+
const fmtDate = (d: Date) => `${d.getDate()}.${d.getMonth() + 1}`;
|
|
948
|
+
const label = sameDay
|
|
949
|
+
? `${fmtTime(preview.start)} – ${fmtTime(preview.end)}`
|
|
950
|
+
: `${fmtDate(preview.start)} ${fmtTime(preview.start)} – ${fmtDate(
|
|
951
|
+
preview.end,
|
|
952
|
+
)} ${fmtTime(preview.end)}`;
|
|
953
|
+
const durationMs = Math.abs(preview.end.getTime() - preview.start.getTime());
|
|
954
|
+
const durationMinutes = durationMs / (1000 * 60);
|
|
955
|
+
const isShortEvent = durationMinutes < 15;
|
|
956
|
+
const firstColX = getDayColumnX(preview.start);
|
|
957
|
+
const lastColX = getDayColumnX(preview.end);
|
|
958
|
+
const startY = getTimeY(
|
|
959
|
+
preview.start,
|
|
960
|
+
preview.start.getHours(),
|
|
961
|
+
preview.start.getMinutes(),
|
|
962
|
+
);
|
|
963
|
+
const endY = getTimeY(
|
|
964
|
+
preview.end,
|
|
965
|
+
preview.end.getHours(),
|
|
966
|
+
preview.end.getMinutes(),
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
if (firstColX != null && lastColX != null && startY != null && endY != null) {
|
|
970
|
+
ctx.font = `600 11px ${fontFamily}`;
|
|
971
|
+
ctx.fillStyle = preview.color.text;
|
|
972
|
+
ctx.textAlign = "left";
|
|
973
|
+
const labelColX = firstColX;
|
|
974
|
+
|
|
975
|
+
if (isShortEvent) {
|
|
976
|
+
const labelY = startY;
|
|
977
|
+
const labelBgY = labelY + 6;
|
|
978
|
+
const labelTextY = labelY + 10;
|
|
979
|
+
const textWidth = ctx.measureText(label).width;
|
|
980
|
+
const bgPaddingX = 6;
|
|
981
|
+
const bgElevated = styles["--bg-elevated"] || "rgba(0, 0, 0, 0.8)";
|
|
982
|
+
ctx.fillStyle = bgElevated;
|
|
983
|
+
ctx.beginPath();
|
|
984
|
+
ctx.roundRect(labelColX + 4, labelBgY, textWidth + bgPaddingX * 2, 16, 4);
|
|
985
|
+
ctx.fill();
|
|
986
|
+
ctx.fillStyle = preview.color.text;
|
|
987
|
+
ctx.textBaseline = "top";
|
|
988
|
+
ctx.fillText(label, labelColX + 4 + bgPaddingX, labelTextY);
|
|
989
|
+
} else {
|
|
990
|
+
ctx.textBaseline = "top";
|
|
991
|
+
ctx.fillText(label, labelColX + DAY_CELL_EVENT_INSET + 4, startY + 4);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function renderStackedPreview(
|
|
997
|
+
ctx: CanvasRenderingContext2D,
|
|
998
|
+
preview: PreviewEventData,
|
|
999
|
+
allWeeks: WeekInfo[],
|
|
1000
|
+
scrollTop: number,
|
|
1001
|
+
scrollLeft: number,
|
|
1002
|
+
dayWidth: number,
|
|
1003
|
+
dayHeight: number,
|
|
1004
|
+
leftGutterWidth: number,
|
|
1005
|
+
showCompactEvents: boolean,
|
|
1006
|
+
monthHeaderWeeks: Set<number>,
|
|
1007
|
+
dayOccupiedRows: Map<string, Set<number>>,
|
|
1008
|
+
): void {
|
|
1009
|
+
const rowHeight = showCompactEvents
|
|
1010
|
+
? COMPACT_EVENT_HEIGHT + 2
|
|
1011
|
+
: MIN_EVENT_HEIGHT + 2;
|
|
1012
|
+
const eventBlockHeight = showCompactEvents
|
|
1013
|
+
? COMPACT_EVENT_HEIGHT
|
|
1014
|
+
: MIN_EVENT_HEIGHT;
|
|
1015
|
+
|
|
1016
|
+
for (const [weekIndex, week] of allWeeks.entries()) {
|
|
1017
|
+
if (week.height === 0) continue;
|
|
1018
|
+
const weekStart = week.days[0];
|
|
1019
|
+
const weekEnd = week.days[6];
|
|
1020
|
+
if (!weekStart || !weekEnd) continue;
|
|
1021
|
+
const weekStartMs = weekStart.getTime();
|
|
1022
|
+
const weekEndMs = weekEnd.getTime() + 86399999;
|
|
1023
|
+
if (
|
|
1024
|
+
preview.end.getTime() < weekStartMs ||
|
|
1025
|
+
preview.start.getTime() > weekEndMs
|
|
1026
|
+
) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let startDayIndex = 0;
|
|
1031
|
+
let endDayIndex = 6;
|
|
1032
|
+
if (preview.start.getTime() >= weekStartMs) {
|
|
1033
|
+
startDayIndex = getDayIndexInWeek(week, preview.start);
|
|
1034
|
+
}
|
|
1035
|
+
if (preview.end.getTime() <= weekEndMs) {
|
|
1036
|
+
endDayIndex = getDayIndexInWeek(week, preview.end);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
let rowIndex = 0;
|
|
1040
|
+
while (true) {
|
|
1041
|
+
let rowFree = true;
|
|
1042
|
+
for (let d = startDayIndex; d <= endDayIndex; d++) {
|
|
1043
|
+
const occupied = dayOccupiedRows.get(`${weekIndex}-${d}`);
|
|
1044
|
+
if (occupied?.has(rowIndex)) {
|
|
1045
|
+
rowFree = false;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
938
1048
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1049
|
+
if (rowFree) break;
|
|
1050
|
+
rowIndex++;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const topInset = monthHeaderWeeks.has(weekIndex)
|
|
1054
|
+
? MONTH_LABEL_HEIGHT + MONTH_LABEL_GAP + DAY_CELL_EVENT_INSET
|
|
1055
|
+
: DAY_CELL_EVENT_INSET;
|
|
1056
|
+
const x =
|
|
1057
|
+
leftGutterWidth +
|
|
1058
|
+
startDayIndex * dayWidth -
|
|
1059
|
+
scrollLeft +
|
|
1060
|
+
DAY_CELL_EVENT_INSET;
|
|
1061
|
+
const width =
|
|
1062
|
+
(endDayIndex - startDayIndex + 1) * dayWidth - DAY_CELL_EVENT_INSET * 2;
|
|
1063
|
+
const y = week.yOffset + topInset + rowIndex * rowHeight - scrollTop;
|
|
1064
|
+
|
|
1065
|
+
ctx.fillStyle = preview.color.fill;
|
|
1066
|
+
ctx.beginPath();
|
|
1067
|
+
ctx.roundRect(x, y, width, eventBlockHeight, 4);
|
|
1068
|
+
ctx.fill();
|
|
1069
|
+
ctx.strokeStyle = preview.color.stroke;
|
|
1070
|
+
ctx.lineWidth = 1;
|
|
1071
|
+
if (preview.color.dashed !== false) ctx.setLineDash([6, 3]);
|
|
1072
|
+
ctx.stroke();
|
|
1073
|
+
ctx.setLineDash([]);
|
|
1074
|
+
}
|
|
942
1075
|
}
|
|
943
1076
|
|
|
944
1077
|
function getDayIndexInWeek(week: WeekInfo, date: Date) {
|
|
945
1078
|
const dateStart = new Date(date).setHours(0, 0, 0, 0);
|
|
946
1079
|
for (let i = 0; i < 7; i++) {
|
|
947
1080
|
const weekDay = week.days[i];
|
|
948
|
-
if (
|
|
949
|
-
weekDay &&
|
|
950
|
-
new Date(weekDay).setHours(0, 0, 0, 0) === dateStart
|
|
951
|
-
) {
|
|
1081
|
+
if (weekDay && new Date(weekDay).setHours(0, 0, 0, 0) === dateStart) {
|
|
952
1082
|
return i;
|
|
953
1083
|
}
|
|
954
1084
|
}
|