@chaaanito/event-resource-calendar 1.0.1 → 1.2.0

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/src/index.js CHANGED
@@ -1,176 +1,18 @@
1
1
  /**
2
- * @typedef {Object} CalendarRoom
3
- * @description Defines a resource row within the calendar matrix grid layout.
4
- * @property {string|number} id - REQUIRED: Unique identifier for the room or resource. Must be absolutely unique across all room rows.
5
- * @property {string} name - REQUIRED: Fallback display title of the room/resource used if `renderRoomHeader` is omitted.
6
- * @property {*} [key: string] - OPTIONAL: Any additional open-ended properties used for custom filtering or rich rendering templates.
7
- * @example
8
- * // Example of a valid CalendarRoom object
9
- * { id: 'room-101', name: 'Conference Room A', capacity: 12, hasProjector: true }
2
+ * @typedef {import('./event-resource.types.js').EventResourceOptions} EventResourceOptions
3
+ * @typedef {import('./event-resource.types.js').CalendarRoom} CalendarRoom
4
+ * @typedef {import('./event-resource.types.js').TimeSlot} TimeSlot
5
+ * @typedef {import('./event-resource.types.js').CalendarEvent} CalendarEvent
6
+ * @typedef {import('./event-resource.types.js').CalendarHoliday} CalendarHoliday
7
+ * @typedef {import('./event-resource.types.js').CustomButton} CustomButton
10
8
  */
11
9
 
