@forcecalendar/core 2.0.0 → 2.1.1
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 +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +13 -19
- package/core/events/EventStore.js +26 -14
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +32 -20
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +432 -432
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +7 -2
|
@@ -357,11 +357,7 @@ export class Calendar {
|
|
|
357
357
|
* @returns {string} Formatted date string
|
|
358
358
|
*/
|
|
359
359
|
formatInTimezone(date, timezone = null, options = {}) {
|
|
360
|
-
return this.timezoneManager.formatInTimezone(
|
|
361
|
-
date,
|
|
362
|
-
timezone || this.config.timeZone,
|
|
363
|
-
options
|
|
364
|
-
);
|
|
360
|
+
return this.timezoneManager.formatInTimezone(date, timezone || this.config.timeZone, options);
|
|
365
361
|
}
|
|
366
362
|
|
|
367
363
|
/**
|
|
@@ -437,7 +433,9 @@ export class Calendar {
|
|
|
437
433
|
let currentDate = new Date(startDate);
|
|
438
434
|
|
|
439
435
|
// Generate weeks
|
|
440
|
-
const maxWeeks = fixedWeekCount
|
|
436
|
+
const maxWeeks = fixedWeekCount
|
|
437
|
+
? 6
|
|
438
|
+
: Math.ceil((lastDay.getDate() + DateUtils.getDayOfWeek(firstDay, weekStartsOn)) / 7);
|
|
441
439
|
|
|
442
440
|
for (let weekIndex = 0; weekIndex < maxWeeks; weekIndex++) {
|
|
443
441
|
const week = {
|
|
@@ -502,7 +500,7 @@ export class Calendar {
|
|
|
502
500
|
events: this.getEventsForDate(dayDate),
|
|
503
501
|
// Add overlap groups for positioning overlapping events
|
|
504
502
|
overlapGroups: this.eventStore.getOverlapGroups(dayDate, true),
|
|
505
|
-
getEventPositions:
|
|
503
|
+
getEventPositions: events => this.eventStore.calculateEventPositions(events)
|
|
506
504
|
});
|
|
507
505
|
// Move to next day
|
|
508
506
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
@@ -706,7 +704,7 @@ export class Calendar {
|
|
|
706
704
|
});
|
|
707
705
|
|
|
708
706
|
// Listen to event store changes
|
|
709
|
-
this.eventStore.subscribe(
|
|
707
|
+
this.eventStore.subscribe(change => {
|
|
710
708
|
this._emit('eventStoreChange', change);
|
|
711
709
|
});
|
|
712
710
|
}
|
|
@@ -752,4 +750,4 @@ export class Calendar {
|
|
|
752
750
|
|
|
753
751
|
this._emit('destroy');
|
|
754
752
|
}
|
|
755
|
-
}// Test workflow
|
|
753
|
+
} // Test workflow
|
|
@@ -209,9 +209,11 @@ export class DateUtils {
|
|
|
209
209
|
* @returns {boolean}
|
|
210
210
|
*/
|
|
211
211
|
static isSameDay(date1, date2) {
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
return (
|
|
213
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
214
|
+
date1.getMonth() === date2.getMonth() &&
|
|
215
|
+
date1.getDate() === date2.getDate()
|
|
216
|
+
);
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
/**
|
|
@@ -234,8 +236,7 @@ export class DateUtils {
|
|
|
234
236
|
* @returns {boolean}
|
|
235
237
|
*/
|
|
236
238
|
static isSameMonth(date1, date2) {
|
|
237
|
-
return date1.getFullYear() === date2.getFullYear() &&
|
|
238
|
-
date1.getMonth() === date2.getMonth();
|
|
239
|
+
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth();
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
/**
|
|
@@ -389,7 +390,7 @@ export class DateUtils {
|
|
|
389
390
|
* @returns {boolean}
|
|
390
391
|
*/
|
|
391
392
|
static isLeapYear(year) {
|
|
392
|
-
return (year % 4 === 0 && year % 100 !== 0) ||
|
|
393
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
393
394
|
}
|
|
394
395
|
|
|
395
396
|
/**
|
|
@@ -504,7 +505,7 @@ export class DateUtils {
|
|
|
504
505
|
const originalOffset = DateUtils.getTimezoneOffset(date, timeZone);
|
|
505
506
|
|
|
506
507
|
// Add hours
|
|
507
|
-
result.setTime(result.getTime() +
|
|
508
|
+
result.setTime(result.getTime() + hours * 60 * 60 * 1000);
|
|
508
509
|
|
|
509
510
|
// Check if DST transition occurred
|
|
510
511
|
const newOffset = DateUtils.getTimezoneOffset(result, timeZone);
|
|
@@ -550,8 +551,8 @@ export class DateUtils {
|
|
|
550
551
|
|
|
551
552
|
// Get offset and adjust
|
|
552
553
|
const offset = DateUtils.getTimezoneOffset(localDate, timeZone);
|
|
553
|
-
const utcTime = localDate.getTime() +
|
|
554
|
+
const utcTime = localDate.getTime() + offset * 60000;
|
|
554
555
|
|
|
555
556
|
return new Date(utcTime);
|
|
556
557
|
}
|
|
557
|
-
}
|
|
558
|
+
}
|
|
@@ -47,7 +47,8 @@ export class ConflictDetector {
|
|
|
47
47
|
const searchStart = new Date(event.start.getTime() - opts.bufferMinutes * 60000);
|
|
48
48
|
const searchEnd = new Date(event.end.getTime() + opts.bufferMinutes * 60000);
|
|
49
49
|
|
|
50
|
-
const potentialConflicts = this.eventStore
|
|
50
|
+
const potentialConflicts = this.eventStore
|
|
51
|
+
.getEventsInRange(searchStart, searchEnd, false)
|
|
51
52
|
.filter(e => {
|
|
52
53
|
// Exclude self
|
|
53
54
|
if (e.id === event.id) return false;
|
|
@@ -64,11 +65,7 @@ export class ConflictDetector {
|
|
|
64
65
|
|
|
65
66
|
// Check each potential conflict
|
|
66
67
|
for (const conflictingEvent of potentialConflicts) {
|
|
67
|
-
const eventConflicts = this._detectEventConflicts(
|
|
68
|
-
event,
|
|
69
|
-
conflictingEvent,
|
|
70
|
-
opts
|
|
71
|
-
);
|
|
68
|
+
const eventConflicts = this._detectEventConflicts(event, conflictingEvent, opts);
|
|
72
69
|
|
|
73
70
|
if (eventConflicts.length > 0) {
|
|
74
71
|
conflicts.push(...eventConflicts);
|
|
@@ -131,8 +128,8 @@ export class ConflictDetector {
|
|
|
131
128
|
if (!opts.includeStatuses.includes(event.status)) return false;
|
|
132
129
|
if (event.status === 'cancelled') return false;
|
|
133
130
|
|
|
134
|
-
return
|
|
135
|
-
attendeeEmails.includes(attendee.email)
|
|
131
|
+
return (
|
|
132
|
+
event.attendees && event.attendees.some(attendee => attendeeEmails.includes(attendee.email))
|
|
136
133
|
);
|
|
137
134
|
});
|
|
138
135
|
|
|
@@ -173,9 +170,10 @@ export class ConflictDetector {
|
|
|
173
170
|
const freePeriods = [];
|
|
174
171
|
|
|
175
172
|
// Get busy periods
|
|
176
|
-
const busyPeriods =
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
const busyPeriods =
|
|
174
|
+
opts.attendeeEmails.length > 0
|
|
175
|
+
? this.getBusyPeriods(opts.attendeeEmails, start, end)
|
|
176
|
+
: this._getAllBusyPeriods(start, end);
|
|
179
177
|
|
|
180
178
|
// Find gaps between busy periods
|
|
181
179
|
let currentTime = new Date(start);
|
|
@@ -186,7 +184,10 @@ export class ConflictDetector {
|
|
|
186
184
|
const gapDuration = (busy.start - currentTime) / 60000; // minutes
|
|
187
185
|
if (gapDuration >= duration) {
|
|
188
186
|
// Check if within business hours if required
|
|
189
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
!opts.businessHoursOnly ||
|
|
189
|
+
this._isWithinBusinessHours(currentTime, busy.start, opts)
|
|
190
|
+
) {
|
|
190
191
|
freePeriods.push({
|
|
191
192
|
start: new Date(currentTime),
|
|
192
193
|
end: new Date(busy.start)
|
|
@@ -221,11 +222,7 @@ export class ConflictDetector {
|
|
|
221
222
|
const conflicts = [];
|
|
222
223
|
|
|
223
224
|
// Check time overlap with buffer
|
|
224
|
-
const hasTimeOverlap = this._checkTimeOverlap(
|
|
225
|
-
event1,
|
|
226
|
-
event2,
|
|
227
|
-
options.bufferMinutes
|
|
228
|
-
);
|
|
225
|
+
const hasTimeOverlap = this._checkTimeOverlap(event1, event2, options.bufferMinutes);
|
|
229
226
|
|
|
230
227
|
if (hasTimeOverlap) {
|
|
231
228
|
// Time conflict
|
|
@@ -490,14 +487,17 @@ export class ConflictDetector {
|
|
|
490
487
|
* @private
|
|
491
488
|
*/
|
|
492
489
|
_getAllBusyPeriods(start, end) {
|
|
493
|
-
const events = this.eventStore
|
|
490
|
+
const events = this.eventStore
|
|
491
|
+
.getEventsInRange(start, end, false)
|
|
494
492
|
.filter(e => e.status !== 'cancelled');
|
|
495
493
|
|
|
496
|
-
return events
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
return events
|
|
495
|
+
.map(event => ({
|
|
496
|
+
start: event.start,
|
|
497
|
+
end: event.end,
|
|
498
|
+
eventIds: [event.id]
|
|
499
|
+
}))
|
|
500
|
+
.sort((a, b) => a.start - b.start);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
/**
|
|
@@ -514,4 +514,4 @@ export class ConflictDetector {
|
|
|
514
514
|
|
|
515
515
|
return startHour >= businessStart && endHour <= businessEnd;
|
|
516
516
|
}
|
|
517
|
-
}
|
|
517
|
+
}
|
package/core/events/Event.js
CHANGED
|
@@ -179,12 +179,12 @@ export class Event {
|
|
|
179
179
|
organizer = null,
|
|
180
180
|
attendees = [],
|
|
181
181
|
reminders = [],
|
|
182
|
-
category,
|
|
183
|
-
categories,
|
|
182
|
+
category, // Support singular category (no default)
|
|
183
|
+
categories, // Support plural categories (no default)
|
|
184
184
|
attachments = [],
|
|
185
185
|
conferenceData = null,
|
|
186
186
|
metadata = {},
|
|
187
|
-
...rest
|
|
187
|
+
...rest // Capture any extra properties
|
|
188
188
|
}) {
|
|
189
189
|
// Normalize and validate input
|
|
190
190
|
const normalized = Event.normalize({
|
|
@@ -208,12 +208,12 @@ export class Event {
|
|
|
208
208
|
organizer,
|
|
209
209
|
attendees,
|
|
210
210
|
reminders,
|
|
211
|
-
category,
|
|
212
|
-
categories,
|
|
211
|
+
category, // Pass category to normalize
|
|
212
|
+
categories, // Pass categories to normalize
|
|
213
213
|
attachments,
|
|
214
214
|
conferenceData,
|
|
215
215
|
metadata,
|
|
216
|
-
...rest
|
|
216
|
+
...rest // Pass any extra properties
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
// Validate normalized data
|
|
@@ -374,7 +374,7 @@ export class Event {
|
|
|
374
374
|
* @returns {boolean} True if event spans multiple days
|
|
375
375
|
*/
|
|
376
376
|
get isMultiDay() {
|
|
377
|
-
if (!
|
|
377
|
+
if (!Object.prototype.hasOwnProperty.call(this._cache, 'isMultiDay')) {
|
|
378
378
|
const startDay = this.start.toDateString();
|
|
379
379
|
const endDay = this.end.toDateString();
|
|
380
380
|
this._cache.isMultiDay = startDay !== endDay;
|
|
@@ -417,10 +417,9 @@ export class Event {
|
|
|
417
417
|
dayEnd.setHours(23, 59, 59, 999);
|
|
418
418
|
|
|
419
419
|
return this.start <= dayEnd && this.end >= dayStart;
|
|
420
|
-
} else {
|
|
421
|
-
// Single day event: check if it's on the same day
|
|
422
|
-
return startString === dateString;
|
|
423
420
|
}
|
|
421
|
+
// Single day event: check if it's on the same day
|
|
422
|
+
return startString === dateString;
|
|
424
423
|
}
|
|
425
424
|
|
|
426
425
|
/**
|
|
@@ -436,9 +435,8 @@ export class Event {
|
|
|
436
435
|
} else if (otherEvent && otherEvent.start && otherEvent.end) {
|
|
437
436
|
// Allow checking against time ranges
|
|
438
437
|
return !(this.end <= otherEvent.start || this.start >= otherEvent.end);
|
|
439
|
-
} else {
|
|
440
|
-
throw new Error('Parameter must be an Event instance or have start/end properties');
|
|
441
438
|
}
|
|
439
|
+
throw new Error('Parameter must be an Event instance or have start/end properties');
|
|
442
440
|
}
|
|
443
441
|
|
|
444
442
|
/**
|
|
@@ -586,9 +584,7 @@ export class Event {
|
|
|
586
584
|
* @returns {boolean} True if attendee was removed
|
|
587
585
|
*/
|
|
588
586
|
removeAttendee(emailOrId) {
|
|
589
|
-
const index = this.attendees.findIndex(
|
|
590
|
-
a => a.email === emailOrId || a.id === emailOrId
|
|
591
|
-
);
|
|
587
|
+
const index = this.attendees.findIndex(a => a.email === emailOrId || a.id === emailOrId);
|
|
592
588
|
|
|
593
589
|
if (index !== -1) {
|
|
594
590
|
this.attendees.splice(index, 1);
|
|
@@ -747,9 +743,7 @@ export class Event {
|
|
|
747
743
|
*/
|
|
748
744
|
removeCategory(category) {
|
|
749
745
|
const normalizedCategory = category.trim().toLowerCase();
|
|
750
|
-
const index = this.categories.findIndex(
|
|
751
|
-
c => c.toLowerCase() === normalizedCategory
|
|
752
|
-
);
|
|
746
|
+
const index = this.categories.findIndex(c => c.toLowerCase() === normalizedCategory);
|
|
753
747
|
|
|
754
748
|
if (index !== -1) {
|
|
755
749
|
this.categories.splice(index, 1);
|
|
@@ -917,4 +911,4 @@ export class Event {
|
|
|
917
911
|
get isVirtual() {
|
|
918
912
|
return this.conferenceData !== null;
|
|
919
913
|
}
|
|
920
|
-
}
|
|
914
|
+
}
|
|
@@ -226,12 +226,12 @@ export class EventStore {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Filter by all-day events
|
|
229
|
-
if (
|
|
229
|
+
if (Object.prototype.hasOwnProperty.call(filters, 'allDay')) {
|
|
230
230
|
results = results.filter(event => event.allDay === filters.allDay);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// Filter by recurring
|
|
234
|
-
if (
|
|
234
|
+
if (Object.prototype.hasOwnProperty.call(filters, 'recurring')) {
|
|
235
235
|
results = results.filter(event => event.recurring === filters.recurring);
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -250,14 +250,16 @@ export class EventStore {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
// Filter by having attendees
|
|
253
|
-
if (
|
|
254
|
-
results = results.filter(event =>
|
|
253
|
+
if (Object.prototype.hasOwnProperty.call(filters, 'hasAttendees')) {
|
|
254
|
+
results = results.filter(event =>
|
|
255
|
+
filters.hasAttendees ? event.hasAttendees : !event.hasAttendees
|
|
256
|
+
);
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
// Filter by organizer email
|
|
258
260
|
if (filters.organizerEmail) {
|
|
259
|
-
results = results.filter(
|
|
260
|
-
event.organizer && event.organizer.email === filters.organizerEmail
|
|
261
|
+
results = results.filter(
|
|
262
|
+
event => event.organizer && event.organizer.email === filters.organizerEmail
|
|
261
263
|
);
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -456,7 +458,7 @@ export class EventStore {
|
|
|
456
458
|
events.sort((a, b) => {
|
|
457
459
|
const startDiff = a.start - b.start;
|
|
458
460
|
if (startDiff !== 0) return startDiff;
|
|
459
|
-
return
|
|
461
|
+
return b.end - b.start - (a.end - a.start);
|
|
460
462
|
});
|
|
461
463
|
|
|
462
464
|
// Track which columns are occupied at each time
|
|
@@ -717,8 +719,10 @@ export class EventStore {
|
|
|
717
719
|
// Index first week
|
|
718
720
|
const firstWeekEnd = new Date(startDate);
|
|
719
721
|
firstWeekEnd.setDate(firstWeekEnd.getDate() + 7);
|
|
720
|
-
const firstWeekDates = DateUtils.getDateRange(
|
|
721
|
-
|
|
722
|
+
const firstWeekDates = DateUtils.getDateRange(
|
|
723
|
+
startDate,
|
|
724
|
+
firstWeekEnd < endDate ? firstWeekEnd : endDate
|
|
725
|
+
);
|
|
722
726
|
|
|
723
727
|
firstWeekDates.forEach(date => {
|
|
724
728
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
@@ -851,11 +855,19 @@ export class EventStore {
|
|
|
851
855
|
this.batchBackup = {
|
|
852
856
|
events: new Map(this.events),
|
|
853
857
|
indices: {
|
|
854
|
-
byDate: new Map(
|
|
855
|
-
|
|
858
|
+
byDate: new Map(
|
|
859
|
+
Array.from(this.indices.byDate.entries()).map(([k, v]) => [k, new Set(v)])
|
|
860
|
+
),
|
|
861
|
+
byMonth: new Map(
|
|
862
|
+
Array.from(this.indices.byMonth.entries()).map(([k, v]) => [k, new Set(v)])
|
|
863
|
+
),
|
|
856
864
|
recurring: new Set(this.indices.recurring),
|
|
857
|
-
byCategory: new Map(
|
|
858
|
-
|
|
865
|
+
byCategory: new Map(
|
|
866
|
+
Array.from(this.indices.byCategory.entries()).map(([k, v]) => [k, new Set(v)])
|
|
867
|
+
),
|
|
868
|
+
byStatus: new Map(
|
|
869
|
+
Array.from(this.indices.byStatus.entries()).map(([k, v]) => [k, new Set(v)])
|
|
870
|
+
)
|
|
859
871
|
},
|
|
860
872
|
version: this.version
|
|
861
873
|
};
|
|
@@ -1209,4 +1221,4 @@ export class EventStore {
|
|
|
1209
1221
|
|
|
1210
1222
|
return eventsWithConflicts;
|
|
1211
1223
|
}
|
|
1212
|
-
}
|
|
1224
|
+
}
|