@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/README.md +64 -25
- package/jsconfig.json +12 -0
- package/package.json +17 -2
- package/src/event-resource.types.js +98 -0
- package/src/index.js +155 -524
- package/src/styles.css +171 -0
package/src/index.js
CHANGED
|
@@ -1,176 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {
|
|
3
|
-
* @
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
60
|
+
|
|
235
61
|
this.fetchEvents = options.fetchEvents || null;
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
680
|
-
* Uses
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
.
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
}
|