12
- /**
13
- * @typedef {Object} TimeSlot
14
- * @description Defines a timeline column structure partitioning the matrix grid workspace.
15
- * @property {string|number} id - REQUIRED: Unique identifier for the chronological slot. Must be unique across all columns.
16
- * @property {string} label - REQUIRED: Fallback timeline display text used if `renderTimeSlotHeader` is omitted.
17
- * @property {*} [key: string] - OPTIONAL: Any additional extensible properties used for custom filtering or rich rendering templates.
18
- * @example
19
- * // Example of a valid TimeSlot object
20
- * { id: 'slot-0900', label: '09:00 AM', isLunchHour: false }
21
- */
22
-
23
- /**
24
- * @typedef {Object} CalendarEvent
25
- * @description Represents an allocated timeline event mapped directly into a specific intersection cell.
26
- * @property {string|number} id - REQUIRED: Unique identifier for the scheduled event element.
27
- * @property {string|number} roomId - REQUIRED: Relational foreign key binding the item to a valid {@link CalendarRoom.id}.
28
- * @property {string|number} timeId - REQUIRED: Relational foreign key binding the item to a valid {@link TimeSlot.id}.
29
- * @property {string} title - REQUIRED: Plain-text title injected inside the DOM node element card.
30
- * @property {string} [color='#3b82f6'] - OPTIONAL: Valid CSS color value (hex, rgb, hsl, keyword) for the background tracking card.
31
- * @property {*} [key: string] - OPTIONAL: Custom meta data attributes parsed down to click event callback streams.
32
- * @example
33
- * // Example of a valid CalendarEvent object
34
- * { id: 'evt-1', roomId: 'room-101', timeId: 'slot-0900', title: 'Q3 Planning Sync', color: '#10b981', attendees: 5 }
35
- */
36
-
37
- /**
38
- * @typedef {Object} CalendarHoliday
39
- * @description Maps specific dates to holiday statuses, shifting backgrounds and appending context data to interaction payloads.
40
- * @property {string|Date|number} date - REQUIRED: Parsable temporal timestamp mapping the holiday milestone. Must be resolvable by `new Date()`.
41
- * @property {string} name - REQUIRED: The human-readable label injected into global notice badges and cell descriptors.
42
- * @property {*} [key: string] - OPTIONAL: Open-ended customer specific holiday data.
43
- * @example
44
- * // Example of a valid CalendarHoliday object
45
- * { date: '2026-12-25', name: 'Christmas Day', isCompanyPaid: true }
46
- */
47
-
48
- /**
49
- * @typedef {Object} CustomButton
50
- * @description Injectable client control appended directly onto the right-hand toolbar grouping matrix.
51
- * @property {string} label - REQUIRED: Text descriptor rendered inside the interactive control button element frame.
52
- * @property {function(MouseEvent): void} onClick - REQUIRED: Action handler fired immediately upon client interactions.
53
- * @property {string} [className] - OPTIONAL: Space-delimited functional CSS style modifiers for custom visual overrides.
54
- * @example
55
- * // Example of a valid CustomButton object
56
- * { label: 'Export PDF', className: 'bg-red-500 text-white', onClick: (e) => console.log('Exporting...', e) }
57
- */
58
-
59
- /**
60
- * @typedef {Object} ClickContextPayload
61
- * @description Consolidated operational telemetry shared universally across grid click response lifecycles.
62
- * @property {Date} date - Chronological state baseline actively mounted inside the viewport template frame.
63
- * @property {'daily'|'weekly'} view - Current structural mode index configuration.
64
- * @property {CalendarHoliday|null} holiday - Associated holiday data object if the current frame falls on a configured day milestone.
65
- * @property {Object} row - Track metadata coordinates.
66
- * @property {number} row.index - Vertical index array placement coordinate.
67
- * @property {CalendarRoom} row.data - Complete root object data context passed down from initialization.
68
- * @property {Object} col - Timeline metadata coordinates.
69
- * @property {number} col.index - Horizontal layout coordinate tracking indexes.
70
- * @property {TimeSlot} col.data - Complete root chronological object parameters.
71
- * @property {Object} cell - Operational target cell contents.
72
- * @property {string|number} cell.roomId - Unique cell row lookup index.
73
- * @property {string|number} cell.timeId - Unique cell column chronological coordinate index.
74
- * @property {CalendarEvent[]} cell.events - Contextual array containing all events currently occupying this grid location.
75
- */
76
-
77
- /**
78
- * @typedef {Object} EventClickPayload
79
- * @description Multi-layered payload shared exclusively with the `onEventClick` subscriber method.
80
- * @extends ClickContextPayload
81
- * @property {CalendarEvent} event - REQUIRED: The explicit, unique target event parameters bound to the clicked card element.
82
- * @property {MouseEvent} nativeEvent - REQUIRED: Raw browser click interaction data used for analytical intercept positioning or element tracking.
83
- */
84
-
85
- /**
86
- * @typedef {Object} EventResourceOptions
87
- * @description Input operational context map parsing standard parameters through class factories.
88
- * @property {string|HTMLElement|Node} container - REQUIRED: CSS selector engine target or explicit DOM pointer mount node.
89
- * @property {CalendarRoom[]} [rooms=[]] - OPTIONAL: Master source list establishing rows along the vertical plane matrix mapping layout.
90
- * @property {TimeSlot[]} [timeSlots=[]] - OPTIONAL: Master source list defining columns mapped along the horizontal path lane.
91
- * @property {CalendarEvent[]} [initialEvents=[]] - OPTIONAL: In-memory event array populating coordinates during setup initialization workflows.
92
- * @property {CalendarHoliday[]} [holidays=[]] - OPTIONAL: Array configuration identifying structural exceptions and specialized global days.
93
- * @property {CustomButton[]} [customButtons=[]] - OPTIONAL: Extensible collections rendering specialized tool structures within toolbars.
94
- * @property {boolean} [showControls=false] - OPTIONAL: Flag managing initialization visibility of structural management toolbars.
95
- * @property {boolean} [stickyHeaders=true] - OPTIONAL: Toggles CSS sticky double-axis tracking logic across layout headers on load.
96
- * @property {'daily'|'weekly'} [defaultView='daily'] - OPTIONAL: Default presentation structure layout selection state. Must be 'daily' or 'weekly'.
97
- * @property {Date|string|number} [defaultDate=new Date()] - OPTIONAL: Frame configuration locking starting lifecycle boundaries.
98
- * @property {function(ClickContextPayload): void} [onCellClick] - OPTIONAL: Interaction callback capturing clicks targeting empty coordinates.
99
- * @property {function(EventClickPayload): void} [onEventClick] - OPTIONAL: Interaction callback targeting allocated calendar card coordinates.
100
- * @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Data fetching intercept. Async method returning structural item sets.
101
- * @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for row slots.
102
- * @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for column slots.
103
- * @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept returning custom markup for individual event cards. Overrides default title text.
104
- * @example
105
- */
106
-
107
- /**
108
- * EventResource
109
- * A lightweight, high-performance vanilla JavaScript resource calendar library.
110
- * Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
111
- * @class
112
- */
113
10
  export default class EventResource {
114
- /**
115
- * Initializes a new EventResource calendar instance, mounts it to the DOM, and fires initial render passes.
116
- * @param {EventResourceOptions} options - Configuration parameters required to bootstrap the calendar matrix.
117
- * @throws {Error} Throws if a valid `container` element, Node, or string selector cannot be resolved in the DOM.
118
- * @example
119
- * const calendar = new EventResource({
120
- * // 1. Core Mount & State
121
- * container: '#calendar-root',
122
- * defaultView: 'daily',
123
- * defaultDate: '2026-06-24', // Accepts string, number (epoch), or Date object
124
- * * // 2. Structural UI Toggles
125
- * showControls: true,
126
- * stickyHeaders: true,
127
- * * // 3. Grid Definitions (Rows, Columns, and Exceptions)
128
- * rooms: [{ id: 'r1', name: 'Studio A', capacity: 10 }],
129
- * timeSlots: [{ id: 't1', label: '09:00 AM' }],
130
- * holidays: [{ date: '2026-12-25', name: 'Christmas Day' }],
131
- * * // 4. Initial In-Memory Data
132
- * initialEvents: [{
133
- * id: 'evt-1',
134
- * roomId: 'r1',
135
- * timeId: 't1',
136
- * title: 'Morning Sync',
137
- * color: '#10b981'
138
- * }],
139
- * * // 5. Toolbar Extensions
140
- * customButtons: [{
141
- * label: 'Export PDF',
142
- * className: 'bg-red-500 text-white hover:bg-red-600',
143
- * onClick: (e) => console.log('Triggering PDF generation...', e)
144
- * }],
145
- * * // 6. Interaction Event Hooks
146
- * onCellClick: (payload) => {
147
- * console.log(`Empty slot clicked! Room: ${payload.cell.roomId}, Time: ${payload.cell.timeId}`);
148
- * },
149
- * onEventClick: (payload) => {
150
- * alert(`Opening details for: ${payload.event.title}`);
151
- * },
152
- * * // 7. Rich HTML Generation Intercepts
153
- * renderRoomHeader: (room) => `<div class="p-2 border-b"><h3>${room.name}</h3><small>Cap: ${room.capacity}</small></div>`,
154
- * renderTimeSlotHeader: (slot) => `<div class="text-center font-bold text-gray-700">${slot.label}</div>`,
155
- * * // 8. Async Lifecycle Management
156
- * fetchEvents: async (date, view) => {
157
- * const res = await fetch(`/api/events?date=${date.toISOString()}&view=${view}`);
158
- * return await res.json();
159
- * }
160
- * });
161
- * renderEvent: (event) => `
162
- * <div class="flex flex-col gap-1 p-1">
163
- * <span class="font-bold text-xs truncate">${event.title}</span>
164
- * <span class="text-[10px] opacity-75">${event.roomName || 'TBD'}</span>
165
- * </div>
166
- * `
167
- */
168
11
  constructor(options) {
169
12
  let resolvedContainer = null;
170
13
 
171
14
  if (typeof options.container === "string") {
172
15
  resolvedContainer = document.querySelector(options.container);
173
-
174
16
  if (!resolvedContainer) {
175
17
  resolvedContainer =
176
18
  document.getElementById(options.container) ||
@@ -190,93 +32,54 @@ export default class EventResource {
190
32
 
191
33
  if (!resolvedContainer) {
192
34
  throw new Error(
193
- "EventResource: A valid container (CSS selector string, Node, or HTMLElement) is required and must exist in the DOM.",
35
+ "EventResource: A valid container is required and must exist in the DOM.",
194
36
  );
195
37
  }
196
38
 
197
- /**
198
- * @type {HTMLElement}
199
- * @description The verified root DOM node where the calendar is mounted.
200
- */
201
39
  this.container = resolvedContainer;
202
40
 
203
- // 2. Data Options
204
- /** @type {CalendarRoom[]} */
41
+ // Data Options
205
42
  this.rooms = options.rooms || [];
206
- /** @type {TimeSlot[]} */
207
43
  this.timeSlots = options.timeSlots || [];
208
- /** @type {CalendarEvent[]} */
209
44
  this.events = options.initialEvents || [];
210
- /** @type {CalendarHoliday[]} */
211
45
  this.holidays = options.holidays || [];
212
- /** @type {CustomButton[]} */
213
46
  this.customButtons = options.customButtons || [];
214
47
 
215
- // 3. UI Controls & State
216
- /** @type {boolean} */
48
+ // UI Controls & State
217
49
  this.showControls = options.showControls || false;
218
- /** @type {boolean} */
219
50
  this.stickyHeaders = options.stickyHeaders !== false;
220
- /** @type {'daily'|'weekly'} */
221
51
  this.currentView = options.defaultView || "daily";
222
- /** @type {Date} */
223
52
  this.currentDate = options.defaultDate
224
53
  ? new Date(options.defaultDate)
225
54
  : new Date();
226
- /** @type {boolean} */
227
55
  this.isFetching = false;
228
56
 
229
- // 4. Callbacks & Rich Formatting Helpers
230
- /** @type {function|null} */
57
+ // Callbacks & Async Fetchers
231
58
  this.onCellClick = options.onCellClick || null;
232
- /** @type {function|null} */
233
59
  this.onEventClick = options.onEventClick || null;
234
- /** @type {function|null} */
60
+
235
61
  this.fetchEvents = options.fetchEvents || null;
236
- /** @type {function|null} */
62
+ this.fetchRooms = options.fetchRooms || null;
63
+ this.fetchTimeSlots = options.fetchTimeSlots || null;
64
+ this.fetchHolidays = options.fetchHolidays || null;
65
+
237
66
  this.renderRoomHeader = options.renderRoomHeader || null;
238
- /** @type {function|null} */
239
67
  this.renderTimeSlotHeader = options.renderTimeSlotHeader || null;
240
- /** @type {function|null} */
241
68
  this.renderEvent = options.renderEvent || null;
242
69
 
243
- // 5. Initialization
244
- /**
245
- * @type {Map<string, CalendarEvent[]>}
246
- * @description Internal O(1) lookup map isolating events into cell clusters based on `roomId-timeId` keys.
247
- * @private
248
- */
249
70
  this.eventsMap = new Map();
250
- this._injectStyles();
251
71
  this._buildEventsMap();
72
+
252
73
  this.forceRender();
253
74
  }
254
75
 
255
76
  // --- State & Date Management ---
256
77
 
257
- /**
258
- * Transforms a multi-format date payload into a strictly normalized `YYYY-MM-DD` string for standardized comparison operations.
259
- * @param {Date|string|number} dateObj - REQUIRED: The temporal object, ISO string, or epoch timestamp to normalize.
260
- * @returns {string} The normalized local date string, formatted strictly as "YYYY-MM-DD".
261
- * @private
262
- * @example
263
- * // Returns "2026-06-24"
264
- * this._getNormalizedDateString(new Date('2026-06-24T12:00:00Z'));
265
- */
266
78
  _getNormalizedDateString = (dateObj) => {
267
79
  const d = new Date(dateObj);
268
80
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
269
81
  };
270
82
 
271
- /**
272
- * Scans the internal `holidays` array to determine if the provided date target matches a configured milestone.
273
- * @param {Date|string|number} dateObj - REQUIRED: The temporal target to evaluate against known holidays.
274
- * @returns {CalendarHoliday|null} Returns the exact matched holiday configuration object, or `null` if no match occurs.
275
- * @private
276
- * @example
277
- * const holiday = this._getHolidayForDate('2026-12-25');
278
- * if (holiday) console.log(holiday.name); // Logs "Christmas"
279
- */
280
83
  _getHolidayForDate = (dateObj) => {
281
84
  const targetDate = this._getNormalizedDateString(dateObj);
282
85
  return (
@@ -286,16 +89,11 @@ export default class EventResource {
286
89
  );
287
90
  };
288
91
 
289
- /**
290
- * Wipes and rebuilds the highly-optimized internal collision map. Groups flat event arrays into isolated index blocks.
291
- * Executes dynamically behind the scenes prior to every grid rendering phase to ensure data consistency.
292
- * @returns {void}
293
- * @private
294
- */
295
92
  _buildEventsMap = () => {
296
93
  this.eventsMap.clear();
297
94
  for (const ev of this.events) {
298
- const key = `${ev.roomId}-${ev.timeId}`;
95
+ // PERFORMANCE FIX: Use '::' as a delimiter in case user IDs contain hyphens
96
+ const key = `${ev.roomId}::${ev.timeId}`;
299
97
  if (!this.eventsMap.has(key)) {
300
98
  this.eventsMap.set(key, []);
301
99
  }
@@ -303,42 +101,17 @@ export default class EventResource {
303
101
  }
304
102
  };
305
103
 
306
- /**
307
- * Forces a chronological state shift, rewiring internal date parameters and firing synchronous asynchronous re-render loops.
308
- * @param {Date|string|number} newDate - REQUIRED: The new calendar baseline target date parameter.
309
- * @returns {Promise<void>} Resolves automatically when the async data refetch and complete re-render lifecycle are complete.
310
- * @example
311
- * // Jump directly to Halloween 2026
312
- * await calendar.setDate('2026-10-31');
313
- */
314
104
  setDate = async (newDate) => {
315
105
  this.currentDate = new Date(newDate);
316
- await this.forceRender();
106
+ await this.forceRender(); // Requires full rebuild to evaluate new holidays
317
107
  };
318
108
 
319
- /**
320
- * Mutates the application structural layout framework dynamically between daily and weekly granularities.
321
- * @param {'daily'|'weekly'} newView - REQUIRED: The strict target structural mode selection string.
322
- * @returns {Promise<void>} Resolves when the refetch, DOM teardown, and structural framework update complete.
323
- * @example
324
- * // Swap calendar into weekly perspective mode
325
- * await calendar.setView('weekly');
326
- */
327
109
  setView = async (newView) => {
328
110
  if (this.currentView === newView) return;
329
111
  this.currentView = newView;
330
112
  await this.forceRender();
331
113
  };
332
114
 
333
- /**
334
- * Calculates timeline vector offsets based on the currently active view mode, steps the internal date parameter, and triggers reconciliations.
335
- * Moves timeline by exactly 1 day for 'daily' views, or exactly 7 days for 'weekly' views.
336
- * @param {'prev'|'next'} direction - REQUIRED: The chronological vector direction keyword.
337
- * @returns {Promise<void>} Resolves upon successful navigation and canvas redraw.
338
- * @example
339
- * // Step forward in time based on current view step sizes
340
- * await calendar.navigate('next');
341
- */
342
115
  navigate = async (direction) => {
343
116
  const daysToMove = this.currentView === "weekly" ? 7 : 1;
344
117
  const multiplier = direction === "next" ? 1 : -1;
@@ -351,93 +124,74 @@ export default class EventResource {
351
124
 
352
125
  // --- Public API ---
353
126
 
354
- /**
355
- * Injects a raw configuration event into the application data state. Recompiles the collision map and forces a synchronous DOM update instantly.
356
- * @param {CalendarEvent} newEvent - REQUIRED: A valid object data map matching configuration structural specs exactly.
357
- * @returns {void}
358
- * @example
359
- * // Programmatically create a new event card
360
- * calendar.addEvent({
361
- * id: 'evt-999',
362
- * roomId: 'room-101',
363
- * timeId: 'slot-0900',
364
- * title: 'Ad-hoc Emergency Sync',
365
- * color: '#ef4444'
366
- * });
367
- */
368
127
  addEvent = (newEvent) => {
369
128
  this.events.push(newEvent);
370
129
  this._buildEventsMap();
371
- this.render();
130
+ // PERFORMANCE FIX: Only redraw the events, leave the layout untouched
131
+ this._renderEvents();
372
132
  };
373
133
 
374
- /**
375
- * Executes a hard delete across the internal event arrays based strictly on a uniquely matched id reference string. Rebuilds structures automatically.
376
- * @param {string|number} eventId - REQUIRED: The exact, unique reference key index matching the target {@link CalendarEvent.id}.
377
- * @returns {void}
378
- * @example
379
- * // Purge a specific event from the DOM and memory
380
- * calendar.removeEvent('evt-999');
381
- */
382
134
  removeEvent = (eventId) => {
383
135
  this.events = this.events.filter((e) => e.id !== eventId);
384
136
  this._buildEventsMap();
385
- this.render();
137
+ // PERFORMANCE FIX: Only redraw the events, leave the layout untouched
138
+ this._renderEvents();
386
139
  };
387
140
 
388
- /**
389
- * Wipes out all transient operational event records cached in client memory structures while preserving column rules and grid row configurations.
390
- * Excellent for resetting layouts between deep navigational transitions without destroying the core class instance.
391
- * @returns {void}
392
- * @example
393
- * // Clean the board completely
394
- * calendar.clearAllEvents();
395
- */
396
141
  clearAllEvents = () => {
397
142
  this.events = [];
398
143
  this.eventsMap.clear();
399
- this.render();
144
+ // PERFORMANCE FIX: Fast clear of events without layout thrashing
145
+ this._renderEvents();
400
146
  };
401
147
 
402
- /**
403
- * Triggers a comprehensive data replenishment cycle. Evaluates external `fetchEvents` connector methods, updates map caches, and rebuilds the visual DOM.
404
- * Implements internal `isFetching` lock mechanisms to protect against asynchronous data race conditions or parallel API execution.
405
- * @returns {Promise<void>} Resolves once all external data resolves and the DOM reconciliation concludes successfully.
406
- * @example
407
- * // Force a data refresh from the server
408
- * await calendar.forceRender();
409
- */
410
148
  forceRender = async () => {
411
149
  if (this.isFetching) return;
412
150
 
413
- if (typeof this.fetchEvents === "function") {
151
+ // 1. Draw empty skeleton instantly based on current memory
152
+ this.render();
153
+
154
+ const hasAsyncSources =
155
+ typeof this.fetchEvents === "function" ||
156
+ typeof this.fetchRooms === "function" ||
157
+ typeof this.fetchTimeSlots === "function" ||
158
+ typeof this.fetchHolidays === "function";
159
+
160
+ if (hasAsyncSources) {
414
161
  this.isFetching = true;
415
162
  try {
416
- const freshEvents = await this.fetchEvents(
417
- this.currentDate,
418
- this.currentView,
419
- );
163
+ const [freshEvents, freshRooms, freshTimeSlots, freshHolidays] =
164
+ await Promise.all([
165
+ typeof this.fetchEvents === "function"
166
+ ? this.fetchEvents(this.currentDate, this.currentView)
167
+ : Promise.resolve(this.events),
168
+ typeof this.fetchRooms === "function"
169
+ ? this.fetchRooms(this.currentDate, this.currentView)
170
+ : Promise.resolve(this.rooms),
171
+ typeof this.fetchTimeSlots === "function"
172
+ ? this.fetchTimeSlots(this.currentDate, this.currentView)
173
+ : Promise.resolve(this.timeSlots),
174
+ typeof this.fetchHolidays === "function"
175
+ ? this.fetchHolidays(this.currentDate, this.currentView)
176
+ : Promise.resolve(this.holidays),
177
+ ]);
178
+
420
179
  this.events = freshEvents || [];
180
+ this.rooms = freshRooms || [];
181
+ this.timeSlots = freshTimeSlots || [];
182
+ this.holidays = freshHolidays || [];
421
183
  } catch (error) {
422
- console.error("EventResource: Failed to refetch events.", error);
184
+ console.error("EventResource: Failed to refetch calendar data.", error);
423
185
  } finally {
424
186
  this.isFetching = false;
425
187
  }
426
188
  }
427
189
 
428
190
  this._buildEventsMap();
191
+ // 2. Re-render entirely to apply fetched layout rules and fetched events
429
192
  this.render();
430
193
  };
431
194
 
432
- /**
433
- * Executes irreversible teardown routines protecting application host memory state cycles.
434
- * Wipes parameters, drops listener closures, clears map caches, and securely unmounts UI sub-trees from the primary node without shattering external reactive bindings.
435
- * @returns {void}
436
- * @example
437
- * // Unmount calendar gracefully
438
- * calendar.destroy();
439
- * calendar = null;
440
- */
441
195
  destroy = () => {
442
196
  this.events = [];
443
197
  this.rooms = [];
@@ -448,6 +202,9 @@ export default class EventResource {
448
202
  this.onCellClick = null;
449
203
  this.onEventClick = null;
450
204
  this.fetchEvents = null;
205
+ this.fetchRooms = null;
206
+ this.fetchTimeSlots = null;
207
+ this.fetchHolidays = null;
451
208
  this.renderRoomHeader = null;
452
209
  this.renderTimeSlotHeader = null;
453
210
 
@@ -461,20 +218,10 @@ export default class EventResource {
461
218
 
462
219
  // --- DOM Creation & Rendering ---
463
220
 
464
- /**
465
- * Generates and mounts the sophisticated navigation, date-selection, and view toggle toolbar components inside the DOM container.
466
- * @param {HTMLElement} wrapper - REQUIRED: The main root layout frame container reference node where the toolbar will be injected.
467
- * @returns {void}
468
- * @private
469
- * @example
470
- * // Internal execution call structure
471
- * this._renderToolbar(wrapperElementNode);
472
- */
473
221
  _renderToolbar = (wrapper) => {
474
222
  const toolbar = document.createElement("div");
475
223
  toolbar.className = "er-toolbar";
476
224
 
477
- // Left: Navigation Controls
478
225
  const navGroup = document.createElement("div");
479
226
  navGroup.className = "er-toolbar-group";
480
227
 
@@ -493,7 +240,6 @@ export default class EventResource {
493
240
  btnNext.textContent = "▶";
494
241
  btnNext.onclick = () => this.navigate("next");
495
242
 
496
- // Date Picker HTML5 Input Integration
497
243
  const datePicker = document.createElement("input");
498
244
  datePicker.type = "date";
499
245
  datePicker.className = "er-date-picker";
@@ -506,11 +252,9 @@ export default class EventResource {
506
252
 
507
253
  navGroup.append(btnPrev, btnToday, btnNext, datePicker);
508
254
 
509
- // Right: View Framework Mode Toggles & Freezing Management Elements
510
255
  const viewGroup = document.createElement("div");
511
256
  viewGroup.className = "er-toolbar-group";
512
257
 
513
- // Append Client Extensible Custom Action Elements
514
258
  this.customButtons.forEach((btnConfig) => {
515
259
  const customBtn = document.createElement("button");
516
260
  customBtn.className = `er-btn ${btnConfig.className || ""}`.trim();
@@ -530,7 +274,6 @@ export default class EventResource {
530
274
  btnWeekly.onclick = () => this.setView("weekly");
531
275
 
532
276
  viewGroup.append(btnDaily, btnWeekly);
533
-
534
277
  toolbar.append(navGroup, viewGroup);
535
278
 
536
279
  const btnFreeze = document.createElement("button");
@@ -542,7 +285,6 @@ export default class EventResource {
542
285
  };
543
286
  navGroup.appendChild(btnFreeze);
544
287
 
545
- // Active Holiday Indicator Check
546
288
  const currentHoliday = this._getHolidayForDate(this.currentDate);
547
289
  if (currentHoliday) {
548
290
  const holidayBadge = document.createElement("span");
@@ -555,15 +297,20 @@ export default class EventResource {
555
297
  };
556
298
 
557
299
  /**
558
- * Master execution engine responsible for parsing memory arrays into physical layout nodes.
559
- * Builds coordinate grid intersections, mounts event cards, appends propagation intercepts, maps CSS grid templates dynamically, and finalizes DOM commitments.
300
+ * Manual render hook. Triggers a complete synchronization of the Skeleton UI and the Data Layer.
560
301
  * @returns {void}
561
- * @private
562
- * @example
563
- * // Synchronous UI redraw
564
- * this.render();
565
302
  */
566
303
  render = () => {
304
+ this._renderSkeleton();
305
+ this._renderEvents();
306
+ };
307
+
308
+ /**
309
+ * PERFORMANCE FIX: Renders ONLY the static layout (rows, columns, headers, empty cells).
310
+ * Never touches actual event data or cards.
311
+ * @private
312
+ */
313
+ _renderSkeleton = () => {
567
314
  this.container.innerHTML = "";
568
315
 
569
316
  const wrapper = document.createElement("div");
@@ -578,13 +325,12 @@ export default class EventResource {
578
325
 
579
326
  const grid = document.createElement("div");
580
327
  grid.className = `er-grid ${this.stickyHeaders ? "er-sticky" : ""}`.trim();
581
- grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length}, minmax(120px, 1fr))`;
328
+ grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length || 1}, minmax(120px, 1fr))`;
582
329
 
583
330
  const corner = document.createElement("div");
584
331
  corner.className = "er-header-cell er-corner";
585
332
  grid.appendChild(corner);
586
333
 
587
- // Column Map Processing Loop
588
334
  this.timeSlots.forEach((time) => {
589
335
  const timeHeader = document.createElement("div");
590
336
  timeHeader.className = "er-header-cell er-time-header";
@@ -600,7 +346,6 @@ export default class EventResource {
600
346
 
601
347
  const activeHoliday = this._getHolidayForDate(this.currentDate);
602
348
 
603
- // Row Matrix Layout Intercept Processing Loops
604
349
  this.rooms.forEach((room, rowIndex) => {
605
350
  const roomHeader = document.createElement("div");
606
351
  roomHeader.className = "er-header-cell er-room-header";
@@ -617,228 +362,114 @@ export default class EventResource {
617
362
  const cell = document.createElement("div");
618
363
  cell.className = `er-grid-cell ${activeHoliday ? "er-holiday-cell" : ""}`;
619
364
 
620
- const cellEvents = this.eventsMap.get(`${room.id}-${time.id}`) || [];
365
+ // Inject queryable coordinates for high-speed DOM updates
366
+ cell.dataset.roomId = room.id;
367
+ cell.dataset.timeId = time.id;
621
368
 
622
369
  cell.addEventListener("click", () => {
623
370
  if (this.onCellClick) {
371
+ // PERFORMANCE FIX: Dynamically fetch current events at click time.
372
+ // Avoids needing to detach/reattach listeners when data changes.
373
+ const currentEvents =
374
+ this.eventsMap.get(`${room.id}::${time.id}`) || [];
375
+
624
376
  this.onCellClick({
625
377
  date: this.currentDate,
626
378
  view: this.currentView,
627
379
  holiday: activeHoliday,
628
380
  row: { index: rowIndex, data: room },
629
381
  col: { index: colIndex, data: time },
630
- cell: { roomId: room.id, timeId: time.id, events: cellEvents },
382
+ cell: { roomId: room.id, timeId: time.id, events: currentEvents },
631
383
  });
632
384
  }
633
385
  });
634
386
 
635
- cellEvents.forEach((ev) => {
636
- const eventDiv = document.createElement("div");
637
- eventDiv.className = "er-event";
638
-
639
- // Apply base background color, allowing CSS/Tailwind classes inside the custom HTML to inherit or override it.
640
- eventDiv.style.backgroundColor = ev.color || "#3b82f6";
641
-
642
- // Intercept rendering if the custom hook exists
643
- if (typeof this.renderEvent === "function") {
644
- eventDiv.innerHTML = this.renderEvent(ev);
645
- } else {
646
- eventDiv.textContent = ev.title;
647
- }
648
-
649
- eventDiv.addEventListener("click", (e) => {
650
- e.stopPropagation();
651
-
652
- if (this.onEventClick) {
653
- this.onEventClick({
654
- event: ev,
655
- nativeEvent: e,
656
- date: this.currentDate,
657
- view: this.currentView,
658
- holiday: activeHoliday,
659
- row: { index: rowIndex, data: room },
660
- col: { index: colIndex, data: time },
661
- cell: { roomId: room.id, timeId: time.id, events: cellEvents },
662
- });
663
- }
664
- });
665
-
666
- cell.appendChild(eventDiv);
667
- });
668
-
669
387
  grid.appendChild(cell);
670
388
  });
671
389
  });
672
390
 
391
+ if (
392
+ this.rooms.length === 0 &&
393
+ this.timeSlots.length === 0 &&
394
+ this.isFetching
395
+ ) {
396
+ const loadingIndicator = document.createElement("div");
397
+ loadingIndicator.style.padding = "20px";
398
+ loadingIndicator.style.color = "#6b7280";
399
+ loadingIndicator.style.textAlign = "center";
400
+ loadingIndicator.textContent = "Loading grid data...";
401
+ grid.appendChild(loadingIndicator);
402
+ }
403
+
673
404
  gridWrapper.appendChild(grid);
674
405
  wrapper.appendChild(gridWrapper);
675
406
  this.container.appendChild(wrapper);
676
407
  };
677
408
 
678
409
  /**
679
- * Installs required native structural CSS behaviors directly into the `<head>` of the underlying document frame.
680
- * Uses a hardcoded ID lookup to ensure injection occurs exactly once per session globally, preventing DOM bloat across multiple library invocations.
681
- * @returns {void}
410
+ * PERFORMANCE FIX: Renders ONLY the event cards. Leaves the skeleton UI completely intact.
411
+ * Uses O(1) DOM targeting via data attributes.
682
412
  * @private
683
- * @example
684
- * // Automatic execution during construction
685
- * this._injectStyles();
686
413
  */
687
- _injectStyles = () => {
688
- if (document.getElementById("er-library-styles")) return;
689
-
690
- const style = document.createElement("style");
691
- style.id = "er-library-styles";
692
- style.textContent = `
693
- .er-container {
694
- font-family: system-ui, -apple-system, sans-serif;
695
- border: 1px solid #e5e7eb;
696
- border-radius: 8px;
697
- background: #fff;
698
- width: 100%;
699
- display: flex;
700
- flex-direction: column;
701
- }
702
- .er-toolbar {
703
- display: flex;
704
- justify-content: space-between;
705
- align-items: center;
706
- padding: 12px 16px;
707
- border-bottom: 1px solid #e5e7eb;
708
- background: #f9fafb;
709
- border-radius: 8px 8px 0 0;
710
- flex-wrap: wrap;
711
- gap: 12px;
712
- }
713
- .er-toolbar-group {
714
- display: flex;
715
- align-items: center;
716
- gap: 8px;
717
- }
718
- .er-btn {
719
- padding: 6px 12px;
720
- background: #fff;
721
- border: 1px solid #d1d5db;
722
- border-radius: 6px;
723
- cursor: pointer;
724
- font-size: 0.875rem;
725
- font-weight: 500;
726
- color: #374151;
727
- transition: all 0.2s;
728
- }
729
- .er-btn:hover { background: #f3f4f6; }
730
- .er-btn.active {
731
- background: #e0e7ff;
732
- color: #4f46e5;
733
- border-color: #c7d2fe;
734
- }
735
- .er-date-picker {
736
- padding: 5px 10px;
737
- border: 1px solid #d1d5db;
738
- border-radius: 6px;
739
- font-family: inherit;
740
- font-size: 0.875rem;
741
- color: #374151;
742
- background: #fff;
743
- cursor: pointer;
744
- }
745
- .er-holiday-badge {
746
- font-size: 0.875rem;
747
- font-weight: 600;
748
- color: #059669;
749
- background: #d1fae5;
750
- padding: 4px 10px;
751
- border-radius: 9999px;
752
- margin-left: 8px;
753
- }
754
- .er-grid-wrapper {
755
- overflow: auto;
756
- max-height: 65vh;
757
- width: 100%;
758
- }
759
- .er-grid {
760
- display: grid;
761
- grid-auto-rows: minmax(60px, auto);
762
- }
763
- .er-header-cell {
764
- padding: 12px;
765
- font-size: 0.875rem;
766
- background: #f9fafb;
767
- border-bottom: 1px solid #e5e7eb;
768
- border-right: 1px solid #e5e7eb;
769
- display: flex;
770
- align-items: center;
771
- box-sizing: border-box;
772
- }
773
-
774
- .er-grid.er-sticky .er-corner {
775
- position: sticky;
776
- top: 0;
777
- left: 0;
778
- z-index: 3;
779
- background: #f9fafb;
780
- }
781
- .er-grid.er-sticky .er-time-header {
782
- position: sticky;
783
- top: 0;
784
- z-index: 2;
785
- background: #f9fafb;
786
- justify-content: center;
787
- color: #4b5563;
788
- }
789
- .er-grid.er-sticky .er-room-header {
790
- position: sticky;
791
- left: 0;
792
- z-index: 2;
793
- background: #f9fafb;
794
- justify-content: flex-start;
795
- }
796
-
797
- .er-grid:not(.er-sticky) .er-corner,
798
- .er-grid:not(.er-sticky) .er-time-header,
799
- .er-grid:not(.er-sticky) .er-room-header {
800
- position: static;
801
- background: #f9fafb;
802
- }
803
- .er-grid:not(.er-sticky) .er-time-header { justify-content: center; }
804
-
805
- .er-grid-cell {
806
- border-bottom: 1px solid #e5e7eb;
807
- border-right: 1px solid #e5e7eb;
808
- padding: 4px;
809
- transition: background-color 0.2s;
810
- cursor: pointer;
811
- display: flex;
812
- flex-direction: column;
813
- gap: 4px;
814
- box-sizing: border-box;
815
- }
816
- .er-grid-cell:hover { background-color: #f3f4f6; }
817
- .er-holiday-cell { background-color: #fdfbf7; }
818
- .er-holiday-cell:hover { background-color: #fef3c7; }
819
- .er-event {
820
- padding: 4px 8px;
821
- border-radius: 4px;
822
- color: white;
823
- font-size: 0.75rem;
824
- font-weight: 500;
825
- white-space: nowrap;
826
- overflow: hidden;
827
- text-overflow: ellipsis;
828
- cursor: pointer;
829
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
830
- }
831
- .er-event:hover { transform: scale(1.02); z-index: 10; }
832
-
833
- .er-rich-wrapper {
834
- display: flex;
835
- flex-direction: column;
836
- width: 100%;
837
- gap: 2px;
838
- }
839
- .er-rich-title { font-weight: 600; color: #111827; }
840
- .er-rich-subtitle { font-size: 0.75rem; color: #6b7280; font-weight: 400; }
841
- `;
842
- document.head.appendChild(style);
414
+ _renderEvents = () => {
415
+ // 1. Wipe only the existing event DOM nodes, leaving empty cells perfectly intact
416
+ const existingEvents = this.container.querySelectorAll(".er-event");
417
+ existingEvents.forEach((el) => el.remove());
418
+
419
+ const activeHoliday = this._getHolidayForDate(this.currentDate);
420
+
421
+ // 2. Map and inject fresh events into their specific coordinate cells
422
+ this.events.forEach((ev) => {
423
+ const cell = this.container.querySelector(
424
+ `[data-room-id="${ev.roomId}"][data-time-id="${ev.timeId}"]`,
425
+ );
426
+
427
+ // If cell doesn't exist (e.g., event is scheduled for a room not currently in view), skip rendering
428
+ if (!cell) return;
429
+
430
+ const eventDiv = document.createElement("div");
431
+ eventDiv.className = "er-event";
432
+ eventDiv.style.backgroundColor = ev.color || "#3b82f6";
433
+
434
+ if (typeof this.renderEvent === "function") {
435
+ eventDiv.innerHTML = this.renderEvent(ev);
436
+ } else {
437
+ eventDiv.textContent = ev.title;
438
+ }
439
+
440
+ eventDiv.addEventListener("click", (e) => {
441
+ e.stopPropagation();
442
+ if (this.onEventClick) {
443
+ // Dynamically fetch sibling events sharing this exact coordinate
444
+ const sharedEvents =
445
+ this.eventsMap.get(`${ev.roomId}::${ev.timeId}`) || [];
446
+
447
+ // Look up current row/col index dynamically based on UI position
448
+ const roomIndex = this.rooms.findIndex(
449
+ (r) => String(r.id) === String(ev.roomId),
450
+ );
451
+ const timeIndex = this.timeSlots.findIndex(
452
+ (t) => String(t.id) === String(ev.timeId),
453
+ );
454
+
455
+ this.onEventClick({
456
+ event: ev,
457
+ nativeEvent: e,
458
+ date: this.currentDate,
459
+ view: this.currentView,
460
+ holiday: activeHoliday,
461
+ row: { index: roomIndex, data: this.rooms[roomIndex] },
462
+ col: { index: timeIndex, data: this.timeSlots[timeIndex] },
463
+ cell: {
464
+ roomId: ev.roomId,
465
+ timeId: ev.timeId,
466
+ events: sharedEvents,
467
+ },
468
+ });
469
+ }
470
+ });
471
+
472
+ cell.appendChild(eventDiv);
473
+ });
843
474
  };
844
475
  }