@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/dist/force-calendar-interface.esm.js +112 -41
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +33 -33
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EventForm.js +15 -10
- package/src/core/EventBus.js +66 -0
- package/src/core/StateManager.js +92 -21
package/package.json
CHANGED
|
@@ -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 -
|
|
304
|
-
if (this.
|
|
305
|
-
|
|
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
|
|
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
|
|
package/src/core/EventBus.js
CHANGED
|
@@ -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
|
package/src/core/StateManager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
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
|
-
|
|
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
|
}
|