@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
- plugin.uninstall(this);
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
- const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
292
- const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
293
- return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
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
- next.setDate(rule.byMonthDay[0]); // Use first specified day
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
- this.setToWeekdayOfMonth(next, rule.byDay[0], rule.bySetPos[0] || 1);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",