trud-calendar-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +471 -0
- package/dist/index.d.ts +471 -0
- package/dist/index.js +838 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
// src/constants/index.ts
|
|
2
|
+
var DEFAULT_LABELS = {
|
|
3
|
+
today: "Today",
|
|
4
|
+
month: "Month",
|
|
5
|
+
week: "Week",
|
|
6
|
+
day: "Day",
|
|
7
|
+
agenda: "Agenda",
|
|
8
|
+
allDay: "all-day",
|
|
9
|
+
noEvents: "No events in this period",
|
|
10
|
+
more: (n) => `+${n} more`
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_LOCALE = {
|
|
13
|
+
locale: "en-US",
|
|
14
|
+
weekStartsOn: 0
|
|
15
|
+
};
|
|
16
|
+
var DEFAULT_VIEW = "month";
|
|
17
|
+
var HOURS_IN_DAY = 24;
|
|
18
|
+
var MINUTES_IN_HOUR = 60;
|
|
19
|
+
var MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
|
|
20
|
+
var VIEWS = ["month", "week", "day", "agenda"];
|
|
21
|
+
var DEFAULT_DAY_START_HOUR = 0;
|
|
22
|
+
var DEFAULT_DAY_END_HOUR = 24;
|
|
23
|
+
|
|
24
|
+
// src/utils/date.ts
|
|
25
|
+
function parseDate(iso) {
|
|
26
|
+
if (iso.length === 10) {
|
|
27
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
28
|
+
return new Date(y, m - 1, d);
|
|
29
|
+
}
|
|
30
|
+
return new Date(iso);
|
|
31
|
+
}
|
|
32
|
+
function toDateString(date) {
|
|
33
|
+
const y = date.getFullYear();
|
|
34
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
35
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
36
|
+
return `${y}-${m}-${d}`;
|
|
37
|
+
}
|
|
38
|
+
function toDateTimeString(date) {
|
|
39
|
+
const ds = toDateString(date);
|
|
40
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
41
|
+
const min = String(date.getMinutes()).padStart(2, "0");
|
|
42
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
43
|
+
return `${ds}T${h}:${min}:${s}`;
|
|
44
|
+
}
|
|
45
|
+
function addDays(date, days) {
|
|
46
|
+
const d = parseDate(date);
|
|
47
|
+
d.setDate(d.getDate() + days);
|
|
48
|
+
return toDateString(d);
|
|
49
|
+
}
|
|
50
|
+
function addMonths(date, months) {
|
|
51
|
+
const d = parseDate(date);
|
|
52
|
+
d.setMonth(d.getMonth() + months);
|
|
53
|
+
return toDateString(d);
|
|
54
|
+
}
|
|
55
|
+
function addWeeks(date, weeks) {
|
|
56
|
+
return addDays(date, weeks * 7);
|
|
57
|
+
}
|
|
58
|
+
function startOfWeek(date, weekStartsOn = 0) {
|
|
59
|
+
const d = parseDate(date);
|
|
60
|
+
const day = d.getDay();
|
|
61
|
+
const diff = (day - weekStartsOn + 7) % 7;
|
|
62
|
+
d.setDate(d.getDate() - diff);
|
|
63
|
+
return toDateString(d);
|
|
64
|
+
}
|
|
65
|
+
function startOfMonth(date) {
|
|
66
|
+
const d = parseDate(date);
|
|
67
|
+
return toDateString(new Date(d.getFullYear(), d.getMonth(), 1));
|
|
68
|
+
}
|
|
69
|
+
function endOfMonth(date) {
|
|
70
|
+
const d = parseDate(date);
|
|
71
|
+
return toDateString(new Date(d.getFullYear(), d.getMonth() + 1, 0));
|
|
72
|
+
}
|
|
73
|
+
function isSameDay(a, b) {
|
|
74
|
+
return a.slice(0, 10) === b.slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
function isSameMonth(a, b) {
|
|
77
|
+
return a.slice(0, 7) === b.slice(0, 7);
|
|
78
|
+
}
|
|
79
|
+
function isToday(date) {
|
|
80
|
+
return date.slice(0, 10) === toDateString(/* @__PURE__ */ new Date());
|
|
81
|
+
}
|
|
82
|
+
function isBefore(a, b) {
|
|
83
|
+
return a < b;
|
|
84
|
+
}
|
|
85
|
+
function isAfter(a, b) {
|
|
86
|
+
return a > b;
|
|
87
|
+
}
|
|
88
|
+
function rangesOverlap(startA, endA, startB, endB) {
|
|
89
|
+
return startA < endB && startB < endA;
|
|
90
|
+
}
|
|
91
|
+
function dateInRange(date, rangeStart, rangeEnd) {
|
|
92
|
+
const d = date.slice(0, 10);
|
|
93
|
+
return d >= rangeStart.slice(0, 10) && d <= rangeEnd.slice(0, 10);
|
|
94
|
+
}
|
|
95
|
+
function daysBetween(a, b) {
|
|
96
|
+
const dateA = parseDate(a);
|
|
97
|
+
const dateB = parseDate(b);
|
|
98
|
+
const diffMs = dateB.getTime() - dateA.getTime();
|
|
99
|
+
return Math.round(diffMs / (1e3 * 60 * 60 * 24));
|
|
100
|
+
}
|
|
101
|
+
function eachDayOfRange(start, end) {
|
|
102
|
+
const days = [];
|
|
103
|
+
let current = start;
|
|
104
|
+
while (current <= end) {
|
|
105
|
+
days.push(current);
|
|
106
|
+
current = addDays(current, 1);
|
|
107
|
+
}
|
|
108
|
+
return days;
|
|
109
|
+
}
|
|
110
|
+
function getWeekDays(weekStart) {
|
|
111
|
+
return Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
|
112
|
+
}
|
|
113
|
+
function getMonthViewRange(date, weekStartsOn = 0) {
|
|
114
|
+
const monthStart = startOfMonth(date);
|
|
115
|
+
const monthEnd = endOfMonth(date);
|
|
116
|
+
const start = startOfWeek(monthStart, weekStartsOn);
|
|
117
|
+
const end = addDays(start, 41);
|
|
118
|
+
return { start, end: end > monthEnd ? end : addDays(start, 41) };
|
|
119
|
+
}
|
|
120
|
+
function getWeekViewRange(date, weekStartsOn = 0) {
|
|
121
|
+
const start = startOfWeek(date, weekStartsOn);
|
|
122
|
+
const end = addDays(start, 6);
|
|
123
|
+
return { start, end };
|
|
124
|
+
}
|
|
125
|
+
function getVisibleRange(date, view, weekStartsOn = 0) {
|
|
126
|
+
switch (view) {
|
|
127
|
+
case "month":
|
|
128
|
+
return getMonthViewRange(date, weekStartsOn);
|
|
129
|
+
case "week":
|
|
130
|
+
return getWeekViewRange(date, weekStartsOn);
|
|
131
|
+
case "day":
|
|
132
|
+
return { start: date, end: date };
|
|
133
|
+
case "agenda":
|
|
134
|
+
return { start: date, end: addDays(date, 30) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function getTimeOfDay(datetime) {
|
|
138
|
+
const d = parseDate(datetime);
|
|
139
|
+
return d.getHours() + d.getMinutes() / 60;
|
|
140
|
+
}
|
|
141
|
+
function getDurationHours(start, end) {
|
|
142
|
+
const startDate = parseDate(start);
|
|
143
|
+
const endDate = parseDate(end);
|
|
144
|
+
return (endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60);
|
|
145
|
+
}
|
|
146
|
+
function getHourLabels(startHour = 0, endHour = 24, locale = "en-US") {
|
|
147
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
148
|
+
hour: "numeric",
|
|
149
|
+
hour12: true
|
|
150
|
+
});
|
|
151
|
+
const labels = [];
|
|
152
|
+
const base = new Date(2024, 0, 1);
|
|
153
|
+
for (let h = startHour; h < endHour; h++) {
|
|
154
|
+
base.setHours(h, 0, 0, 0);
|
|
155
|
+
labels.push(formatter.format(base));
|
|
156
|
+
}
|
|
157
|
+
return labels;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/utils/format.ts
|
|
161
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
162
|
+
function getFormatter(locale, options) {
|
|
163
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
164
|
+
let fmt = formatterCache.get(key);
|
|
165
|
+
if (!fmt) {
|
|
166
|
+
fmt = new Intl.DateTimeFormat(locale, options);
|
|
167
|
+
formatterCache.set(key, fmt);
|
|
168
|
+
}
|
|
169
|
+
return fmt;
|
|
170
|
+
}
|
|
171
|
+
function formatToolbarTitle(date, view, locale = "en-US") {
|
|
172
|
+
const d = parseDate(date);
|
|
173
|
+
switch (view) {
|
|
174
|
+
case "month":
|
|
175
|
+
return getFormatter(locale, { month: "long", year: "numeric" }).format(d);
|
|
176
|
+
case "week": {
|
|
177
|
+
const weekFmt = getFormatter(locale, {
|
|
178
|
+
month: "short",
|
|
179
|
+
day: "numeric",
|
|
180
|
+
year: "numeric"
|
|
181
|
+
});
|
|
182
|
+
return weekFmt.format(d);
|
|
183
|
+
}
|
|
184
|
+
case "day":
|
|
185
|
+
return getFormatter(locale, {
|
|
186
|
+
weekday: "long",
|
|
187
|
+
month: "long",
|
|
188
|
+
day: "numeric",
|
|
189
|
+
year: "numeric"
|
|
190
|
+
}).format(d);
|
|
191
|
+
case "agenda":
|
|
192
|
+
return getFormatter(locale, { month: "long", year: "numeric" }).format(d);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function formatWeekdayShort(date, locale = "en-US") {
|
|
196
|
+
return getFormatter(locale, { weekday: "short" }).format(parseDate(date));
|
|
197
|
+
}
|
|
198
|
+
function formatWeekdayNarrow(date, locale = "en-US") {
|
|
199
|
+
return getFormatter(locale, { weekday: "narrow" }).format(parseDate(date));
|
|
200
|
+
}
|
|
201
|
+
function formatDayNumber(date, locale = "en-US") {
|
|
202
|
+
return getFormatter(locale, { day: "numeric" }).format(parseDate(date));
|
|
203
|
+
}
|
|
204
|
+
function formatTime(datetime, locale = "en-US") {
|
|
205
|
+
return getFormatter(locale, {
|
|
206
|
+
hour: "numeric",
|
|
207
|
+
minute: "2-digit",
|
|
208
|
+
hour12: true
|
|
209
|
+
}).format(parseDate(datetime));
|
|
210
|
+
}
|
|
211
|
+
function formatTimeRange(start, end, locale = "en-US") {
|
|
212
|
+
return `${formatTime(start, locale)} \u2013 ${formatTime(end, locale)}`;
|
|
213
|
+
}
|
|
214
|
+
function formatAgendaDate(date, locale = "en-US") {
|
|
215
|
+
return getFormatter(locale, {
|
|
216
|
+
weekday: "long",
|
|
217
|
+
month: "long",
|
|
218
|
+
day: "numeric"
|
|
219
|
+
}).format(parseDate(date));
|
|
220
|
+
}
|
|
221
|
+
function formatMonthDay(date, locale = "en-US") {
|
|
222
|
+
return getFormatter(locale, {
|
|
223
|
+
month: "short",
|
|
224
|
+
day: "numeric"
|
|
225
|
+
}).format(parseDate(date));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/state/reducer.ts
|
|
229
|
+
function createInitialState(date, view) {
|
|
230
|
+
return {
|
|
231
|
+
currentDate: date ?? toDateString(/* @__PURE__ */ new Date()),
|
|
232
|
+
view: view ?? "month"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function navigateByView(date, view, direction) {
|
|
236
|
+
switch (view) {
|
|
237
|
+
case "month":
|
|
238
|
+
return addMonths(date, direction);
|
|
239
|
+
case "week":
|
|
240
|
+
return addWeeks(date, direction);
|
|
241
|
+
case "day":
|
|
242
|
+
return addDays(date, direction);
|
|
243
|
+
case "agenda":
|
|
244
|
+
return addMonths(date, direction);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function calendarReducer(state, action) {
|
|
248
|
+
switch (action.type) {
|
|
249
|
+
case "NAVIGATE_PREV":
|
|
250
|
+
return {
|
|
251
|
+
...state,
|
|
252
|
+
currentDate: navigateByView(state.currentDate, state.view, -1)
|
|
253
|
+
};
|
|
254
|
+
case "NAVIGATE_NEXT":
|
|
255
|
+
return {
|
|
256
|
+
...state,
|
|
257
|
+
currentDate: navigateByView(state.currentDate, state.view, 1)
|
|
258
|
+
};
|
|
259
|
+
case "NAVIGATE_TODAY":
|
|
260
|
+
return {
|
|
261
|
+
...state,
|
|
262
|
+
currentDate: toDateString(/* @__PURE__ */ new Date())
|
|
263
|
+
};
|
|
264
|
+
case "SET_DATE":
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
currentDate: action.payload
|
|
268
|
+
};
|
|
269
|
+
case "SET_VIEW":
|
|
270
|
+
return {
|
|
271
|
+
...state,
|
|
272
|
+
view: action.payload
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/utils/events.ts
|
|
278
|
+
function sortEvents(events) {
|
|
279
|
+
return [...events].sort((a, b) => {
|
|
280
|
+
const startCmp = a.start.localeCompare(b.start);
|
|
281
|
+
if (startCmp !== 0) return startCmp;
|
|
282
|
+
const durA = parseDate(a.end).getTime() - parseDate(a.start).getTime();
|
|
283
|
+
const durB = parseDate(b.end).getTime() - parseDate(b.start).getTime();
|
|
284
|
+
return durB - durA;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function filterEventsInRange(events, rangeStart, rangeEnd) {
|
|
288
|
+
const start = rangeStart + "T00:00:00";
|
|
289
|
+
const end = rangeEnd + "T23:59:59";
|
|
290
|
+
return events.filter((e) => rangesOverlap(e.start, e.end, start, end));
|
|
291
|
+
}
|
|
292
|
+
function getEventsForDay(events, date) {
|
|
293
|
+
return filterEventsInRange(events, date, date);
|
|
294
|
+
}
|
|
295
|
+
function isMultiDayEvent(event) {
|
|
296
|
+
return !isSameDay(event.start, event.end) || !!event.allDay;
|
|
297
|
+
}
|
|
298
|
+
function partitionEvents(events) {
|
|
299
|
+
const allDay = [];
|
|
300
|
+
const timed = [];
|
|
301
|
+
for (const event of events) {
|
|
302
|
+
if (event.allDay || isMultiDayEvent(event)) {
|
|
303
|
+
allDay.push(event);
|
|
304
|
+
} else {
|
|
305
|
+
timed.push(event);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { allDay, timed };
|
|
309
|
+
}
|
|
310
|
+
function segmentMultiDayEvent(event, rangeStart, rangeEnd) {
|
|
311
|
+
const eventStartDate = event.start.slice(0, 10);
|
|
312
|
+
const eventEndDate = event.end.slice(0, 10);
|
|
313
|
+
const segStart = eventStartDate > rangeStart ? eventStartDate : rangeStart;
|
|
314
|
+
const segEnd = eventEndDate < rangeEnd ? eventEndDate : rangeEnd;
|
|
315
|
+
return eachDayOfRange(segStart, segEnd).map((date) => ({
|
|
316
|
+
event,
|
|
317
|
+
date,
|
|
318
|
+
isStart: date === eventStartDate,
|
|
319
|
+
isEnd: date === eventEndDate
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
function getEventSegments(events, rangeStart, rangeEnd) {
|
|
323
|
+
const allDayEvents = events.filter((e) => e.allDay || isMultiDayEvent(e));
|
|
324
|
+
const segments = [];
|
|
325
|
+
for (const event of allDayEvents) {
|
|
326
|
+
segments.push(...segmentMultiDayEvent(event, rangeStart, rangeEnd));
|
|
327
|
+
}
|
|
328
|
+
return segments;
|
|
329
|
+
}
|
|
330
|
+
function segmentTimedMultiDayEvent(event, days, dayStartHour, dayEndHour) {
|
|
331
|
+
const eventStartDate = event.start.slice(0, 10);
|
|
332
|
+
const eventEndDate = event.end.slice(0, 10);
|
|
333
|
+
const eventStartHour = getTimeOfDay(event.start);
|
|
334
|
+
const eventEndHour = getTimeOfDay(event.end);
|
|
335
|
+
const segments = [];
|
|
336
|
+
for (const day of days) {
|
|
337
|
+
if (day < eventStartDate || day > eventEndDate) continue;
|
|
338
|
+
const isStart = day === eventStartDate;
|
|
339
|
+
const isEnd = day === eventEndDate;
|
|
340
|
+
let startHour;
|
|
341
|
+
let endHour;
|
|
342
|
+
if (isStart && isEnd) {
|
|
343
|
+
startHour = eventStartHour;
|
|
344
|
+
endHour = eventEndHour;
|
|
345
|
+
} else if (isStart) {
|
|
346
|
+
startHour = eventStartHour;
|
|
347
|
+
endHour = dayEndHour;
|
|
348
|
+
} else if (isEnd) {
|
|
349
|
+
startHour = dayStartHour;
|
|
350
|
+
endHour = eventEndHour;
|
|
351
|
+
} else {
|
|
352
|
+
startHour = dayStartHour;
|
|
353
|
+
endHour = dayEndHour;
|
|
354
|
+
}
|
|
355
|
+
segments.push({ event, day, startHour, endHour, isStart, isEnd });
|
|
356
|
+
}
|
|
357
|
+
return segments;
|
|
358
|
+
}
|
|
359
|
+
function buildOverlapGroups(events) {
|
|
360
|
+
if (events.length === 0) return [];
|
|
361
|
+
const sorted = sortEvents(events);
|
|
362
|
+
const groups = [];
|
|
363
|
+
let currentGroup = [sorted[0]];
|
|
364
|
+
let groupEnd = sorted[0].end;
|
|
365
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
366
|
+
const event = sorted[i];
|
|
367
|
+
if (event.start < groupEnd) {
|
|
368
|
+
currentGroup.push(event);
|
|
369
|
+
if (event.end > groupEnd) {
|
|
370
|
+
groupEnd = event.end;
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
groups.push({ events: currentGroup });
|
|
374
|
+
currentGroup = [event];
|
|
375
|
+
groupEnd = event.end;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
groups.push({ events: currentGroup });
|
|
379
|
+
return groups;
|
|
380
|
+
}
|
|
381
|
+
function assignColumns(group) {
|
|
382
|
+
const sorted = sortEvents(group.events);
|
|
383
|
+
const columns = [];
|
|
384
|
+
const result = [];
|
|
385
|
+
for (const event of sorted) {
|
|
386
|
+
let placed = false;
|
|
387
|
+
for (let col = 0; col < columns.length; col++) {
|
|
388
|
+
const lastInCol = columns[col][columns[col].length - 1];
|
|
389
|
+
if (lastInCol.end <= event.start) {
|
|
390
|
+
columns[col].push(event);
|
|
391
|
+
result.push({ event, column: col });
|
|
392
|
+
placed = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!placed) {
|
|
397
|
+
columns.push([event]);
|
|
398
|
+
result.push({ event, column: columns.length - 1 });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const totalColumns = columns.length;
|
|
402
|
+
return result.map((r) => ({ ...r, totalColumns }));
|
|
403
|
+
}
|
|
404
|
+
function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24) {
|
|
405
|
+
const totalHours = dayEndHour - dayStartHour;
|
|
406
|
+
const groups = buildOverlapGroups(events);
|
|
407
|
+
const positioned = [];
|
|
408
|
+
for (const group of groups) {
|
|
409
|
+
const columns = assignColumns(group);
|
|
410
|
+
for (const { event, column, totalColumns } of columns) {
|
|
411
|
+
const startTime = Math.max(getTimeOfDay(event.start), dayStartHour);
|
|
412
|
+
const endTime = Math.min(getTimeOfDay(event.end), dayEndHour);
|
|
413
|
+
const top = (startTime - dayStartHour) / totalHours * 100;
|
|
414
|
+
const height = Math.max((endTime - startTime) / totalHours * 100, 1);
|
|
415
|
+
positioned.push({
|
|
416
|
+
event,
|
|
417
|
+
column,
|
|
418
|
+
totalColumns,
|
|
419
|
+
top,
|
|
420
|
+
height
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return positioned;
|
|
425
|
+
}
|
|
426
|
+
function groupEventsByDate(events, rangeStart, rangeEnd) {
|
|
427
|
+
const filtered = filterEventsInRange(events, rangeStart, rangeEnd);
|
|
428
|
+
const sorted = sortEvents(filtered);
|
|
429
|
+
const groups = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const event of sorted) {
|
|
431
|
+
const date = event.start.slice(0, 10);
|
|
432
|
+
const existing = groups.get(date);
|
|
433
|
+
if (existing) {
|
|
434
|
+
existing.push(event);
|
|
435
|
+
} else {
|
|
436
|
+
groups.set(date, [event]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return groups;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/utils/time.ts
|
|
443
|
+
function snapToIncrement(fractionalHour, minutes = 15) {
|
|
444
|
+
const totalMinutes = fractionalHour * 60;
|
|
445
|
+
const snapped = Math.round(totalMinutes / minutes) * minutes;
|
|
446
|
+
return snapped / 60;
|
|
447
|
+
}
|
|
448
|
+
function fractionalHourToDateTime(day, fractionalHour) {
|
|
449
|
+
const totalMinutes = Math.round(fractionalHour * 60);
|
|
450
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
451
|
+
const mins = totalMinutes % 60;
|
|
452
|
+
const hh = String(Math.max(0, Math.min(23, hours))).padStart(2, "0");
|
|
453
|
+
const mm = String(mins).padStart(2, "0");
|
|
454
|
+
return `${day}T${hh}:${mm}:00`;
|
|
455
|
+
}
|
|
456
|
+
function yPositionToFractionalHour(clientY, columnRect, dayStart, dayEnd) {
|
|
457
|
+
const relativeY = clientY - columnRect.top;
|
|
458
|
+
const totalHeight = columnRect.height;
|
|
459
|
+
const totalHours = dayEnd - dayStart;
|
|
460
|
+
const hoursFromTop = relativeY / totalHeight * totalHours;
|
|
461
|
+
return dayStart + hoursFromTop;
|
|
462
|
+
}
|
|
463
|
+
function normalizeRange(a, b) {
|
|
464
|
+
return a <= b ? { start: a, end: b } : { start: b, end: a };
|
|
465
|
+
}
|
|
466
|
+
function computeDropPosition(day, clientY, columnRect, dayStartHour, dayEndHour, durationMs) {
|
|
467
|
+
const fractionalHour = yPositionToFractionalHour(clientY, columnRect, dayStartHour, dayEndHour);
|
|
468
|
+
const snapped = snapToIncrement(fractionalHour, 15);
|
|
469
|
+
const clamped = Math.max(dayStartHour, Math.min(dayEndHour - durationMs / 36e5, snapped));
|
|
470
|
+
const newStart = fractionalHourToDateTime(day, clamped);
|
|
471
|
+
const newStartMs = new Date(newStart).getTime();
|
|
472
|
+
const newEndDate = new Date(newStartMs + durationMs);
|
|
473
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
474
|
+
const endDay = toDateString(newEndDate);
|
|
475
|
+
const newEnd = `${endDay}T${pad(newEndDate.getHours())}:${pad(newEndDate.getMinutes())}:${pad(newEndDate.getSeconds())}`;
|
|
476
|
+
return { newStart, newEnd };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/utils/recurrence.ts
|
|
480
|
+
var DAY_MAP = {
|
|
481
|
+
SU: 0,
|
|
482
|
+
MO: 1,
|
|
483
|
+
TU: 2,
|
|
484
|
+
WE: 3,
|
|
485
|
+
TH: 4,
|
|
486
|
+
FR: 5,
|
|
487
|
+
SA: 6
|
|
488
|
+
};
|
|
489
|
+
var MAX_OCCURRENCES = 730;
|
|
490
|
+
function generateOccurrences(rule, eventStartDate, rangeStart, rangeEnd, exDates = []) {
|
|
491
|
+
const results = [];
|
|
492
|
+
const exSet = new Set(exDates);
|
|
493
|
+
const interval = rule.interval ?? 1;
|
|
494
|
+
const start = parseDate(eventStartDate);
|
|
495
|
+
const rEnd = parseDate(rangeEnd);
|
|
496
|
+
const rStart = parseDate(rangeStart);
|
|
497
|
+
const untilDate = rule.until ? parseDate(rule.until) : null;
|
|
498
|
+
const maxCount = rule.count ?? MAX_OCCURRENCES;
|
|
499
|
+
switch (rule.freq) {
|
|
500
|
+
case "daily":
|
|
501
|
+
generateDaily(start, interval, rStart, rEnd, untilDate, maxCount, exSet, results);
|
|
502
|
+
break;
|
|
503
|
+
case "weekly":
|
|
504
|
+
generateWeekly(start, interval, rule.byDay, rStart, rEnd, untilDate, maxCount, exSet, results);
|
|
505
|
+
break;
|
|
506
|
+
case "monthly":
|
|
507
|
+
generateMonthly(start, interval, rule, rStart, rEnd, untilDate, maxCount, exSet, results);
|
|
508
|
+
break;
|
|
509
|
+
case "yearly":
|
|
510
|
+
generateYearly(start, interval, rStart, rEnd, untilDate, maxCount, exSet, results);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
return results;
|
|
514
|
+
}
|
|
515
|
+
function generateDaily(start, interval, rangeStart, rangeEnd, until, maxCount, exSet, results) {
|
|
516
|
+
const cursor = new Date(start);
|
|
517
|
+
let count = 0;
|
|
518
|
+
if (cursor < rangeStart && maxCount === MAX_OCCURRENCES) {
|
|
519
|
+
const daysDiff = Math.floor((rangeStart.getTime() - cursor.getTime()) / (1e3 * 60 * 60 * 24));
|
|
520
|
+
const stepsToSkip = Math.floor(daysDiff / interval);
|
|
521
|
+
if (stepsToSkip > 0) {
|
|
522
|
+
cursor.setDate(cursor.getDate() + stepsToSkip * interval);
|
|
523
|
+
cursor.setDate(cursor.getDate() - interval);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
while (count < maxCount) {
|
|
527
|
+
if (until && cursor > until) break;
|
|
528
|
+
if (cursor > rangeEnd) break;
|
|
529
|
+
const ds = toDateString(cursor);
|
|
530
|
+
if (cursor >= rangeStart && !exSet.has(ds)) {
|
|
531
|
+
results.push(ds);
|
|
532
|
+
}
|
|
533
|
+
count++;
|
|
534
|
+
cursor.setDate(cursor.getDate() + interval);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function generateWeekly(start, interval, byDay, rangeStart, rangeEnd, until, maxCount, exSet, results) {
|
|
538
|
+
const targetDays = byDay ? byDay.map((d) => DAY_MAP[d]).sort((a, b) => a - b) : [start.getDay()];
|
|
539
|
+
const cursor = new Date(start);
|
|
540
|
+
cursor.setDate(cursor.getDate() - cursor.getDay());
|
|
541
|
+
if (cursor < rangeStart && maxCount === MAX_OCCURRENCES) {
|
|
542
|
+
const daysDiff = Math.floor((rangeStart.getTime() - cursor.getTime()) / (1e3 * 60 * 60 * 24));
|
|
543
|
+
const weeksToSkip = Math.floor(daysDiff / (7 * interval));
|
|
544
|
+
if (weeksToSkip > 1) {
|
|
545
|
+
cursor.setDate(cursor.getDate() + (weeksToSkip - 1) * 7 * interval);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
let count = 0;
|
|
549
|
+
while (count < maxCount) {
|
|
550
|
+
if (until && cursor > until) break;
|
|
551
|
+
if (cursor > rangeEnd && results.length > 0) break;
|
|
552
|
+
if (cursor.getTime() > rangeEnd.getTime() + 7 * 24 * 60 * 60 * 1e3) break;
|
|
553
|
+
const weekStart = new Date(cursor);
|
|
554
|
+
for (const dayNum of targetDays) {
|
|
555
|
+
const candidate = new Date(weekStart);
|
|
556
|
+
candidate.setDate(weekStart.getDate() + dayNum);
|
|
557
|
+
if (candidate < start) continue;
|
|
558
|
+
if (until && candidate > until) break;
|
|
559
|
+
if (count >= maxCount) break;
|
|
560
|
+
const ds = toDateString(candidate);
|
|
561
|
+
if (candidate >= rangeStart && candidate <= rangeEnd && !exSet.has(ds)) {
|
|
562
|
+
results.push(ds);
|
|
563
|
+
}
|
|
564
|
+
count++;
|
|
565
|
+
}
|
|
566
|
+
cursor.setDate(cursor.getDate() + 7 * interval);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function generateMonthly(start, interval, rule, rangeStart, rangeEnd, until, maxCount, exSet, results) {
|
|
570
|
+
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
|
571
|
+
let count = 0;
|
|
572
|
+
if (cursor < rangeStart && maxCount === MAX_OCCURRENCES) {
|
|
573
|
+
const monthsDiff = (rangeStart.getFullYear() - cursor.getFullYear()) * 12 + (rangeStart.getMonth() - cursor.getMonth());
|
|
574
|
+
const monthsToSkip = Math.floor(monthsDiff / interval);
|
|
575
|
+
if (monthsToSkip > 1) {
|
|
576
|
+
cursor.setMonth(cursor.getMonth() + (monthsToSkip - 1) * interval);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
while (count < maxCount) {
|
|
580
|
+
if (until && cursor > until) break;
|
|
581
|
+
if (cursor.getTime() > rangeEnd.getTime() + 31 * 24 * 60 * 60 * 1e3) break;
|
|
582
|
+
const candidates = [];
|
|
583
|
+
if (rule.byDay && rule.bySetPos !== void 0) {
|
|
584
|
+
for (const dayStr of rule.byDay) {
|
|
585
|
+
const dayNum = DAY_MAP[dayStr];
|
|
586
|
+
const found = getNthWeekdayOfMonth(cursor.getFullYear(), cursor.getMonth(), dayNum, rule.bySetPos);
|
|
587
|
+
if (found) candidates.push(found);
|
|
588
|
+
}
|
|
589
|
+
} else if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
590
|
+
for (const md of rule.byMonthDay) {
|
|
591
|
+
const daysInMonth = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0).getDate();
|
|
592
|
+
if (md <= daysInMonth) {
|
|
593
|
+
candidates.push(new Date(cursor.getFullYear(), cursor.getMonth(), md));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
const daysInMonth = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0).getDate();
|
|
598
|
+
const dayOfMonth = Math.min(start.getDate(), daysInMonth);
|
|
599
|
+
candidates.push(new Date(cursor.getFullYear(), cursor.getMonth(), dayOfMonth));
|
|
600
|
+
}
|
|
601
|
+
for (const candidate of candidates) {
|
|
602
|
+
if (candidate < start) continue;
|
|
603
|
+
if (until && candidate > until) break;
|
|
604
|
+
if (count >= maxCount) break;
|
|
605
|
+
const ds = toDateString(candidate);
|
|
606
|
+
if (candidate >= rangeStart && candidate <= rangeEnd && !exSet.has(ds)) {
|
|
607
|
+
results.push(ds);
|
|
608
|
+
}
|
|
609
|
+
count++;
|
|
610
|
+
}
|
|
611
|
+
cursor.setMonth(cursor.getMonth() + interval);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function generateYearly(start, interval, rangeStart, rangeEnd, until, maxCount, exSet, results) {
|
|
615
|
+
const cursor = new Date(start);
|
|
616
|
+
let count = 0;
|
|
617
|
+
while (count < maxCount) {
|
|
618
|
+
if (until && cursor > until) break;
|
|
619
|
+
if (cursor > rangeEnd) break;
|
|
620
|
+
const ds = toDateString(cursor);
|
|
621
|
+
if (cursor >= rangeStart && !exSet.has(ds)) {
|
|
622
|
+
results.push(ds);
|
|
623
|
+
}
|
|
624
|
+
count++;
|
|
625
|
+
cursor.setFullYear(cursor.getFullYear() + interval);
|
|
626
|
+
if (start.getMonth() === 1 && start.getDate() === 29) {
|
|
627
|
+
const daysInFeb = new Date(cursor.getFullYear(), 2, 0).getDate();
|
|
628
|
+
cursor.setMonth(1, Math.min(29, daysInFeb));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function getNthWeekdayOfMonth(year, month, dayOfWeek, n) {
|
|
633
|
+
if (n > 0) {
|
|
634
|
+
const first = new Date(year, month, 1);
|
|
635
|
+
const firstDayOfWeek = first.getDay();
|
|
636
|
+
let dayOfMonth = 1 + (dayOfWeek - firstDayOfWeek + 7) % 7 + (n - 1) * 7;
|
|
637
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
638
|
+
if (dayOfMonth > daysInMonth) return null;
|
|
639
|
+
return new Date(year, month, dayOfMonth);
|
|
640
|
+
} else if (n === -1) {
|
|
641
|
+
const last = new Date(year, month + 1, 0);
|
|
642
|
+
const lastDayOfWeek = last.getDay();
|
|
643
|
+
const diff = (lastDayOfWeek - dayOfWeek + 7) % 7;
|
|
644
|
+
return new Date(year, month, last.getDate() - diff);
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function expandRecurringEvents(events, rangeStart, rangeEnd) {
|
|
649
|
+
const result = [];
|
|
650
|
+
for (const event of events) {
|
|
651
|
+
if (event.recurringEventId) {
|
|
652
|
+
result.push(event);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!event.recurrence) {
|
|
656
|
+
result.push(event);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const eventStartDate = event.start.slice(0, 10);
|
|
660
|
+
const eventStartTime = event.start.length > 10 ? event.start.slice(10) : "T00:00:00";
|
|
661
|
+
const eventEndTime = event.end.length > 10 ? event.end.slice(10) : "T23:59:00";
|
|
662
|
+
const startD = parseDate(event.start);
|
|
663
|
+
const endD = parseDate(event.end);
|
|
664
|
+
const durationDays = Math.floor((endD.getTime() - startD.getTime()) / (1e3 * 60 * 60 * 24));
|
|
665
|
+
const occurrences = generateOccurrences(
|
|
666
|
+
event.recurrence,
|
|
667
|
+
eventStartDate,
|
|
668
|
+
rangeStart,
|
|
669
|
+
rangeEnd,
|
|
670
|
+
event.exDates
|
|
671
|
+
);
|
|
672
|
+
for (const occ of occurrences) {
|
|
673
|
+
const instanceId = `${event.id}::${occ}`;
|
|
674
|
+
let instanceEnd = `${occ}${eventEndTime}`;
|
|
675
|
+
if (durationDays > 0) {
|
|
676
|
+
const occDate = parseDate(occ);
|
|
677
|
+
occDate.setDate(occDate.getDate() + durationDays);
|
|
678
|
+
instanceEnd = `${toDateString(occDate)}${eventEndTime}`;
|
|
679
|
+
}
|
|
680
|
+
result.push({
|
|
681
|
+
...event,
|
|
682
|
+
id: instanceId,
|
|
683
|
+
start: `${occ}${eventStartTime}`,
|
|
684
|
+
end: instanceEnd,
|
|
685
|
+
recurringEventId: event.id,
|
|
686
|
+
originalDate: occ,
|
|
687
|
+
// Don't carry over recurrence to instances
|
|
688
|
+
recurrence: void 0,
|
|
689
|
+
exDates: void 0
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
var FREQ_MAP = {
|
|
696
|
+
DAILY: "daily",
|
|
697
|
+
WEEKLY: "weekly",
|
|
698
|
+
MONTHLY: "monthly",
|
|
699
|
+
YEARLY: "yearly"
|
|
700
|
+
};
|
|
701
|
+
var FREQ_REVERSE = {
|
|
702
|
+
daily: "DAILY",
|
|
703
|
+
weekly: "WEEKLY",
|
|
704
|
+
monthly: "MONTHLY",
|
|
705
|
+
yearly: "YEARLY"
|
|
706
|
+
};
|
|
707
|
+
function parseRRule(rruleString) {
|
|
708
|
+
const str = rruleString.replace(/^RRULE:/, "");
|
|
709
|
+
const parts = str.split(";");
|
|
710
|
+
const rule = { freq: "daily" };
|
|
711
|
+
for (const part of parts) {
|
|
712
|
+
const [key, value] = part.split("=");
|
|
713
|
+
switch (key) {
|
|
714
|
+
case "FREQ":
|
|
715
|
+
rule.freq = FREQ_MAP[value] ?? "daily";
|
|
716
|
+
break;
|
|
717
|
+
case "INTERVAL":
|
|
718
|
+
rule.interval = parseInt(value, 10);
|
|
719
|
+
break;
|
|
720
|
+
case "COUNT":
|
|
721
|
+
rule.count = parseInt(value, 10);
|
|
722
|
+
break;
|
|
723
|
+
case "UNTIL": {
|
|
724
|
+
const dateStr = value.replace(/[TZ]/g, "").slice(0, 8);
|
|
725
|
+
rule.until = `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
case "BYDAY":
|
|
729
|
+
rule.byDay = value.split(",").map((d) => {
|
|
730
|
+
return d.replace(/^-?\d+/, "");
|
|
731
|
+
});
|
|
732
|
+
break;
|
|
733
|
+
case "BYMONTHDAY":
|
|
734
|
+
rule.byMonthDay = value.split(",").map(Number);
|
|
735
|
+
break;
|
|
736
|
+
case "BYSETPOS":
|
|
737
|
+
rule.bySetPos = parseInt(value, 10);
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return rule;
|
|
742
|
+
}
|
|
743
|
+
function toRRuleString(rule) {
|
|
744
|
+
const parts = [];
|
|
745
|
+
parts.push(`FREQ=${FREQ_REVERSE[rule.freq]}`);
|
|
746
|
+
if (rule.interval && rule.interval > 1) {
|
|
747
|
+
parts.push(`INTERVAL=${rule.interval}`);
|
|
748
|
+
}
|
|
749
|
+
if (rule.count) {
|
|
750
|
+
parts.push(`COUNT=${rule.count}`);
|
|
751
|
+
}
|
|
752
|
+
if (rule.until) {
|
|
753
|
+
parts.push(`UNTIL=${rule.until.replace(/-/g, "")}`);
|
|
754
|
+
}
|
|
755
|
+
if (rule.byDay && rule.byDay.length > 0) {
|
|
756
|
+
parts.push(`BYDAY=${rule.byDay.join(",")}`);
|
|
757
|
+
}
|
|
758
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
759
|
+
parts.push(`BYMONTHDAY=${rule.byMonthDay.join(",")}`);
|
|
760
|
+
}
|
|
761
|
+
if (rule.bySetPos !== void 0) {
|
|
762
|
+
parts.push(`BYSETPOS=${rule.bySetPos}`);
|
|
763
|
+
}
|
|
764
|
+
return parts.join(";");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/utils/virtualize.ts
|
|
768
|
+
function filterVisibleEvents(positioned, viewportTop, viewportBottom, overscan = 10) {
|
|
769
|
+
const top = viewportTop - overscan;
|
|
770
|
+
const bottom = viewportBottom + overscan;
|
|
771
|
+
return positioned.filter(
|
|
772
|
+
(p) => p.top + p.height > top && p.top < bottom
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
function scrollToViewportRange(scrollTop, containerHeight, totalHeight, dayStartHour, dayEndHour) {
|
|
776
|
+
if (totalHeight <= 0) {
|
|
777
|
+
return { startHour: dayStartHour, endHour: dayEndHour };
|
|
778
|
+
}
|
|
779
|
+
const totalHours = dayEndHour - dayStartHour;
|
|
780
|
+
const topFraction = scrollTop / totalHeight;
|
|
781
|
+
const bottomFraction = (scrollTop + containerHeight) / totalHeight;
|
|
782
|
+
return {
|
|
783
|
+
startHour: dayStartHour + topFraction * totalHours,
|
|
784
|
+
endHour: dayStartHour + bottomFraction * totalHours
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/utils/undo.ts
|
|
789
|
+
var DEFAULT_MAX_HISTORY = 30;
|
|
790
|
+
function createUndoStack(initial) {
|
|
791
|
+
return {
|
|
792
|
+
past: [],
|
|
793
|
+
present: initial,
|
|
794
|
+
future: []
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function pushState(stack, state, maxHistory = DEFAULT_MAX_HISTORY) {
|
|
798
|
+
const past = [...stack.past, stack.present];
|
|
799
|
+
return {
|
|
800
|
+
past: past.length > maxHistory ? past.slice(past.length - maxHistory) : past,
|
|
801
|
+
present: state,
|
|
802
|
+
future: []
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function undo(stack) {
|
|
806
|
+
if (stack.past.length === 0) {
|
|
807
|
+
return stack;
|
|
808
|
+
}
|
|
809
|
+
const previous = stack.past[stack.past.length - 1];
|
|
810
|
+
const newPast = stack.past.slice(0, -1);
|
|
811
|
+
return {
|
|
812
|
+
past: newPast,
|
|
813
|
+
present: previous,
|
|
814
|
+
future: [stack.present, ...stack.future]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function redo(stack) {
|
|
818
|
+
if (stack.future.length === 0) {
|
|
819
|
+
return stack;
|
|
820
|
+
}
|
|
821
|
+
const next = stack.future[0];
|
|
822
|
+
const newFuture = stack.future.slice(1);
|
|
823
|
+
return {
|
|
824
|
+
past: [...stack.past, stack.present],
|
|
825
|
+
present: next,
|
|
826
|
+
future: newFuture
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function canUndo(stack) {
|
|
830
|
+
return stack.past.length > 0;
|
|
831
|
+
}
|
|
832
|
+
function canRedo(stack) {
|
|
833
|
+
return stack.future.length > 0;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export { DEFAULT_DAY_END_HOUR, DEFAULT_DAY_START_HOUR, DEFAULT_LABELS, DEFAULT_LOCALE, DEFAULT_VIEW, HOURS_IN_DAY, MINUTES_IN_DAY, MINUTES_IN_HOUR, VIEWS, addDays, addMonths, addWeeks, assignColumns, buildOverlapGroups, calendarReducer, canRedo, canUndo, computeDropPosition, computeTimePositions, createInitialState, createUndoStack, dateInRange, daysBetween, eachDayOfRange, endOfMonth, expandRecurringEvents, filterEventsInRange, filterVisibleEvents, formatAgendaDate, formatDayNumber, formatMonthDay, formatTime, formatTimeRange, formatToolbarTitle, formatWeekdayNarrow, formatWeekdayShort, fractionalHourToDateTime, generateOccurrences, getDurationHours, getEventSegments, getEventsForDay, getHourLabels, getMonthViewRange, getTimeOfDay, getVisibleRange, getWeekDays, getWeekViewRange, groupEventsByDate, isAfter, isBefore, isMultiDayEvent, isSameDay, isSameMonth, isToday, normalizeRange, parseDate, parseRRule, partitionEvents, pushState, rangesOverlap, redo, scrollToViewportRange, segmentMultiDayEvent, segmentTimedMultiDayEvent, snapToIncrement, sortEvents, startOfMonth, startOfWeek, toDateString, toDateTimeString, toRRuleString, undo, yPositionToFractionalHour };
|
|
837
|
+
//# sourceMappingURL=index.js.map
|
|
838
|
+
//# sourceMappingURL=index.js.map
|