@forcecalendar/interface 1.0.15 → 1.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/interface",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
6
6
  "main": "dist/force-calendar-interface.umd.js",
@@ -300,16 +300,16 @@ export class EventForm extends BaseComponent {
300
300
  if (e.target === this) this.close();
301
301
  });
302
302
 
303
- // Close on Escape key - remove old listener before adding new one
304
- if (this._handleKeyDown) {
305
- window.removeEventListener('keydown', this._handleKeyDown);
303
+ // Close on Escape key - only add once to prevent memory leaks
304
+ if (!this._keydownListenerAdded) {
305
+ this._handleKeyDown = (e) => {
306
+ if (e.key === 'Escape' && this.hasAttribute('open')) {
307
+ this.close();
308
+ }
309
+ };
310
+ window.addEventListener('keydown', this._handleKeyDown);
311
+ this._keydownListenerAdded = true;
306
312
  }
307
- this._handleKeyDown = (e) => {
308
- if (e.key === 'Escape' && this.hasAttribute('open')) {
309
- this.close();
310
- }
311
- };
312
- window.addEventListener('keydown', this._handleKeyDown);
313
313
  }
314
314
 
315
315
  updateColorSelection() {
@@ -410,7 +410,12 @@ export class EventForm extends BaseComponent {
410
410
  if (this._cleanupFocusTrap) {
411
411
  this._cleanupFocusTrap();
412
412
  }
413
- window.removeEventListener('keydown', this._handleKeyDown);
413
+ // Clean up window listener
414
+ if (this._handleKeyDown) {
415
+ window.removeEventListener('keydown', this._handleKeyDown);
416
+ this._handleKeyDown = null;
417
+ this._keydownListenerAdded = false;
418
+ }
414
419
  }
415
420
  }
416
421
 
