@forcecalendar/core 2.4.0 → 2.5.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/core/calendar/Calendar.js +102 -10
- package/core/events/Event.js +61 -0
- package/core/events/EventStore.js +308 -53
- package/core/events/RecurrenceEngine.js +368 -17
- package/core/events/RecurrenceEngineV2.js +281 -48
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +88 -14
- package/core/types.js +52 -0
- package/package.json +1 -1
- package/types/calendar/Calendar.d.ts +77 -7
- package/types/events/Event.d.ts +39 -0
- package/types/events/EventStore.d.ts +163 -5
- package/types/events/RecurrenceEngine.d.ts +156 -0
- package/types/events/RecurrenceEngineV2.d.ts +112 -8
- package/types/index.d.ts +1 -1
- package/types/integration/EnhancedCalendar.d.ts +66 -1
- package/types/types.d.ts +175 -0
|
@@ -178,9 +178,12 @@ export class Calendar {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Update an event
|
|
181
|
-
*
|
|
181
|
+
*
|
|
182
|
+
* An occurrence id taken from view data (see {@link Event.occurrenceId})
|
|
183
|
+
* updates the recurring master, i.e. the whole series.
|
|
184
|
+
* @param {string} eventId - Event id or occurrence id
|
|
182
185
|
* @param {Object} updates - Properties to update
|
|
183
|
-
* @returns {Event} The updated event
|
|
186
|
+
* @returns {Event} The updated event (the master for an occurrence id)
|
|
184
187
|
*/
|
|
185
188
|
updateEvent(eventId, updates) {
|
|
186
189
|
const oldEvent = this.eventStore.getEvent(eventId);
|
|
@@ -193,7 +196,10 @@ export class Calendar {
|
|
|
193
196
|
|
|
194
197
|
/**
|
|
195
198
|
* Remove an event
|
|
196
|
-
*
|
|
199
|
+
*
|
|
200
|
+
* An occurrence id taken from view data (see {@link Event.occurrenceId})
|
|
201
|
+
* removes the recurring master, i.e. the whole series.
|
|
202
|
+
* @param {string} eventId - Event id or occurrence id
|
|
197
203
|
* @returns {boolean} True if removed
|
|
198
204
|
*/
|
|
199
205
|
removeEvent(eventId) {
|
|
@@ -218,15 +224,19 @@ export class Calendar {
|
|
|
218
224
|
|
|
219
225
|
/**
|
|
220
226
|
* Get an event by ID
|
|
221
|
-
*
|
|
222
|
-
*
|
|
227
|
+
*
|
|
228
|
+
* Occurrence ids taken from view data (`<masterId>_<startMs>`, see
|
|
229
|
+
* {@link Event.occurrenceId}) resolve to the stored recurring master, so
|
|
230
|
+
* every id a renderer hands back can be looked up here.
|
|
231
|
+
* @param {string} eventId - Event id or occurrence id
|
|
232
|
+
* @returns {Event|null} The stored event (the master for an occurrence id) or null
|
|
223
233
|
*/
|
|
224
234
|
getEvent(eventId) {
|
|
225
235
|
return this.eventStore.getEvent(eventId);
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
/**
|
|
229
|
-
* Get all events
|
|
239
|
+
* Get all stored events (recurring masters, never their occurrences)
|
|
230
240
|
* @returns {Event[]}
|
|
231
241
|
*/
|
|
232
242
|
getEvents() {
|
|
@@ -344,7 +354,7 @@ export class Calendar {
|
|
|
344
354
|
}
|
|
345
355
|
|
|
346
356
|
/**
|
|
347
|
-
* Get events for a specific date
|
|
357
|
+
* Get events for a specific date, with recurring series expanded into occurrences
|
|
348
358
|
* @param {Date} date - The date
|
|
349
359
|
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
350
360
|
* @returns {Event[]}
|
|
@@ -353,6 +363,20 @@ export class Calendar {
|
|
|
353
363
|
return this.eventStore.getEventsForDate(date, timezone || this.config.timeZone);
|
|
354
364
|
}
|
|
355
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Get the events for every day in a range, keyed by local date (YYYY-MM-DD)
|
|
368
|
+
*
|
|
369
|
+
* Recurring series are expanded once for the whole range; this is what the
|
|
370
|
+
* month and week views use. See `EventStore.getEventsByDate`.
|
|
371
|
+
* @param {Date} start - First day of the range
|
|
372
|
+
* @param {Date} end - Last day of the range
|
|
373
|
+
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
374
|
+
* @returns {Map<string, Event[]>} Local date string -> events on that day
|
|
375
|
+
*/
|
|
376
|
+
getEventsByDate(start, end, timezone = null) {
|
|
377
|
+
return this.eventStore.getEventsByDate(start, end, timezone || this.config.timeZone);
|
|
378
|
+
}
|
|
379
|
+
|
|
356
380
|
/**
|
|
357
381
|
* Get events in a date range
|
|
358
382
|
* @param {Date} start - Start date
|
|
@@ -364,6 +388,64 @@ export class Calendar {
|
|
|
364
388
|
return this.eventStore.getEventsInRange(start, end, true, timezone || this.config.timeZone);
|
|
365
389
|
}
|
|
366
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Lazily iterate the occurrences of an event in chronological order.
|
|
393
|
+
*
|
|
394
|
+
* Occurrences are produced one at a time, so taking the next few of an
|
|
395
|
+
* open-ended series does not expand the series. `after` and `before`
|
|
396
|
+
* are exclusive unless `inclusive` is set; see
|
|
397
|
+
* RecurrenceEngineV2.iterateOccurrences for the full semantics.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* for (const occurrence of calendar.iterateOccurrences('standup', { after: new Date() })) {
|
|
401
|
+
* if (occurrence.start > deadline) break;
|
|
402
|
+
* remind(occurrence);
|
|
403
|
+
* }
|
|
404
|
+
*
|
|
405
|
+
* @param {string} eventId - The event ID
|
|
406
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
407
|
+
* @returns {Generator<import('../types.js').ExpandedOccurrence, void, undefined>} Occurrences in chronological order
|
|
408
|
+
* @throws {Error} If no event with the ID exists
|
|
409
|
+
*/
|
|
410
|
+
iterateOccurrences(eventId, options = {}) {
|
|
411
|
+
return this.eventStore.iterateOccurrences(eventId, options);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* First occurrence of an event after an instant, or null when the
|
|
416
|
+
* series has no occurrence after it. `after` is exclusive unless
|
|
417
|
+
* `options.inclusive` is set.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* const upcoming = calendar.getNextOccurrence('standup', new Date());
|
|
421
|
+
*
|
|
422
|
+
* @param {string} eventId - The event ID
|
|
423
|
+
* @param {Date|number} [after=null] - Instant to search from (defaults to the series start)
|
|
424
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Further options
|
|
425
|
+
* @returns {import('../types.js').ExpandedOccurrence|null} The next occurrence, or null
|
|
426
|
+
* @throws {Error} If no event with the ID exists
|
|
427
|
+
*/
|
|
428
|
+
getNextOccurrence(eventId, after = null, options = {}) {
|
|
429
|
+
return this.eventStore.getNextOccurrence(eventId, after, options);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The first `count` occurrences of an event inside a window, generated
|
|
434
|
+
* lazily. `count` is capped at the engine's MAX_OCCURRENCES_HARD_LIMIT.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* const nextFive = calendar.takeOccurrences('standup', 5, { after: new Date() });
|
|
438
|
+
*
|
|
439
|
+
* @param {string} eventId - The event ID
|
|
440
|
+
* @param {number} count - Maximum number of occurrences to return
|
|
441
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
442
|
+
* @returns {import('../types.js').ExpandedOccurrence[]} Up to `count` occurrences in chronological order
|
|
443
|
+
* @throws {Error} If no event with the ID exists
|
|
444
|
+
*/
|
|
445
|
+
takeOccurrences(eventId, count, options = {}) {
|
|
446
|
+
return this.eventStore.takeOccurrences(eventId, count, options);
|
|
447
|
+
}
|
|
448
|
+
|
|
367
449
|
/**
|
|
368
450
|
* Set the calendar's timezone
|
|
369
451
|
* @param {string} timezone - IANA timezone identifier
|
|
@@ -529,6 +611,12 @@ export class Calendar {
|
|
|
529
611
|
? 6
|
|
530
612
|
: Math.ceil((lastDay.getDate() + DateUtils.getDayOfWeek(firstDay, weekStartsOn)) / 7);
|
|
531
613
|
|
|
614
|
+
// Expand recurring series once for the whole grid, not once per cell
|
|
615
|
+
const eventsByDate = this.getEventsByDate(
|
|
616
|
+
startDate,
|
|
617
|
+
DateUtils.addDays(startDate, maxWeeks * 7 - 1)
|
|
618
|
+
);
|
|
619
|
+
|
|
532
620
|
for (let weekIndex = 0; weekIndex < maxWeeks; weekIndex++) {
|
|
533
621
|
const week = {
|
|
534
622
|
weekNumber: DateUtils.getWeekNumber(currentDate),
|
|
@@ -547,7 +635,7 @@ export class Calendar {
|
|
|
547
635
|
isCurrentMonth,
|
|
548
636
|
isToday,
|
|
549
637
|
isWeekend,
|
|
550
|
-
events:
|
|
638
|
+
events: eventsByDate.get(DateUtils.getLocalDateString(dayDate)) || []
|
|
551
639
|
});
|
|
552
640
|
|
|
553
641
|
// Use DateUtils.addDays to handle month boundaries correctly
|
|
@@ -580,8 +668,12 @@ export class Calendar {
|
|
|
580
668
|
const days = [];
|
|
581
669
|
const currentDate = new Date(startDate);
|
|
582
670
|
|
|
671
|
+
// Expand recurring series once for the whole week, not once per day
|
|
672
|
+
const eventsByDate = this.getEventsByDate(startDate, endDate);
|
|
673
|
+
|
|
583
674
|
for (let i = 0; i < 7; i++) {
|
|
584
675
|
const dayDate = new Date(currentDate);
|
|
676
|
+
const events = eventsByDate.get(DateUtils.getLocalDateString(dayDate)) || [];
|
|
585
677
|
days.push({
|
|
586
678
|
date: dayDate,
|
|
587
679
|
dayOfMonth: dayDate.getDate(),
|
|
@@ -589,9 +681,9 @@ export class Calendar {
|
|
|
589
681
|
dayName: DateUtils.getDayName(dayDate, this.state.get('locale')),
|
|
590
682
|
isToday: DateUtils.isToday(dayDate),
|
|
591
683
|
isWeekend: dayDate.getDay() === 0 || dayDate.getDay() === 6,
|
|
592
|
-
events
|
|
684
|
+
events,
|
|
593
685
|
// Add overlap groups for positioning overlapping events
|
|
594
|
-
overlapGroups: this.eventStore.
|
|
686
|
+
overlapGroups: this.eventStore.groupOverlappingEvents(events, true),
|
|
595
687
|
getEventPositions: events => this.eventStore.calculateEventPositions(events)
|
|
596
688
|
});
|
|
597
689
|
// Move to next day
|
package/core/events/Event.js
CHANGED
|
@@ -325,6 +325,16 @@ export class Event {
|
|
|
325
325
|
this.recurring = normalized.recurring;
|
|
326
326
|
this.recurrenceRule = normalized.recurrenceRule;
|
|
327
327
|
|
|
328
|
+
// Occurrence identity. Set by EventStore.expandRecurringEvent on the
|
|
329
|
+
// instances it derives from a recurring series; stored events keep the
|
|
330
|
+
// defaults. The id of an occurrence is Event.occurrenceId(master, start).
|
|
331
|
+
/** @type {boolean} True when this instance is one occurrence of a recurring series */
|
|
332
|
+
this.isOccurrence = false;
|
|
333
|
+
/** @type {string|null} Id of the recurring master this occurrence belongs to */
|
|
334
|
+
this.recurringEventId = null;
|
|
335
|
+
/** @type {Date|null} Start of this occurrence as generated by the recurrence rule */
|
|
336
|
+
this.occurrenceStart = null;
|
|
337
|
+
|
|
328
338
|
// Store original timezone from system if not provided
|
|
329
339
|
this._originalTimeZone = normalized.timeZone || null;
|
|
330
340
|
|
|
@@ -721,6 +731,57 @@ export class Event {
|
|
|
721
731
|
return true;
|
|
722
732
|
}
|
|
723
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Build the id of one occurrence of a recurring series.
|
|
736
|
+
*
|
|
737
|
+
* The id is `<recurringEventId>_<startMs>` where `startMs` is the
|
|
738
|
+
* occurrence start as returned by `Date.prototype.getTime()`. It is
|
|
739
|
+
* deterministic, so the same occurrence gets the same id no matter which
|
|
740
|
+
* range it was expanded for, and it matches the ids generated by
|
|
741
|
+
* `RecurrenceEngineV2`. Use {@link Event.parseOccurrenceId} to get the
|
|
742
|
+
* master id back.
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* Event.occurrenceId('standup', new Date(2025, 5, 16, 9)); // 'standup_1750028400000'
|
|
746
|
+
*
|
|
747
|
+
* @param {string} recurringEventId - Id of the recurring master event
|
|
748
|
+
* @param {Date|number|string} occurrenceStart - Start of the occurrence
|
|
749
|
+
* @returns {string} Occurrence id
|
|
750
|
+
*/
|
|
751
|
+
static occurrenceId(recurringEventId, occurrenceStart) {
|
|
752
|
+
const start = occurrenceStart instanceof Date ? occurrenceStart : new Date(occurrenceStart);
|
|
753
|
+
return `${recurringEventId}_${start.getTime()}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Split an occurrence id built by {@link Event.occurrenceId} into the master
|
|
758
|
+
* id and the occurrence start.
|
|
759
|
+
*
|
|
760
|
+
* Returns `null` for ids that do not have the `<id>_<startMs>` shape. A
|
|
761
|
+
* positive result only means the id is well-formed; whether the master
|
|
762
|
+
* exists is for the caller (see `EventStore.getEvent`) to check.
|
|
763
|
+
*
|
|
764
|
+
* @param {string} id - Candidate occurrence id
|
|
765
|
+
* @returns {{recurringEventId: string, occurrenceStart: Date}|null} Parsed parts or null
|
|
766
|
+
*/
|
|
767
|
+
static parseOccurrenceId(id) {
|
|
768
|
+
if (typeof id !== 'string') {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const separator = id.lastIndexOf('_');
|
|
772
|
+
if (separator <= 0 || separator === id.length - 1) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const time = id.slice(separator + 1);
|
|
776
|
+
if (!/^-?\d+$/.test(time)) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
recurringEventId: id.slice(0, separator),
|
|
781
|
+
occurrenceStart: new Date(Number(time))
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
724
785
|
// ============ Attendee Management Methods ============
|
|
725
786
|
|
|
726
787
|
/**
|