@forcecalendar/core 2.1.8 → 2.1.10
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.
|
@@ -731,23 +731,29 @@ export class Calendar {
|
|
|
731
731
|
* Destroy the calendar and clean up
|
|
732
732
|
*/
|
|
733
733
|
destroy() {
|
|
734
|
+
// Emit destroy event before clearing listeners
|
|
735
|
+
this._emit('destroy');
|
|
736
|
+
|
|
734
737
|
// Clear all listeners
|
|
735
738
|
this.listeners.clear();
|
|
736
739
|
|
|
737
740
|
// Properly destroy EventStore (clears events, caches, and cleanup timers)
|
|
738
741
|
this.eventStore.destroy();
|
|
739
742
|
|
|
740
|
-
// Clear plugins
|
|
743
|
+
// Clear plugins — wrap each uninstall in try-catch so one failure
|
|
744
|
+
// doesn't prevent cleanup of remaining plugins
|
|
741
745
|
this.plugins.forEach(plugin => {
|
|
742
746
|
if (typeof plugin.uninstall === 'function') {
|
|
743
|
-
|
|
747
|
+
try {
|
|
748
|
+
plugin.uninstall(this);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
console.error('Error uninstalling plugin:', error);
|
|
751
|
+
}
|
|
744
752
|
}
|
|
745
753
|
});
|
|
746
754
|
this.plugins.clear();
|
|
747
755
|
|
|
748
756
|
// Clear view instances
|
|
749
757
|
this.views.clear();
|
|
750
|
-
|
|
751
|
-
this._emit('destroy');
|
|
752
758
|
}
|
|
753
759
|
} // Test workflow
|
|
@@ -288,9 +288,13 @@ export class DateUtils {
|
|
|
288
288
|
* @returns {number}
|
|
289
289
|
*/
|
|
290
290
|
static getWeekNumber(date) {
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
291
|
+
// ISO 8601: week 1 is the week containing the first Thursday of the year
|
|
292
|
+
const target = new Date(date);
|
|
293
|
+
target.setHours(0, 0, 0, 0);
|
|
294
|
+
// Set to nearest Thursday (current date + 4 - current day number, with Sunday as 7)
|
|
295
|
+
target.setDate(target.getDate() + 4 - (target.getDay() || 7));
|
|
296
|
+
const yearStart = new Date(target.getFullYear(), 0, 1);
|
|
297
|
+
return Math.ceil(((target - yearStart) / 86400000 + 1) / 7);
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
/**
|
|
@@ -94,9 +94,56 @@ export class RecurrenceEngine {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Apply BYSETPOS filtering if present and not already handled by MONTHLY+byDay
|
|
98
|
+
if (rule.bySetPos && rule.bySetPos.length > 0 && rule.freq !== 'MONTHLY') {
|
|
99
|
+
return this._applyBySetPos(occurrences, rule);
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
return occurrences;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Apply BYSETPOS to filter occurrences within each frequency period
|
|
107
|
+
* @param {Array} occurrences - Generated occurrences
|
|
108
|
+
* @param {Object} rule - Recurrence rule
|
|
109
|
+
* @returns {Array} Filtered occurrences
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
static _applyBySetPos(occurrences, rule) {
|
|
113
|
+
if (occurrences.length === 0) return occurrences;
|
|
114
|
+
|
|
115
|
+
// Group occurrences by period
|
|
116
|
+
const groups = new Map();
|
|
117
|
+
for (const occ of occurrences) {
|
|
118
|
+
let key;
|
|
119
|
+
switch (rule.freq) {
|
|
120
|
+
case 'YEARLY':
|
|
121
|
+
key = occ.start.getFullYear();
|
|
122
|
+
break;
|
|
123
|
+
case 'WEEKLY':
|
|
124
|
+
key = `${occ.start.getFullYear()}-W${DateUtils.getWeekNumber(occ.start)}`;
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
key = `${occ.start.getFullYear()}-${occ.start.getMonth()}`;
|
|
128
|
+
}
|
|
129
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
130
|
+
groups.get(key).push(occ);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Filter each group by BYSETPOS positions
|
|
134
|
+
const filtered = [];
|
|
135
|
+
for (const group of groups.values()) {
|
|
136
|
+
for (const pos of rule.bySetPos) {
|
|
137
|
+
const idx = pos > 0 ? pos - 1 : group.length + pos;
|
|
138
|
+
if (idx >= 0 && idx < group.length) {
|
|
139
|
+
filtered.push(group[idx]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return filtered.sort((a, b) => a.start - b.start);
|
|
145
|
+
}
|
|
146
|
+
|
|
100
147
|
/**
|
|
101
148
|
* Parse an RRULE string into a rule object
|
|
102
149
|
* @param {string|import('../../types.js').RecurrenceRule} ruleString - RRULE string (e.g., "FREQ=DAILY;INTERVAL=1;COUNT=10") or rule object
|
|
@@ -151,11 +198,19 @@ export class RecurrenceEngine {
|
|
|
151
198
|
// Specific day(s) of month
|
|
152
199
|
const currentMonth = next.getMonth();
|
|
153
200
|
next.setMonth(currentMonth + rule.interval);
|
|
154
|
-
|
|
201
|
+
// Clamp to last day of month if day doesn't exist
|
|
202
|
+
const daysInMonth = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
|
|
203
|
+
next.setDate(Math.min(rule.byMonthDay[0], daysInMonth));
|
|
155
204
|
} else if (rule.byDay && rule.byDay.length > 0) {
|
|
156
205
|
// Specific weekday of month (e.g., "2nd Tuesday")
|
|
157
206
|
next.setMonth(next.getMonth() + rule.interval);
|
|
158
|
-
|
|
207
|
+
// Extract position from the day code itself (e.g., "2TU" -> pos=2)
|
|
208
|
+
// or fall back to bySetPos
|
|
209
|
+
const dayCode = rule.byDay[0];
|
|
210
|
+
const dayMatch = dayCode.match(/^(-?\d+)?([A-Z]{2})$/);
|
|
211
|
+
const embeddedPos = dayMatch && dayMatch[1] ? parseInt(dayMatch[1], 10) : null;
|
|
212
|
+
const pos = embeddedPos || (rule.bySetPos && rule.bySetPos[0]) || 1;
|
|
213
|
+
this.setToWeekdayOfMonth(next, rule.byDay[0], pos);
|
|
159
214
|
} else {
|
|
160
215
|
// Same day of month
|
|
161
216
|
next.setMonth(next.getMonth() + rule.interval);
|