@@ -58,6 +58,17 @@ class EventBus {
58
58
  * Unsubscribe from an event
59
59
  */
60
60
  off(eventName, handler) {
61
+ // Handle wildcard pattern removal
62
+ if (eventName.includes('*')) {
63
+ for (const sub of this.wildcardHandlers) {
64
+ if (sub.pattern === eventName && sub.handler === handler) {
65
+ this.wildcardHandlers.delete(sub);
66
+ return;
67
+ }
68
+ }
69
+ return;
70
+ }
71
+
61
72
  if (!this.events.has(eventName)) return;
62
73
 
63
74
  const handlers = this.events.get(eventName);
@@ -71,6 +82,43 @@ class EventBus {
71
82
  }
72
83
  }
73
84
 
85
+ /**
86
+ * Remove all wildcard handlers matching a pattern
87
+ * @param {string} pattern - Pattern to match (e.g., 'event:*')
88
+ */
89
+ offWildcard(pattern) {
90
+ for (const sub of [...this.wildcardHandlers]) {
91
+ if (sub.pattern === pattern) {
92
+ this.wildcardHandlers.delete(sub);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Remove all handlers (regular and wildcard) for a specific handler function
99
+ * Useful for cleanup when a component is destroyed
100
+ * @param {Function} handler - Handler function to remove
101
+ */
102
+ offAll(handler) {
103
+ // Remove from regular events
104
+ for (const [eventName, handlers] of this.events) {
105
+ const index = handlers.findIndex(sub => sub.handler === handler);
106
+ if (index > -1) {
107
+ handlers.splice(index, 1);
108
+ }
109
+ if (handlers.length === 0) {
110
+ this.events.delete(eventName);
111
+ }
112
+ }
113
+
114
+ // Remove from wildcard handlers
115
+ for (const sub of [...this.wildcardHandlers]) {
116
+ if (sub.handler === handler) {
117
+ this.wildcardHandlers.delete(sub);
118
+ }
119
+ }
120
+ }
121
+
74
122
  /**
75
123
  * Emit an event
76
124
  * @param {string} eventName - Event name
@@ -157,6 +205,24 @@ class EventBus {
157
205
  getHandlerCount(eventName) {
158
206
  return this.events.has(eventName) ? this.events.get(eventName).length : 0;
159
207
  }
208
+
209
+ /**
210
+ * Get wildcard handler count
211
+ */
212
+ getWildcardHandlerCount() {
213
+ return this.wildcardHandlers.size;
214
+ }
215
+
216
+ /**
217
+ * Get total handler count (for debugging/monitoring)
218
+ */
219
+ getTotalHandlerCount() {
220
+ let count = this.wildcardHandlers.size;
221
+ for (const handlers of this.events.values()) {
222
+ count += handlers.length;
223
+ }
224
+ return count;
225
+ }
160
226
  }
161
227
 
162
228
  // Create singleton instance
@@ -39,6 +39,32 @@ class StateManager {
39
39
  this.subscribe = this.subscribe.bind(this);
40
40
  this.unsubscribe = this.unsubscribe.bind(this);
41
41
  this.setState = this.setState.bind(this);
42
+
43
+ // Initial sync of events from Core (in case events were pre-loaded)
44
+ this._syncEventsFromCore({ silent: true });
45
+ }
46
+
47
+ /**
48
+ * Sync state.events from Core calendar (single source of truth)
49
+ * This ensures state.events always matches Core's event store
50
+ */
51
+ _syncEventsFromCore(options = {}) {
52
+ const coreEvents = this.calendar.getEvents() || [];
53
+ // Only update if different to avoid unnecessary re-renders
54
+ if (this.state.events.length !== coreEvents.length ||
55
+ !this._eventsMatch(this.state.events, coreEvents)) {
56
+ this.setState({ events: [...coreEvents] }, options);
57
+ }
58
+ return coreEvents;
59
+ }
60
+
61
+ /**
62
+ * Check if two event arrays have the same events (by id)
63
+ */
64
+ _eventsMatch(arr1, arr2) {
65
+ if (arr1.length !== arr2.length) return false;
66
+ const ids1 = new Set(arr1.map(e => e.id));
67
+ return arr2.every(e => ids1.has(e.id));
42
68
  }
43
69
 
44
70
  // State management
@@ -59,13 +85,50 @@ class StateManager {
59
85
  return this.state;
60
86
  }
61
87
 
62
- subscribe(callback) {
88
+ subscribe(callback, subscriberId = null) {
63
89
  this.subscribers.add(callback);
64
- return () => this.unsubscribe(callback);
90
+
91
+ // Track subscriber ID for debugging/cleanup
92
+ if (subscriberId) {
93
+ if (!this._subscriberIds) {
94
+ this._subscriberIds = new Map();
95
+ }
96
+ this._subscriberIds.set(subscriberId, callback);
97
+ }
98
+
99
+ return () => this.unsubscribe(callback, subscriberId);
65
100
  }
66
101
 
67
- unsubscribe(callback) {
102
+ unsubscribe(callback, subscriberId = null) {
68
103
  this.subscribers.delete(callback);
104
+
105
+ // Clean up ID tracking
106
+ if (subscriberId && this._subscriberIds) {
107
+ this._subscriberIds.delete(subscriberId);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Unsubscribe by subscriber ID
113
+ * @param {string} subscriberId - ID used when subscribing
114
+ */
115
+ unsubscribeById(subscriberId) {
116
+ if (!this._subscriberIds) return false;
117
+
118
+ const callback = this._subscriberIds.get(subscriberId);
119
+ if (callback) {
120
+ this.subscribers.delete(callback);
121
+ this._subscriberIds.delete(subscriberId);
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * Get subscriber count (for debugging/monitoring)
129
+ */
130
+ getSubscriberCount() {
131
+ return this.subscribers.size;
69
132
  }
70
133
 
71
134
  notifySubscribers(oldState, newState) {
@@ -150,14 +213,16 @@ class StateManager {
150
213
  eventBus.emit('event:error', { action: 'add', event, error: 'Failed to add event' });
151
214
  return null;
152
215
  }
153
- // Create new array to avoid mutation before setState
154
- const newEvents = [...this.state.events, addedEvent];
155
- this.setState({ events: newEvents });
216
+ // Sync from Core to ensure consistency (single source of truth)
217
+ this._syncEventsFromCore();
156
218
  eventBus.emit('event:added', { event: addedEvent });
157
219
  return addedEvent;
158
220
  }
159
221
 
160
222
  updateEvent(eventId, updates) {
223
+ // First, ensure state is in sync with Core (recover from any prior desync)
224
+ this._syncEventsFromCore({ silent: true });
225
+
161
226
  const event = this.calendar.updateEvent(eventId, updates);
162
227
  if (!event) {
163
228
  console.error(`Failed to update event: ${eventId}`);
@@ -165,37 +230,39 @@ class StateManager {
165
230
  return null;
166
231
  }
167
232
 
168
- const index = this.state.events.findIndex(e => e.id === eventId);
169
- if (index === -1) {
170
- console.error(`Event ${eventId} not found in state`);
171
- eventBus.emit('event:error', { action: 'update', eventId, error: 'Event not found in state' });
172
- return null;
173
- }
174
-
175
- // Create new array to avoid mutation before setState
176
- const newEvents = [...this.state.events];
177
- newEvents[index] = event;
178
- this.setState({ events: newEvents });
233
+ // Sync from Core to ensure consistency (single source of truth)
234
+ this._syncEventsFromCore();
179
235
  eventBus.emit('event:updated', { event });
180
236
  return event;
181
237
  }
182
238
 
183
239
  deleteEvent(eventId) {
240
+ // First, ensure state is in sync with Core (recover from any prior desync)
241
+ this._syncEventsFromCore({ silent: true });
242
+
184
243
  const deleted = this.calendar.removeEvent(eventId);
185
244
  if (!deleted) {
186
245
  console.error(`Failed to delete event: ${eventId}`);
187
246
  eventBus.emit('event:error', { action: 'delete', eventId, error: 'Event not found' });
188
247
  return false;
189
248
  }
190
- // Create new array to avoid mutation before setState
191
- const newEvents = this.state.events.filter(e => e.id !== eventId);
192
- this.setState({ events: newEvents });
249
+ // Sync from Core to ensure consistency (single source of truth)
250
+ this._syncEventsFromCore();
193
251
  eventBus.emit('event:deleted', { eventId });
194
252
  return true;
195
253
  }
196
254
 
197
255
  getEvents() {
198
- return this.calendar.getEvents();
256
+ // Return from Core (source of truth)
257
+ return this.calendar.getEvents() || [];
258
+ }
259
+
260
+ /**
261
+ * Force sync state.events from Core calendar
262
+ * Use this if you've modified events directly on the Core calendar
263
+ */
264
+ syncEvents() {
265
+ return this._syncEventsFromCore();
199
266
  }
200
267
 
201
268
  getEventsForDate(date) {
@@ -332,6 +399,10 @@ class StateManager {
332
399
  // Destroy
333
400
  destroy() {
334
401
  this.subscribers.clear();
402
+ if (this._subscriberIds) {
403
+ this._subscriberIds.clear();
404
+ this._subscriberIds = null;
405
+ }
335
406
  this.state = null;
336
407
  this.calendar = null;
337
408
  }