@chaaanito/event-resource-calendar 1.2.0 → 1.3.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/package.json +1 -1
- package/src/index.js +120 -25
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -7,7 +7,69 @@
|
|
|
7
7
|
* @typedef {import('./event-resource.types.js').CustomButton} CustomButton
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* EventResource
|
|
12
|
+
* A lightweight, high-performance vanilla JavaScript resource calendar library.
|
|
13
|
+
* Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
|
|
14
|
+
*/
|
|
10
15
|
export default class EventResource {
|
|
16
|
+
/**
|
|
17
|
+
* Initializes a new EventResource calendar instance, mounts it to the DOM, and fires initial render passes.
|
|
18
|
+
* @param {EventResourceOptions} options - Configuration parameters required to bootstrap the calendar matrix.
|
|
19
|
+
* @throws {Error} Throws if a valid `container` element, Node, or string selector cannot be resolved in the DOM.
|
|
20
|
+
* @example
|
|
21
|
+
* const calendar = new EventResource({
|
|
22
|
+
* // 1. Core Mount & State
|
|
23
|
+
* container: '#calendar-root',
|
|
24
|
+
* defaultView: 'daily',
|
|
25
|
+
* defaultDate: '2026-06-24', // Accepts string, number (epoch), or Date object
|
|
26
|
+
* * // 2. Structural UI Toggles
|
|
27
|
+
* showControls: true,
|
|
28
|
+
* stickyHeaders: true,
|
|
29
|
+
* * // 3. Grid Definitions (Static Fallbacks)
|
|
30
|
+
* rooms: [{ id: 'r1', name: 'Studio A', capacity: 10 }],
|
|
31
|
+
* timeSlots: [{ id: 't1', label: '09:00 AM' }],
|
|
32
|
+
* holidays: [{ date: '2026-12-25', name: 'Christmas Day' }],
|
|
33
|
+
* * // 4. Initial In-Memory Data
|
|
34
|
+
* initialEvents: [{
|
|
35
|
+
* id: 'evt-1',
|
|
36
|
+
* roomId: 'r1',
|
|
37
|
+
* timeId: 't1',
|
|
38
|
+
* title: 'Morning Sync',
|
|
39
|
+
* color: '#10b981'
|
|
40
|
+
* }],
|
|
41
|
+
* * // 5. Toolbar Extensions
|
|
42
|
+
* customButtons: [{
|
|
43
|
+
* label: 'Export PDF',
|
|
44
|
+
* className: 'bg-red-500 text-white',
|
|
45
|
+
* onClick: (e) => console.log('Exporting...', e)
|
|
46
|
+
* }],
|
|
47
|
+
* * // 6. Interaction Event Hooks
|
|
48
|
+
* onCellClick: (payload) => console.log('Empty slot clicked!'),
|
|
49
|
+
* onEventClick: (payload) => alert(`Clicked: ${payload.event.title}`),
|
|
50
|
+
* * // 7. Rich HTML Generation Intercepts
|
|
51
|
+
* renderRoomHeader: (room) => `<div>${room.name}</div>`,
|
|
52
|
+
* renderTimeSlotHeader: (slot) => `<div>${slot.label}</div>`,
|
|
53
|
+
* renderEvent: (event) => `<div>${event.title}</div>`,
|
|
54
|
+
* * // 8. Async Lifecycle Management (Draws skeleton first, then populates data)
|
|
55
|
+
* fetchRooms: async (date, view) => {
|
|
56
|
+
* const res = await fetch(`/api/rooms?date=${date.toISOString()}&view=${view}`);
|
|
57
|
+
* return await res.json();
|
|
58
|
+
* },
|
|
59
|
+
* fetchTimeSlots: async (date, view) => {
|
|
60
|
+
* const res = await fetch(`/api/timeslots?view=${view}`);
|
|
61
|
+
* return await res.json();
|
|
62
|
+
* },
|
|
63
|
+
* fetchHolidays: async (date, view) => {
|
|
64
|
+
* const res = await fetch(`/api/holidays?year=${date.getFullYear()}`);
|
|
65
|
+
* return await res.json();
|
|
66
|
+
* },
|
|
67
|
+
* fetchEvents: async (date, view) => {
|
|
68
|
+
* const res = await fetch(`/api/events?date=${date.toISOString()}&view=${view}`);
|
|
69
|
+
* return await res.json();
|
|
70
|
+
* }
|
|
71
|
+
* });
|
|
72
|
+
*/
|
|
11
73
|
constructor(options) {
|
|
12
74
|
let resolvedContainer = null;
|
|
13
75
|
|
|
@@ -92,7 +154,6 @@ export default class EventResource {
|
|
|
92
154
|
_buildEventsMap = () => {
|
|
93
155
|
this.eventsMap.clear();
|
|
94
156
|
for (const ev of this.events) {
|
|
95
|
-
// PERFORMANCE FIX: Use '::' as a delimiter in case user IDs contain hyphens
|
|
96
157
|
const key = `${ev.roomId}::${ev.timeId}`;
|
|
97
158
|
if (!this.eventsMap.has(key)) {
|
|
98
159
|
this.eventsMap.set(key, []);
|
|
@@ -101,17 +162,38 @@ export default class EventResource {
|
|
|
101
162
|
}
|
|
102
163
|
};
|
|
103
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Forces a chronological state shift, rewiring internal date parameters and firing synchronous/asynchronous re-render loops.
|
|
167
|
+
* @param {Date|string|number} newDate - REQUIRED: The new calendar baseline target date parameter.
|
|
168
|
+
* @returns {Promise<void>} Resolves automatically when the async data refetch and complete re-render lifecycle are complete.
|
|
169
|
+
* @example
|
|
170
|
+
* await calendar.setDate('2026-10-31');
|
|
171
|
+
*/
|
|
104
172
|
setDate = async (newDate) => {
|
|
105
173
|
this.currentDate = new Date(newDate);
|
|
106
|
-
await this.forceRender();
|
|
174
|
+
await this.forceRender();
|
|
107
175
|
};
|
|
108
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Mutates the application structural layout framework dynamically between daily and weekly granularities.
|
|
179
|
+
* @param {'daily'|'weekly'} newView - REQUIRED: The strict target structural mode selection string.
|
|
180
|
+
* @returns {Promise<void>} Resolves when the refetch, DOM teardown, and structural framework update complete.
|
|
181
|
+
* @example
|
|
182
|
+
* await calendar.setView('weekly');
|
|
183
|
+
*/
|
|
109
184
|
setView = async (newView) => {
|
|
110
185
|
if (this.currentView === newView) return;
|
|
111
186
|
this.currentView = newView;
|
|
112
187
|
await this.forceRender();
|
|
113
188
|
};
|
|
114
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Calculates timeline vector offsets based on the active view mode, steps the internal date parameter, and triggers reconciliations.
|
|
192
|
+
* @param {'prev'|'next'} direction - REQUIRED: The chronological vector direction keyword.
|
|
193
|
+
* @returns {Promise<void>} Resolves upon successful navigation and canvas redraw.
|
|
194
|
+
* @example
|
|
195
|
+
* await calendar.navigate('next');
|
|
196
|
+
*/
|
|
115
197
|
navigate = async (direction) => {
|
|
116
198
|
const daysToMove = this.currentView === "weekly" ? 7 : 1;
|
|
117
199
|
const multiplier = direction === "next" ? 1 : -1;
|
|
@@ -124,31 +206,53 @@ export default class EventResource {
|
|
|
124
206
|
|
|
125
207
|
// --- Public API ---
|
|
126
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Injects a raw configuration event into the application data state. Recompiles the collision map and forces a high-speed DOM update.
|
|
211
|
+
* @param {CalendarEvent} newEvent - REQUIRED: A valid object data map matching configuration structural specs exactly.
|
|
212
|
+
* @returns {void}
|
|
213
|
+
* @example
|
|
214
|
+
* calendar.addEvent({ id: 'evt-999', roomId: 'r1', timeId: 't1', title: 'Emergency Sync' });
|
|
215
|
+
*/
|
|
127
216
|
addEvent = (newEvent) => {
|
|
128
217
|
this.events.push(newEvent);
|
|
129
218
|
this._buildEventsMap();
|
|
130
|
-
// PERFORMANCE FIX: Only redraw the events, leave the layout untouched
|
|
131
219
|
this._renderEvents();
|
|
132
220
|
};
|
|
133
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Executes a hard delete across the internal event arrays based strictly on a uniquely matched id reference string.
|
|
224
|
+
* @param {string|number} eventId - REQUIRED: The exact, unique reference key index matching the target CalendarEvent.id.
|
|
225
|
+
* @returns {void}
|
|
226
|
+
* @example
|
|
227
|
+
* calendar.removeEvent('evt-999');
|
|
228
|
+
*/
|
|
134
229
|
removeEvent = (eventId) => {
|
|
135
230
|
this.events = this.events.filter((e) => e.id !== eventId);
|
|
136
231
|
this._buildEventsMap();
|
|
137
|
-
// PERFORMANCE FIX: Only redraw the events, leave the layout untouched
|
|
138
232
|
this._renderEvents();
|
|
139
233
|
};
|
|
140
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Wipes out all transient operational event records cached in client memory structures while preserving column rules and grid row configurations.
|
|
237
|
+
* @returns {void}
|
|
238
|
+
* @example
|
|
239
|
+
* calendar.clearAllEvents();
|
|
240
|
+
*/
|
|
141
241
|
clearAllEvents = () => {
|
|
142
242
|
this.events = [];
|
|
143
243
|
this.eventsMap.clear();
|
|
144
|
-
// PERFORMANCE FIX: Fast clear of events without layout thrashing
|
|
145
244
|
this._renderEvents();
|
|
146
245
|
};
|
|
147
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Triggers a comprehensive data replenishment cycle. Evaluates external fetch connectors, updates map caches, and rebuilds the visual DOM.
|
|
249
|
+
* @returns {Promise<void>} Resolves once all external data resolves and the DOM reconciliation concludes successfully.
|
|
250
|
+
* @example
|
|
251
|
+
* await calendar.forceRender();
|
|
252
|
+
*/
|
|
148
253
|
forceRender = async () => {
|
|
149
254
|
if (this.isFetching) return;
|
|
150
255
|
|
|
151
|
-
// 1. Draw empty skeleton instantly based on current memory
|
|
152
256
|
this.render();
|
|
153
257
|
|
|
154
258
|
const hasAsyncSources =
|
|
@@ -188,10 +292,17 @@ export default class EventResource {
|
|
|
188
292
|
}
|
|
189
293
|
|
|
190
294
|
this._buildEventsMap();
|
|
191
|
-
// 2. Re-render entirely to apply fetched layout rules and fetched events
|
|
192
295
|
this.render();
|
|
193
296
|
};
|
|
194
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Executes irreversible teardown routines protecting application host memory state cycles.
|
|
300
|
+
* Wipes parameters, drops listener closures, clears map caches, and securely unmounts UI sub-trees.
|
|
301
|
+
* @returns {void}
|
|
302
|
+
* @example
|
|
303
|
+
* calendar.destroy();
|
|
304
|
+
* calendar = null;
|
|
305
|
+
*/
|
|
195
306
|
destroy = () => {
|
|
196
307
|
this.events = [];
|
|
197
308
|
this.rooms = [];
|
|
@@ -299,17 +410,14 @@ export default class EventResource {
|
|
|
299
410
|
/**
|
|
300
411
|
* Manual render hook. Triggers a complete synchronization of the Skeleton UI and the Data Layer.
|
|
301
412
|
* @returns {void}
|
|
413
|
+
* @example
|
|
414
|
+
* calendar.render();
|
|
302
415
|
*/
|
|
303
416
|
render = () => {
|
|
304
417
|
this._renderSkeleton();
|
|
305
418
|
this._renderEvents();
|
|
306
419
|
};
|
|
307
420
|
|
|
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
421
|
_renderSkeleton = () => {
|
|
314
422
|
this.container.innerHTML = "";
|
|
315
423
|
|
|
@@ -362,14 +470,11 @@ export default class EventResource {
|
|
|
362
470
|
const cell = document.createElement("div");
|
|
363
471
|
cell.className = `er-grid-cell ${activeHoliday ? "er-holiday-cell" : ""}`;
|
|
364
472
|
|
|
365
|
-
// Inject queryable coordinates for high-speed DOM updates
|
|
366
473
|
cell.dataset.roomId = room.id;
|
|
367
474
|
cell.dataset.timeId = time.id;
|
|
368
475
|
|
|
369
476
|
cell.addEventListener("click", () => {
|
|
370
477
|
if (this.onCellClick) {
|
|
371
|
-
// PERFORMANCE FIX: Dynamically fetch current events at click time.
|
|
372
|
-
// Avoids needing to detach/reattach listeners when data changes.
|
|
373
478
|
const currentEvents =
|
|
374
479
|
this.eventsMap.get(`${room.id}::${time.id}`) || [];
|
|
375
480
|
|
|
@@ -406,25 +511,17 @@ export default class EventResource {
|
|
|
406
511
|
this.container.appendChild(wrapper);
|
|
407
512
|
};
|
|
408
513
|
|
|
409
|
-
/**
|
|
410
|
-
* PERFORMANCE FIX: Renders ONLY the event cards. Leaves the skeleton UI completely intact.
|
|
411
|
-
* Uses O(1) DOM targeting via data attributes.
|
|
412
|
-
* @private
|
|
413
|
-
*/
|
|
414
514
|
_renderEvents = () => {
|
|
415
|
-
// 1. Wipe only the existing event DOM nodes, leaving empty cells perfectly intact
|
|
416
515
|
const existingEvents = this.container.querySelectorAll(".er-event");
|
|
417
516
|
existingEvents.forEach((el) => el.remove());
|
|
418
517
|
|
|
419
518
|
const activeHoliday = this._getHolidayForDate(this.currentDate);
|
|
420
519
|
|
|
421
|
-
// 2. Map and inject fresh events into their specific coordinate cells
|
|
422
520
|
this.events.forEach((ev) => {
|
|
423
521
|
const cell = this.container.querySelector(
|
|
424
522
|
`[data-room-id="${ev.roomId}"][data-time-id="${ev.timeId}"]`,
|
|
425
523
|
);
|
|
426
524
|
|
|
427
|
-
// If cell doesn't exist (e.g., event is scheduled for a room not currently in view), skip rendering
|
|
428
525
|
if (!cell) return;
|
|
429
526
|
|
|
430
527
|
const eventDiv = document.createElement("div");
|
|
@@ -440,11 +537,9 @@ export default class EventResource {
|
|
|
440
537
|
eventDiv.addEventListener("click", (e) => {
|
|
441
538
|
e.stopPropagation();
|
|
442
539
|
if (this.onEventClick) {
|
|
443
|
-
// Dynamically fetch sibling events sharing this exact coordinate
|
|
444
540
|
const sharedEvents =
|
|
445
541
|
this.eventsMap.get(`${ev.roomId}::${ev.timeId}`) || [];
|
|
446
542
|
|
|
447
|
-
// Look up current row/col index dynamically based on UI position
|
|
448
543
|
const roomIndex = this.rooms.findIndex(
|
|
449
544
|
(r) => String(r.id) === String(ev.roomId),
|
|
450
545
|
);
|