trud-calendar-core 0.1.4 → 1.0.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/index.cjs +509 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +323 -9
- package/dist/index.d.ts +323 -9
- package/dist/index.js +491 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,8 @@ var DEFAULT_LABELS = {
|
|
|
7
7
|
week: "Week",
|
|
8
8
|
day: "Day",
|
|
9
9
|
agenda: "Agenda",
|
|
10
|
+
year: "Year",
|
|
11
|
+
timeline: "Timeline",
|
|
10
12
|
allDay: "all-day",
|
|
11
13
|
noEvents: "No events in this period",
|
|
12
14
|
more: (n) => `+${n} more`
|
|
@@ -19,9 +21,10 @@ var DEFAULT_VIEW = "month";
|
|
|
19
21
|
var HOURS_IN_DAY = 24;
|
|
20
22
|
var MINUTES_IN_HOUR = 60;
|
|
21
23
|
var MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
|
|
22
|
-
var VIEWS = ["month", "week", "day", "agenda"];
|
|
24
|
+
var VIEWS = ["month", "week", "day", "agenda", "year", "timeline"];
|
|
23
25
|
var DEFAULT_DAY_START_HOUR = 0;
|
|
24
26
|
var DEFAULT_DAY_END_HOUR = 24;
|
|
27
|
+
var DEFAULT_SNAP_DURATION = 15;
|
|
25
28
|
|
|
26
29
|
// src/utils/date.ts
|
|
27
30
|
function parseDate(iso) {
|
|
@@ -134,8 +137,29 @@ function getVisibleRange(date, view, weekStartsOn = 0) {
|
|
|
134
137
|
return { start: date, end: date };
|
|
135
138
|
case "agenda":
|
|
136
139
|
return { start: date, end: addDays(date, 30) };
|
|
140
|
+
case "year": {
|
|
141
|
+
const d = parseDate(date);
|
|
142
|
+
const yearStart = `${d.getFullYear()}-01-01`;
|
|
143
|
+
const yearEnd = `${d.getFullYear()}-12-31`;
|
|
144
|
+
return { start: yearStart, end: yearEnd };
|
|
145
|
+
}
|
|
146
|
+
case "timeline":
|
|
147
|
+
return { start: date, end: date };
|
|
137
148
|
}
|
|
138
149
|
}
|
|
150
|
+
function getISOWeekNumber(date) {
|
|
151
|
+
const d = parseDate(date);
|
|
152
|
+
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
153
|
+
const dayNum = target.getDay() || 7;
|
|
154
|
+
target.setDate(target.getDate() + 4 - dayNum);
|
|
155
|
+
const yearStart = new Date(target.getFullYear(), 0, 1);
|
|
156
|
+
return Math.ceil(((target.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
157
|
+
}
|
|
158
|
+
function filterHiddenDays(days, hiddenDays) {
|
|
159
|
+
if (hiddenDays.length === 0) return days;
|
|
160
|
+
const hiddenSet = new Set(hiddenDays);
|
|
161
|
+
return days.filter((d) => !hiddenSet.has(parseDate(d).getDay()));
|
|
162
|
+
}
|
|
139
163
|
function getTimeOfDay(datetime) {
|
|
140
164
|
const d = parseDate(datetime);
|
|
141
165
|
return d.getHours() + d.getMinutes() / 60;
|
|
@@ -192,6 +216,15 @@ function formatToolbarTitle(date, view, locale = "en-US") {
|
|
|
192
216
|
}).format(d);
|
|
193
217
|
case "agenda":
|
|
194
218
|
return getFormatter(locale, { month: "long", year: "numeric" }).format(d);
|
|
219
|
+
case "year":
|
|
220
|
+
return getFormatter(locale, { year: "numeric" }).format(d);
|
|
221
|
+
case "timeline":
|
|
222
|
+
return getFormatter(locale, {
|
|
223
|
+
weekday: "long",
|
|
224
|
+
month: "long",
|
|
225
|
+
day: "numeric",
|
|
226
|
+
year: "numeric"
|
|
227
|
+
}).format(d);
|
|
195
228
|
}
|
|
196
229
|
}
|
|
197
230
|
function formatWeekdayShort(date, locale = "en-US") {
|
|
@@ -244,6 +277,10 @@ function navigateByView(date, view, direction) {
|
|
|
244
277
|
return addDays(date, direction);
|
|
245
278
|
case "agenda":
|
|
246
279
|
return addMonths(date, direction);
|
|
280
|
+
case "year":
|
|
281
|
+
return addMonths(date, 12 * direction);
|
|
282
|
+
case "timeline":
|
|
283
|
+
return addDays(date, direction);
|
|
247
284
|
}
|
|
248
285
|
}
|
|
249
286
|
function calendarReducer(state, action) {
|
|
@@ -276,6 +313,261 @@ function calendarReducer(state, action) {
|
|
|
276
313
|
}
|
|
277
314
|
}
|
|
278
315
|
|
|
316
|
+
// src/utils/timezone.ts
|
|
317
|
+
var WALL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?$/;
|
|
318
|
+
var UTC_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/;
|
|
319
|
+
function pad2(n) {
|
|
320
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
321
|
+
}
|
|
322
|
+
function parseWall(wall) {
|
|
323
|
+
const m = WALL_RE.exec(wall);
|
|
324
|
+
if (!m) throw new RangeError(`Invalid wall-clock DateTimeString: ${wall}`);
|
|
325
|
+
return {
|
|
326
|
+
year: +m[1],
|
|
327
|
+
month: +m[2],
|
|
328
|
+
day: +m[3],
|
|
329
|
+
hour: +m[4],
|
|
330
|
+
minute: +m[5],
|
|
331
|
+
second: +m[6]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function formatWall(p) {
|
|
335
|
+
return `${p.year}-${pad2(p.month)}-${pad2(p.day)}T${pad2(p.hour)}:${pad2(p.minute)}:${pad2(p.second)}`;
|
|
336
|
+
}
|
|
337
|
+
function parseUtcMs(utc) {
|
|
338
|
+
if (UTC_RE.test(utc)) return Date.parse(utc);
|
|
339
|
+
if (WALL_RE.test(utc)) return Date.parse(`${utc}Z`);
|
|
340
|
+
throw new RangeError(`Invalid datetime string: ${utc}`);
|
|
341
|
+
}
|
|
342
|
+
function utcMsToIso(ms) {
|
|
343
|
+
return new Date(ms).toISOString().replace(/\.\d+Z$/, "Z");
|
|
344
|
+
}
|
|
345
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
346
|
+
function dtfFor(timeZone) {
|
|
347
|
+
let dtf = dtfCache.get(timeZone);
|
|
348
|
+
if (!dtf) {
|
|
349
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
350
|
+
timeZone,
|
|
351
|
+
year: "numeric",
|
|
352
|
+
month: "2-digit",
|
|
353
|
+
day: "2-digit",
|
|
354
|
+
hour: "2-digit",
|
|
355
|
+
minute: "2-digit",
|
|
356
|
+
second: "2-digit",
|
|
357
|
+
hour12: false,
|
|
358
|
+
hourCycle: "h23"
|
|
359
|
+
});
|
|
360
|
+
dtfCache.set(timeZone, dtf);
|
|
361
|
+
}
|
|
362
|
+
return dtf;
|
|
363
|
+
}
|
|
364
|
+
function partsAt(timeZone, utcMs) {
|
|
365
|
+
const parts = dtfFor(timeZone).formatToParts(new Date(utcMs));
|
|
366
|
+
const result = {};
|
|
367
|
+
for (const part of parts) {
|
|
368
|
+
switch (part.type) {
|
|
369
|
+
case "year":
|
|
370
|
+
result.year = +part.value;
|
|
371
|
+
break;
|
|
372
|
+
case "month":
|
|
373
|
+
result.month = +part.value;
|
|
374
|
+
break;
|
|
375
|
+
case "day":
|
|
376
|
+
result.day = +part.value;
|
|
377
|
+
break;
|
|
378
|
+
case "hour":
|
|
379
|
+
result.hour = +part.value % 24;
|
|
380
|
+
break;
|
|
381
|
+
case "minute":
|
|
382
|
+
result.minute = +part.value;
|
|
383
|
+
break;
|
|
384
|
+
case "second":
|
|
385
|
+
result.second = +part.value;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
function getBrowserTimeZone() {
|
|
392
|
+
try {
|
|
393
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
394
|
+
if (typeof tz === "string" && tz.length > 0) return tz;
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
return "UTC";
|
|
398
|
+
}
|
|
399
|
+
function eventWallToDisplay(eventWall, eventTimeZone, displayTimeZone) {
|
|
400
|
+
if (!eventTimeZone) return eventWall;
|
|
401
|
+
const target = displayTimeZone ?? getBrowserTimeZone();
|
|
402
|
+
if (eventTimeZone === target) return eventWall;
|
|
403
|
+
return convertWallTime(eventWall, eventTimeZone, target);
|
|
404
|
+
}
|
|
405
|
+
function displayWallToEvent(displayWall, eventTimeZone, displayTimeZone, options) {
|
|
406
|
+
if (!eventTimeZone) return displayWall;
|
|
407
|
+
const source = displayTimeZone ?? getBrowserTimeZone();
|
|
408
|
+
if (eventTimeZone === source) return displayWall;
|
|
409
|
+
return convertWallTime(displayWall, source, eventTimeZone, options);
|
|
410
|
+
}
|
|
411
|
+
var validTzCache = /* @__PURE__ */ new Map();
|
|
412
|
+
function isValidTimeZone(timeZone) {
|
|
413
|
+
if (typeof timeZone !== "string" || timeZone.length === 0) return false;
|
|
414
|
+
const cached = validTzCache.get(timeZone);
|
|
415
|
+
if (cached !== void 0) return cached;
|
|
416
|
+
let ok;
|
|
417
|
+
try {
|
|
418
|
+
Intl.DateTimeFormat("en-US", { timeZone });
|
|
419
|
+
ok = true;
|
|
420
|
+
} catch {
|
|
421
|
+
ok = false;
|
|
422
|
+
}
|
|
423
|
+
validTzCache.set(timeZone, ok);
|
|
424
|
+
return ok;
|
|
425
|
+
}
|
|
426
|
+
var FALLBACK_TIMEZONES = Object.freeze([
|
|
427
|
+
"UTC",
|
|
428
|
+
"Africa/Cairo",
|
|
429
|
+
"Africa/Johannesburg",
|
|
430
|
+
"Africa/Lagos",
|
|
431
|
+
"Africa/Nairobi",
|
|
432
|
+
"America/Anchorage",
|
|
433
|
+
"America/Argentina/Buenos_Aires",
|
|
434
|
+
"America/Bogota",
|
|
435
|
+
"America/Caracas",
|
|
436
|
+
"America/Chicago",
|
|
437
|
+
"America/Denver",
|
|
438
|
+
"America/Halifax",
|
|
439
|
+
"America/Lima",
|
|
440
|
+
"America/Los_Angeles",
|
|
441
|
+
"America/Mexico_City",
|
|
442
|
+
"America/Montevideo",
|
|
443
|
+
"America/New_York",
|
|
444
|
+
"America/Phoenix",
|
|
445
|
+
"America/Santiago",
|
|
446
|
+
"America/Sao_Paulo",
|
|
447
|
+
"America/St_Johns",
|
|
448
|
+
"America/Toronto",
|
|
449
|
+
"America/Vancouver",
|
|
450
|
+
"Asia/Bangkok",
|
|
451
|
+
"Asia/Dubai",
|
|
452
|
+
"Asia/Hong_Kong",
|
|
453
|
+
"Asia/Jakarta",
|
|
454
|
+
"Asia/Jerusalem",
|
|
455
|
+
"Asia/Karachi",
|
|
456
|
+
"Asia/Kathmandu",
|
|
457
|
+
"Asia/Kolkata",
|
|
458
|
+
"Asia/Kuala_Lumpur",
|
|
459
|
+
"Asia/Manila",
|
|
460
|
+
"Asia/Riyadh",
|
|
461
|
+
"Asia/Seoul",
|
|
462
|
+
"Asia/Shanghai",
|
|
463
|
+
"Asia/Singapore",
|
|
464
|
+
"Asia/Taipei",
|
|
465
|
+
"Asia/Tehran",
|
|
466
|
+
"Asia/Tokyo",
|
|
467
|
+
"Atlantic/Azores",
|
|
468
|
+
"Atlantic/Cape_Verde",
|
|
469
|
+
"Atlantic/Reykjavik",
|
|
470
|
+
"Australia/Adelaide",
|
|
471
|
+
"Australia/Brisbane",
|
|
472
|
+
"Australia/Darwin",
|
|
473
|
+
"Australia/Melbourne",
|
|
474
|
+
"Australia/Perth",
|
|
475
|
+
"Australia/Sydney",
|
|
476
|
+
"Europe/Amsterdam",
|
|
477
|
+
"Europe/Athens",
|
|
478
|
+
"Europe/Berlin",
|
|
479
|
+
"Europe/Brussels",
|
|
480
|
+
"Europe/Bucharest",
|
|
481
|
+
"Europe/Budapest",
|
|
482
|
+
"Europe/Copenhagen",
|
|
483
|
+
"Europe/Dublin",
|
|
484
|
+
"Europe/Helsinki",
|
|
485
|
+
"Europe/Istanbul",
|
|
486
|
+
"Europe/Lisbon",
|
|
487
|
+
"Europe/London",
|
|
488
|
+
"Europe/Madrid",
|
|
489
|
+
"Europe/Moscow",
|
|
490
|
+
"Europe/Oslo",
|
|
491
|
+
"Europe/Paris",
|
|
492
|
+
"Europe/Prague",
|
|
493
|
+
"Europe/Rome",
|
|
494
|
+
"Europe/Stockholm",
|
|
495
|
+
"Europe/Vienna",
|
|
496
|
+
"Europe/Warsaw",
|
|
497
|
+
"Europe/Zurich",
|
|
498
|
+
"Pacific/Auckland",
|
|
499
|
+
"Pacific/Fiji",
|
|
500
|
+
"Pacific/Guam",
|
|
501
|
+
"Pacific/Honolulu",
|
|
502
|
+
"Pacific/Midway",
|
|
503
|
+
"Pacific/Pago_Pago",
|
|
504
|
+
"Pacific/Tongatapu"
|
|
505
|
+
]);
|
|
506
|
+
function listTimeZones() {
|
|
507
|
+
const fn = Intl.supportedValuesOf;
|
|
508
|
+
if (typeof fn === "function") {
|
|
509
|
+
try {
|
|
510
|
+
const values = fn("timeZone");
|
|
511
|
+
return values.includes("UTC") ? values.slice() : ["UTC", ...values];
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return FALLBACK_TIMEZONES.slice();
|
|
516
|
+
}
|
|
517
|
+
function getTimeZoneOffset(utcInstant, timeZone) {
|
|
518
|
+
const utcMs = parseUtcMs(utcInstant);
|
|
519
|
+
const wall = partsAt(timeZone, utcMs);
|
|
520
|
+
const wallAsUtcMs = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);
|
|
521
|
+
return Math.round((wallAsUtcMs - utcMs) / 6e4);
|
|
522
|
+
}
|
|
523
|
+
function wallTimeToUtc(wallTime, timeZone, options = {}) {
|
|
524
|
+
const ambiguous = options.ambiguous ?? "earlier";
|
|
525
|
+
const invalid = options.invalid ?? "shift";
|
|
526
|
+
const w = parseWall(wallTime);
|
|
527
|
+
const guessUtcMs = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second);
|
|
528
|
+
const offsetBefore = getTimeZoneOffset(utcMsToIso(guessUtcMs - 12 * 36e5), timeZone);
|
|
529
|
+
const offsetAfter = getTimeZoneOffset(utcMsToIso(guessUtcMs + 12 * 36e5), timeZone);
|
|
530
|
+
const candidateBefore = guessUtcMs - offsetBefore * 6e4;
|
|
531
|
+
const candidateAfter = guessUtcMs - offsetAfter * 6e4;
|
|
532
|
+
const roundTripBefore = roundTripWall(candidateBefore, timeZone);
|
|
533
|
+
const roundTripAfter = roundTripWall(candidateAfter, timeZone);
|
|
534
|
+
const beforeMatches = wallEquals(roundTripBefore, w);
|
|
535
|
+
const afterMatches = wallEquals(roundTripAfter, w);
|
|
536
|
+
if (beforeMatches && afterMatches) {
|
|
537
|
+
if (candidateBefore === candidateAfter) {
|
|
538
|
+
return utcMsToIso(candidateBefore);
|
|
539
|
+
}
|
|
540
|
+
return utcMsToIso(ambiguous === "earlier" ? Math.min(candidateBefore, candidateAfter) : Math.max(candidateBefore, candidateAfter));
|
|
541
|
+
}
|
|
542
|
+
if (beforeMatches) return utcMsToIso(candidateBefore);
|
|
543
|
+
if (afterMatches) return utcMsToIso(candidateAfter);
|
|
544
|
+
if (invalid === "throw") {
|
|
545
|
+
throw new RangeError(`Wall time ${wallTime} does not exist in ${timeZone} (DST gap)`);
|
|
546
|
+
}
|
|
547
|
+
return utcMsToIso(Math.max(candidateBefore, candidateAfter));
|
|
548
|
+
}
|
|
549
|
+
function roundTripWall(utcMs, timeZone) {
|
|
550
|
+
return partsAt(timeZone, utcMs);
|
|
551
|
+
}
|
|
552
|
+
function wallEquals(a, b) {
|
|
553
|
+
return a.year === b.year && a.month === b.month && a.day === b.day && a.hour === b.hour && a.minute === b.minute && a.second === b.second;
|
|
554
|
+
}
|
|
555
|
+
function utcToWallTime(utcInstant, timeZone) {
|
|
556
|
+
return formatWall(partsAt(timeZone, parseUtcMs(utcInstant)));
|
|
557
|
+
}
|
|
558
|
+
function convertWallTime(wallTime, fromTimeZone, toTimeZone, options) {
|
|
559
|
+
if (fromTimeZone === toTimeZone) return wallTime;
|
|
560
|
+
const utc = wallTimeToUtc(wallTime, fromTimeZone, options);
|
|
561
|
+
return utcToWallTime(utc, toTimeZone);
|
|
562
|
+
}
|
|
563
|
+
function getTimeZoneAbbreviation(timeZone, atInstant) {
|
|
564
|
+
const utcMs = atInstant === void 0 ? Date.now() : parseUtcMs(atInstant);
|
|
565
|
+
const dtf = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
|
|
566
|
+
const parts = dtf.formatToParts(new Date(utcMs));
|
|
567
|
+
const tzPart = parts.find((p) => p.type === "timeZoneName");
|
|
568
|
+
return tzPart?.value ?? timeZone;
|
|
569
|
+
}
|
|
570
|
+
|
|
279
571
|
// src/utils/events.ts
|
|
280
572
|
function sortEvents(events) {
|
|
281
573
|
return [...events].sort((a, b) => {
|
|
@@ -300,14 +592,17 @@ function isMultiDayEvent(event) {
|
|
|
300
592
|
function partitionEvents(events) {
|
|
301
593
|
const allDay = [];
|
|
302
594
|
const timed = [];
|
|
595
|
+
const background = [];
|
|
303
596
|
for (const event of events) {
|
|
304
|
-
if (event.
|
|
597
|
+
if (event.display === "background") {
|
|
598
|
+
background.push(event);
|
|
599
|
+
} else if (event.allDay || isMultiDayEvent(event)) {
|
|
305
600
|
allDay.push(event);
|
|
306
601
|
} else {
|
|
307
602
|
timed.push(event);
|
|
308
603
|
}
|
|
309
604
|
}
|
|
310
|
-
return { allDay, timed };
|
|
605
|
+
return { allDay, timed, background };
|
|
311
606
|
}
|
|
312
607
|
function segmentMultiDayEvent(event, rangeStart, rangeEnd) {
|
|
313
608
|
const eventStartDate = event.start.slice(0, 10);
|
|
@@ -403,15 +698,17 @@ function assignColumns(group) {
|
|
|
403
698
|
const totalColumns = columns.length;
|
|
404
699
|
return result.map((r) => ({ ...r, totalColumns }));
|
|
405
700
|
}
|
|
406
|
-
function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24) {
|
|
701
|
+
function computeTimePositions(events, dayStartHour = 0, dayEndHour = 24, displayTimeZone) {
|
|
407
702
|
const totalHours = dayEndHour - dayStartHour;
|
|
408
703
|
const groups = buildOverlapGroups(events);
|
|
409
704
|
const positioned = [];
|
|
410
705
|
for (const group of groups) {
|
|
411
706
|
const columns = assignColumns(group);
|
|
412
707
|
for (const { event, column, totalColumns } of columns) {
|
|
413
|
-
const
|
|
414
|
-
const
|
|
708
|
+
const eventStart = displayTimeZone && event.timeZone ? eventWallToDisplay(event.start, event.timeZone, displayTimeZone) : event.start;
|
|
709
|
+
const eventEnd = displayTimeZone && event.timeZone ? eventWallToDisplay(event.end, event.timeZone, displayTimeZone) : event.end;
|
|
710
|
+
const startTime = Math.max(getTimeOfDay(eventStart), dayStartHour);
|
|
711
|
+
const endTime = Math.min(getTimeOfDay(eventEnd), dayEndHour);
|
|
415
712
|
const top = (startTime - dayStartHour) / totalHours * 100;
|
|
416
713
|
const height = Math.max((endTime - startTime) / totalHours * 100, 1);
|
|
417
714
|
positioned.push({
|
|
@@ -465,9 +762,9 @@ function yPositionToFractionalHour(clientY, columnRect, dayStart, dayEnd) {
|
|
|
465
762
|
function normalizeRange(a, b) {
|
|
466
763
|
return a <= b ? { start: a, end: b } : { start: b, end: a };
|
|
467
764
|
}
|
|
468
|
-
function computeDropPosition(day, clientY, columnRect, dayStartHour, dayEndHour, durationMs) {
|
|
765
|
+
function computeDropPosition(day, clientY, columnRect, dayStartHour, dayEndHour, durationMs, snapMinutes = 15) {
|
|
469
766
|
const fractionalHour = yPositionToFractionalHour(clientY, columnRect, dayStartHour, dayEndHour);
|
|
470
|
-
const snapped = snapToIncrement(fractionalHour,
|
|
767
|
+
const snapped = snapToIncrement(fractionalHour, snapMinutes);
|
|
471
768
|
const clamped = Math.max(dayStartHour, Math.min(dayEndHour - durationMs / 36e5, snapped));
|
|
472
769
|
const newStart = fractionalHourToDateTime(day, clamped);
|
|
473
770
|
const newStartMs = new Date(newStart).getTime();
|
|
@@ -787,6 +1084,191 @@ function scrollToViewportRange(scrollTop, containerHeight, totalHeight, dayStart
|
|
|
787
1084
|
};
|
|
788
1085
|
}
|
|
789
1086
|
|
|
1087
|
+
// src/utils/ical.ts
|
|
1088
|
+
function toICalDate(dateTime) {
|
|
1089
|
+
return dateTime.replace(/[-:]/g, "").replace("T", "T");
|
|
1090
|
+
}
|
|
1091
|
+
function escapeICalText(text) {
|
|
1092
|
+
return text.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n");
|
|
1093
|
+
}
|
|
1094
|
+
function eventsToICal(events, calendarName = "trud-calendar") {
|
|
1095
|
+
const lines = [
|
|
1096
|
+
"BEGIN:VCALENDAR",
|
|
1097
|
+
"VERSION:2.0",
|
|
1098
|
+
`PRODID:-//${calendarName}//EN`,
|
|
1099
|
+
"CALSCALE:GREGORIAN",
|
|
1100
|
+
"METHOD:PUBLISH"
|
|
1101
|
+
];
|
|
1102
|
+
for (const event of events) {
|
|
1103
|
+
lines.push("BEGIN:VEVENT");
|
|
1104
|
+
lines.push(`UID:${event.id}`);
|
|
1105
|
+
if (event.allDay) {
|
|
1106
|
+
lines.push(`DTSTART;VALUE=DATE:${event.start.slice(0, 10).replace(/-/g, "")}`);
|
|
1107
|
+
lines.push(`DTEND;VALUE=DATE:${event.end.slice(0, 10).replace(/-/g, "")}`);
|
|
1108
|
+
} else {
|
|
1109
|
+
lines.push(`DTSTART:${toICalDate(event.start)}`);
|
|
1110
|
+
lines.push(`DTEND:${toICalDate(event.end)}`);
|
|
1111
|
+
}
|
|
1112
|
+
lines.push(`SUMMARY:${escapeICalText(event.title)}`);
|
|
1113
|
+
if (event.recurrence) {
|
|
1114
|
+
const parts = [`FREQ=${event.recurrence.freq.toUpperCase()}`];
|
|
1115
|
+
if (event.recurrence.interval && event.recurrence.interval > 1) {
|
|
1116
|
+
parts.push(`INTERVAL=${event.recurrence.interval}`);
|
|
1117
|
+
}
|
|
1118
|
+
if (event.recurrence.count) {
|
|
1119
|
+
parts.push(`COUNT=${event.recurrence.count}`);
|
|
1120
|
+
}
|
|
1121
|
+
if (event.recurrence.until) {
|
|
1122
|
+
parts.push(`UNTIL=${event.recurrence.until.replace(/-/g, "")}T235959`);
|
|
1123
|
+
}
|
|
1124
|
+
if (event.recurrence.byDay && event.recurrence.byDay.length > 0) {
|
|
1125
|
+
parts.push(`BYDAY=${event.recurrence.byDay.join(",")}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (event.recurrence.byMonthDay && event.recurrence.byMonthDay.length > 0) {
|
|
1128
|
+
parts.push(`BYMONTHDAY=${event.recurrence.byMonthDay.join(",")}`);
|
|
1129
|
+
}
|
|
1130
|
+
if (event.recurrence.bySetPos) {
|
|
1131
|
+
parts.push(`BYSETPOS=${event.recurrence.bySetPos}`);
|
|
1132
|
+
}
|
|
1133
|
+
lines.push(`RRULE:${parts.join(";")}`);
|
|
1134
|
+
}
|
|
1135
|
+
if (event.exDates && event.exDates.length > 0) {
|
|
1136
|
+
const exDateValues = event.exDates.map((d) => d.replace(/-/g, "")).join(",");
|
|
1137
|
+
lines.push(`EXDATE;VALUE=DATE:${exDateValues}`);
|
|
1138
|
+
}
|
|
1139
|
+
lines.push("END:VEVENT");
|
|
1140
|
+
}
|
|
1141
|
+
lines.push("END:VCALENDAR");
|
|
1142
|
+
return lines.join("\r\n");
|
|
1143
|
+
}
|
|
1144
|
+
function downloadICal(events, filename = "calendar.ics", calendarName) {
|
|
1145
|
+
const content = eventsToICal(events, calendarName);
|
|
1146
|
+
const blob = new Blob([content], { type: "text/calendar;charset=utf-8" });
|
|
1147
|
+
const url = URL.createObjectURL(blob);
|
|
1148
|
+
const a = document.createElement("a");
|
|
1149
|
+
a.href = url;
|
|
1150
|
+
a.download = filename;
|
|
1151
|
+
a.click();
|
|
1152
|
+
URL.revokeObjectURL(url);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/utils/resources.ts
|
|
1156
|
+
function flattenResources(resources) {
|
|
1157
|
+
const result = [];
|
|
1158
|
+
for (const resource of resources) {
|
|
1159
|
+
result.push(resource);
|
|
1160
|
+
if (resource.children && resource.children.length > 0) {
|
|
1161
|
+
result.push(...flattenResources(resource.children));
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
function getEventsForResource(events, resourceId) {
|
|
1167
|
+
return events.filter((e) => e.resourceId === resourceId);
|
|
1168
|
+
}
|
|
1169
|
+
function groupEventsByResource(events, resources) {
|
|
1170
|
+
const map = /* @__PURE__ */ new Map();
|
|
1171
|
+
for (const resource of resources) {
|
|
1172
|
+
map.set(resource.id, []);
|
|
1173
|
+
}
|
|
1174
|
+
for (const event of events) {
|
|
1175
|
+
if (event.resourceId && map.has(event.resourceId)) {
|
|
1176
|
+
map.get(event.resourceId).push(event);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return map;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/utils/timeline.ts
|
|
1183
|
+
var datePart = (s) => s.slice(0, 10);
|
|
1184
|
+
function intersectDay(event, day) {
|
|
1185
|
+
const evStartDay = datePart(event.start);
|
|
1186
|
+
const evEndDay = datePart(event.end);
|
|
1187
|
+
if (day < evStartDay || day > evEndDay) return null;
|
|
1188
|
+
const startsToday = evStartDay === day;
|
|
1189
|
+
const endsToday = evEndDay === day;
|
|
1190
|
+
const start = startsToday ? getTimeOfDay(event.start) : 0;
|
|
1191
|
+
const end = endsToday ? getTimeOfDay(event.end) : 24;
|
|
1192
|
+
if (end <= start) return null;
|
|
1193
|
+
return { start, end, isStart: startsToday, isEnd: endsToday };
|
|
1194
|
+
}
|
|
1195
|
+
function computeTimelinePositions(events, resourceIds, day, dayStartHour = 0, dayEndHour = 24) {
|
|
1196
|
+
const totalHours = Math.max(1e-4, dayEndHour - dayStartHour);
|
|
1197
|
+
const result = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const id of resourceIds) result.set(id, []);
|
|
1199
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const event of events) {
|
|
1201
|
+
if (event.allDay) continue;
|
|
1202
|
+
if (event.display === "background") continue;
|
|
1203
|
+
const rid = event.resourceId;
|
|
1204
|
+
if (!rid || !result.has(rid)) continue;
|
|
1205
|
+
let bucket = byResource.get(rid);
|
|
1206
|
+
if (!bucket) {
|
|
1207
|
+
bucket = [];
|
|
1208
|
+
byResource.set(rid, bucket);
|
|
1209
|
+
}
|
|
1210
|
+
bucket.push(event);
|
|
1211
|
+
}
|
|
1212
|
+
for (const [resourceId, bucketEvents] of byResource) {
|
|
1213
|
+
const clipped = [];
|
|
1214
|
+
for (const event of bucketEvents) {
|
|
1215
|
+
const window = intersectDay(event, day);
|
|
1216
|
+
if (!window) continue;
|
|
1217
|
+
const start = Math.max(window.start, dayStartHour);
|
|
1218
|
+
const end = Math.min(window.end, dayEndHour);
|
|
1219
|
+
if (end <= start) continue;
|
|
1220
|
+
clipped.push({
|
|
1221
|
+
original: event,
|
|
1222
|
+
clippedStart: start,
|
|
1223
|
+
clippedEnd: end,
|
|
1224
|
+
isSegmentStart: window.isStart && window.start >= dayStartHour,
|
|
1225
|
+
isSegmentEnd: window.isEnd && window.end <= dayEndHour
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (clipped.length === 0) continue;
|
|
1229
|
+
const fakeIso = (h) => {
|
|
1230
|
+
const hours = Math.floor(h);
|
|
1231
|
+
const minutes = Math.floor((h - hours) * 60);
|
|
1232
|
+
const seconds = Math.floor(((h - hours) * 60 - minutes) * 60);
|
|
1233
|
+
const pad = (n) => n < 10 ? `0${n}` : `${n}`;
|
|
1234
|
+
return `${day}T${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
1235
|
+
};
|
|
1236
|
+
const synthetic = clipped.map((c, idx) => ({
|
|
1237
|
+
...c.original,
|
|
1238
|
+
// Keep the original id but tag the index so two clipped segments
|
|
1239
|
+
// of the same parent never collide in a Map lookup.
|
|
1240
|
+
id: `${c.original.id}::tl::${idx}`,
|
|
1241
|
+
start: fakeIso(c.clippedStart),
|
|
1242
|
+
end: fakeIso(c.clippedEnd)
|
|
1243
|
+
}));
|
|
1244
|
+
const groups = buildOverlapGroups(synthetic);
|
|
1245
|
+
const positioned = [];
|
|
1246
|
+
for (const group of groups) {
|
|
1247
|
+
const cols = assignColumns(group);
|
|
1248
|
+
for (const { event, column, totalColumns } of cols) {
|
|
1249
|
+
const idx = parseInt(event.id.split("::tl::")[1] ?? "0", 10);
|
|
1250
|
+
const meta = clipped[idx];
|
|
1251
|
+
const start = meta.clippedStart;
|
|
1252
|
+
const end = meta.clippedEnd;
|
|
1253
|
+
const leftPct = (start - dayStartHour) / totalHours * 100;
|
|
1254
|
+
const widthPct = Math.max((end - start) / totalHours * 100, 0.5);
|
|
1255
|
+
positioned.push({
|
|
1256
|
+
event: meta.original,
|
|
1257
|
+
resourceId,
|
|
1258
|
+
leftPct,
|
|
1259
|
+
widthPct,
|
|
1260
|
+
row: column,
|
|
1261
|
+
totalRows: totalColumns,
|
|
1262
|
+
isSegmentStart: meta.isSegmentStart,
|
|
1263
|
+
isSegmentEnd: meta.isSegmentEnd
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
result.set(resourceId, positioned);
|
|
1268
|
+
}
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
790
1272
|
// src/utils/undo.ts
|
|
791
1273
|
var DEFAULT_MAX_HISTORY = 30;
|
|
792
1274
|
function createUndoStack(initial) {
|
|
@@ -839,6 +1321,7 @@ exports.DEFAULT_DAY_END_HOUR = DEFAULT_DAY_END_HOUR;
|
|
|
839
1321
|
exports.DEFAULT_DAY_START_HOUR = DEFAULT_DAY_START_HOUR;
|
|
840
1322
|
exports.DEFAULT_LABELS = DEFAULT_LABELS;
|
|
841
1323
|
exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
|
|
1324
|
+
exports.DEFAULT_SNAP_DURATION = DEFAULT_SNAP_DURATION;
|
|
842
1325
|
exports.DEFAULT_VIEW = DEFAULT_VIEW;
|
|
843
1326
|
exports.HOURS_IN_DAY = HOURS_IN_DAY;
|
|
844
1327
|
exports.MINUTES_IN_DAY = MINUTES_IN_DAY;
|
|
@@ -854,15 +1337,23 @@ exports.canRedo = canRedo;
|
|
|
854
1337
|
exports.canUndo = canUndo;
|
|
855
1338
|
exports.computeDropPosition = computeDropPosition;
|
|
856
1339
|
exports.computeTimePositions = computeTimePositions;
|
|
1340
|
+
exports.computeTimelinePositions = computeTimelinePositions;
|
|
1341
|
+
exports.convertWallTime = convertWallTime;
|
|
857
1342
|
exports.createInitialState = createInitialState;
|
|
858
1343
|
exports.createUndoStack = createUndoStack;
|
|
859
1344
|
exports.dateInRange = dateInRange;
|
|
860
1345
|
exports.daysBetween = daysBetween;
|
|
1346
|
+
exports.displayWallToEvent = displayWallToEvent;
|
|
1347
|
+
exports.downloadICal = downloadICal;
|
|
861
1348
|
exports.eachDayOfRange = eachDayOfRange;
|
|
862
1349
|
exports.endOfMonth = endOfMonth;
|
|
1350
|
+
exports.eventWallToDisplay = eventWallToDisplay;
|
|
1351
|
+
exports.eventsToICal = eventsToICal;
|
|
863
1352
|
exports.expandRecurringEvents = expandRecurringEvents;
|
|
864
1353
|
exports.filterEventsInRange = filterEventsInRange;
|
|
1354
|
+
exports.filterHiddenDays = filterHiddenDays;
|
|
865
1355
|
exports.filterVisibleEvents = filterVisibleEvents;
|
|
1356
|
+
exports.flattenResources = flattenResources;
|
|
866
1357
|
exports.formatAgendaDate = formatAgendaDate;
|
|
867
1358
|
exports.formatDayNumber = formatDayNumber;
|
|
868
1359
|
exports.formatMonthDay = formatMonthDay;
|
|
@@ -873,22 +1364,30 @@ exports.formatWeekdayNarrow = formatWeekdayNarrow;
|
|
|
873
1364
|
exports.formatWeekdayShort = formatWeekdayShort;
|
|
874
1365
|
exports.fractionalHourToDateTime = fractionalHourToDateTime;
|
|
875
1366
|
exports.generateOccurrences = generateOccurrences;
|
|
1367
|
+
exports.getBrowserTimeZone = getBrowserTimeZone;
|
|
876
1368
|
exports.getDurationHours = getDurationHours;
|
|
877
1369
|
exports.getEventSegments = getEventSegments;
|
|
878
1370
|
exports.getEventsForDay = getEventsForDay;
|
|
1371
|
+
exports.getEventsForResource = getEventsForResource;
|
|
879
1372
|
exports.getHourLabels = getHourLabels;
|
|
1373
|
+
exports.getISOWeekNumber = getISOWeekNumber;
|
|
880
1374
|
exports.getMonthViewRange = getMonthViewRange;
|
|
881
1375
|
exports.getTimeOfDay = getTimeOfDay;
|
|
1376
|
+
exports.getTimeZoneAbbreviation = getTimeZoneAbbreviation;
|
|
1377
|
+
exports.getTimeZoneOffset = getTimeZoneOffset;
|
|
882
1378
|
exports.getVisibleRange = getVisibleRange;
|
|
883
1379
|
exports.getWeekDays = getWeekDays;
|
|
884
1380
|
exports.getWeekViewRange = getWeekViewRange;
|
|
885
1381
|
exports.groupEventsByDate = groupEventsByDate;
|
|
1382
|
+
exports.groupEventsByResource = groupEventsByResource;
|
|
886
1383
|
exports.isAfter = isAfter;
|
|
887
1384
|
exports.isBefore = isBefore;
|
|
888
1385
|
exports.isMultiDayEvent = isMultiDayEvent;
|
|
889
1386
|
exports.isSameDay = isSameDay;
|
|
890
1387
|
exports.isSameMonth = isSameMonth;
|
|
891
1388
|
exports.isToday = isToday;
|
|
1389
|
+
exports.isValidTimeZone = isValidTimeZone;
|
|
1390
|
+
exports.listTimeZones = listTimeZones;
|
|
892
1391
|
exports.normalizeRange = normalizeRange;
|
|
893
1392
|
exports.parseDate = parseDate;
|
|
894
1393
|
exports.parseRRule = parseRRule;
|
|
@@ -907,6 +1406,8 @@ exports.toDateString = toDateString;
|
|
|
907
1406
|
exports.toDateTimeString = toDateTimeString;
|
|
908
1407
|
exports.toRRuleString = toRRuleString;
|
|
909
1408
|
exports.undo = undo;
|
|
1409
|
+
exports.utcToWallTime = utcToWallTime;
|
|
1410
|
+
exports.wallTimeToUtc = wallTimeToUtc;
|
|
910
1411
|
exports.yPositionToFractionalHour = yPositionToFractionalHour;
|
|
911
1412
|
//# sourceMappingURL=index.cjs.map
|
|
912
1413
|
//# sourceMappingURL=index.cjs.map
|