@chaaanito/event-resource-calendar 1.0.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.
Files changed (3) hide show
  1. package/README.md +119 -0
  2. package/package.json +9 -0
  3. package/src/index.js +844 -0
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # EventResource
2
+
3
+ A lightweight, high-performance vanilla JavaScript resource calendar library. Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
4
+
5
+ ## Installation
6
+
7
+ \`\`\`bash
8
+ npm install event-resource-calendar
9
+ \`\`\`
10
+
11
+ ## Quick Start
12
+
13
+ \`\`\`javascript
14
+ import EventResource from 'event-resource-calendar';
15
+
16
+ const calendar = new EventResource({
17
+ container: '#calendar-root',
18
+ defaultView: 'daily',
19
+ defaultDate: '2026-06-24',
20
+ showControls: true,
21
+ stickyHeaders: true,
22
+ rooms: [{ id: 'r1', name: 'Studio A', capacity: 10 }],
23
+ timeSlots: [{ id: 't1', label: '09:00 AM' }],
24
+ initialEvents: [{
25
+ id: 'evt-1',
26
+ roomId: 'r1',
27
+ timeId: 't1',
28
+ title: 'Morning Sync',
29
+ color: '#10b981'
30
+ }]
31
+ });
32
+ \`\`\`
33
+
34
+ ## Configuration Options
35
+
36
+ Pass these properties into the `EventResource` constructor to customize your calendar.
37
+
38
+ | Property | Type | Default | Description |
39
+ | :---------------- | :------------- | :----------- | :-------------------------------------------------------------------- |
40
+ | **container** | `string\|Node` | _Required_ | CSS selector or DOM pointer mount node. |
41
+ | **rooms** | `Array` | `[]` | Master source list establishing rows along the vertical plane. |
42
+ | **timeSlots** | `Array` | `[]` | Master source list defining columns mapped along the horizontal path. |
43
+ | **initialEvents** | `Array` | `[]` | In-memory event array populating coordinates on load. |
44
+ | **holidays** | `Array` | `[]` | Configuration identifying structural exceptions and global days. |
45
+ | **customButtons** | `Array` | `[]` | Extensible collections rendering specialized tool structures. |
46
+ | **showControls** | `boolean` | `false` | Visibility of structural management toolbars. |
47
+ | **stickyHeaders** | `boolean` | `true` | Toggles CSS sticky double-axis tracking logic. |
48
+ | **defaultView** | `string` | `'daily'` | Default presentation layout. Must be 'daily' or 'weekly'. |
49
+ | **defaultDate** | `Date\|string` | `new Date()` | Frame configuration locking starting lifecycle boundaries. |
50
+
51
+ ### Callbacks & Renderers
52
+
53
+ | Property | Type | Description |
54
+ | :----------------------- | :--------- | :----------------------------------------------------------------- |
55
+ | **onCellClick** | `Function` | Callback capturing clicks targeting empty coordinates. |
56
+ | **onEventClick** | `Function` | Callback targeting allocated calendar card coordinates. |
57
+ | **fetchEvents** | `Function` | Async method returning structural item sets based on date/view. |
58
+ | **renderRoomHeader** | `Function` | HTML generator returning structural formatting for row slots. |
59
+ | **renderTimeSlotHeader** | `Function` | HTML generator returning structural formatting for column slots. |
60
+ | **renderEvent** | `Function` | HTML generator returning custom markup for individual event cards. |
61
+
62
+ ## Data Models
63
+
64
+ ### CalendarRoom
65
+
66
+ Defines a resource row within the calendar matrix grid layout.
67
+
68
+ | Property | Type | Description |
69
+ | :-------- | :--------------- | :--------------------------------------------------------- |
70
+ | **id** | `string\|number` | **Required.** Unique identifier for the room or resource. |
71
+ | **name** | `string` | **Required.** Fallback display title of the room/resource. |
72
+ | **[key]** | `any` | Optional custom properties (e.g., capacity, hasProjector). |
73
+
74
+ ### TimeSlot
75
+
76
+ Defines a timeline column structure partitioning the matrix grid workspace.
77
+
78
+ | Property | Type | Description |
79
+ | :-------- | :--------------- | :---------------------------------------------------------- |
80
+ | **id** | `string\|number` | **Required.** Unique identifier for the chronological slot. |
81
+ | **label** | `string` | **Required.** Fallback timeline display text. |
82
+ | **[key]** | `any` | Optional custom properties (e.g., isLunchHour). |
83
+
84
+ ### CalendarEvent
85
+
86
+ Represents an allocated timeline event mapped directly into a specific intersection cell.
87
+
88
+ | Property | Type | Description |
89
+ | :--------- | :--------------- | :-------------------------------------------------------------------- |
90
+ | **id** | `string\|number` | **Required.** Unique identifier for the scheduled event element. |
91
+ | **roomId** | `string\|number` | **Required.** Foreign key binding the item to a valid Room ID. |
92
+ | **timeId** | `string\|number` | **Required.** Foreign key binding the item to a valid TimeSlot ID. |
93
+ | **title** | `string` | **Required.** Plain-text title injected inside the card element. |
94
+ | **color** | `string` | Optional CSS color value for the background card. Default: `#3b82f6`. |
95
+ | **[key]** | `any` | Optional custom meta data attributes. |
96
+
97
+ ## Interaction Payloads
98
+
99
+ When interacting with the grid, the library fires callbacks with contextual payloads.
100
+
101
+ ### ClickContextPayload (Empty Cell Click)
102
+
103
+ | Property | Type | Description |
104
+ | :---------- | :------------- | :----------------------------------------------------------------- |
105
+ | **date** | `Date` | Chronological state baseline actively mounted inside the viewport. |
106
+ | **view** | `string` | Current structural mode index ('daily' or 'weekly'). |
107
+ | **holiday** | `Object\|null` | Associated holiday data object if applicable. |
108
+ | **row** | `Object` | Track metadata coordinates (`index` and `data`). |
109
+ | **col** | `Object` | Timeline metadata coordinates (`index` and `data`). |
110
+ | **cell** | `Object` | Target cell contents (`roomId`, `timeId`, and `events` array). |
111
+
112
+ ### EventClickPayload (Event Card Click)
113
+
114
+ Inherits everything from `ClickContextPayload` and adds:
115
+
116
+ | Property | Type | Description |
117
+ | :-------------- | :----------- | :-------------------------------------------------------------------------- |
118
+ | **event** | `Object` | **Required.** The unique target event parameters bound to the clicked card. |
119
+ | **nativeEvent** | `MouseEvent` | **Required.** Raw browser click interaction data. |
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@chaaanito/event-resource-calendar",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ }
9
+ }
package/src/index.js ADDED
@@ -0,0 +1,844 @@
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 }
10
+ */
11
+
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
+ 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
+ constructor(options) {
169
+ let resolvedContainer = null;
170
+
171
+ if (typeof options.container === "string") {
172
+ resolvedContainer = document.querySelector(options.container);
173
+
174
+ if (!resolvedContainer) {
175
+ resolvedContainer =
176
+ document.getElementById(options.container) ||
177
+ document.getElementsByClassName(options.container)[0];
178
+ }
179
+ } else if (
180
+ options.container instanceof Node ||
181
+ options.container instanceof Element
182
+ ) {
183
+ resolvedContainer = options.container;
184
+ } else if (
185
+ options.container &&
186
+ typeof options.container[0] !== "undefined"
187
+ ) {
188
+ resolvedContainer = options.container[0];
189
+ }
190
+
191
+ if (!resolvedContainer) {
192
+ throw new Error(
193
+ "EventResource: A valid container (CSS selector string, Node, or HTMLElement) is required and must exist in the DOM.",
194
+ );
195
+ }
196
+
197
+ /**
198
+ * @type {HTMLElement}
199
+ * @description The verified root DOM node where the calendar is mounted.
200
+ */
201
+ this.container = resolvedContainer;
202
+
203
+ // 2. Data Options
204
+ /** @type {CalendarRoom[]} */
205
+ this.rooms = options.rooms || [];
206
+ /** @type {TimeSlot[]} */
207
+ this.timeSlots = options.timeSlots || [];
208
+ /** @type {CalendarEvent[]} */
209
+ this.events = options.initialEvents || [];
210
+ /** @type {CalendarHoliday[]} */
211
+ this.holidays = options.holidays || [];
212
+ /** @type {CustomButton[]} */
213
+ this.customButtons = options.customButtons || [];
214
+
215
+ // 3. UI Controls & State
216
+ /** @type {boolean} */
217
+ this.showControls = options.showControls || false;
218
+ /** @type {boolean} */
219
+ this.stickyHeaders = options.stickyHeaders !== false;
220
+ /** @type {'daily'|'weekly'} */
221
+ this.currentView = options.defaultView || "daily";
222
+ /** @type {Date} */
223
+ this.currentDate = options.defaultDate
224
+ ? new Date(options.defaultDate)
225
+ : new Date();
226
+ /** @type {boolean} */
227
+ this.isFetching = false;
228
+
229
+ // 4. Callbacks & Rich Formatting Helpers
230
+ /** @type {function|null} */
231
+ this.onCellClick = options.onCellClick || null;
232
+ /** @type {function|null} */
233
+ this.onEventClick = options.onEventClick || null;
234
+ /** @type {function|null} */
235
+ this.fetchEvents = options.fetchEvents || null;
236
+ /** @type {function|null} */
237
+ this.renderRoomHeader = options.renderRoomHeader || null;
238
+ /** @type {function|null} */
239
+ this.renderTimeSlotHeader = options.renderTimeSlotHeader || null;
240
+ /** @type {function|null} */
241
+ this.renderEvent = options.renderEvent || null;
242
+
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
+ this.eventsMap = new Map();
250
+ this._injectStyles();
251
+ this._buildEventsMap();
252
+ this.forceRender();
253
+ }
254
+
255
+ // --- State & Date Management ---
256
+
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
+ _getNormalizedDateString = (dateObj) => {
267
+ const d = new Date(dateObj);
268
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
269
+ };
270
+
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
+ _getHolidayForDate = (dateObj) => {
281
+ const targetDate = this._getNormalizedDateString(dateObj);
282
+ return (
283
+ this.holidays.find(
284
+ (h) => this._getNormalizedDateString(h.date) === targetDate,
285
+ ) || null
286
+ );
287
+ };
288
+
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
+ _buildEventsMap = () => {
296
+ this.eventsMap.clear();
297
+ for (const ev of this.events) {
298
+ const key = `${ev.roomId}-${ev.timeId}`;
299
+ if (!this.eventsMap.has(key)) {
300
+ this.eventsMap.set(key, []);
301
+ }
302
+ this.eventsMap.get(key).push(ev);
303
+ }
304
+ };
305
+
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
+ setDate = async (newDate) => {
315
+ this.currentDate = new Date(newDate);
316
+ await this.forceRender();
317
+ };
318
+
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
+ setView = async (newView) => {
328
+ if (this.currentView === newView) return;
329
+ this.currentView = newView;
330
+ await this.forceRender();
331
+ };
332
+
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
+ navigate = async (direction) => {
343
+ const daysToMove = this.currentView === "weekly" ? 7 : 1;
344
+ const multiplier = direction === "next" ? 1 : -1;
345
+
346
+ const newDate = new Date(this.currentDate);
347
+ newDate.setDate(newDate.getDate() + daysToMove * multiplier);
348
+
349
+ await this.setDate(newDate);
350
+ };
351
+
352
+ // --- Public API ---
353
+
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
+ addEvent = (newEvent) => {
369
+ this.events.push(newEvent);
370
+ this._buildEventsMap();
371
+ this.render();
372
+ };
373
+
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
+ removeEvent = (eventId) => {
383
+ this.events = this.events.filter((e) => e.id !== eventId);
384
+ this._buildEventsMap();
385
+ this.render();
386
+ };
387
+
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
+ clearAllEvents = () => {
397
+ this.events = [];
398
+ this.eventsMap.clear();
399
+ this.render();
400
+ };
401
+
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
+ forceRender = async () => {
411
+ if (this.isFetching) return;
412
+
413
+ if (typeof this.fetchEvents === "function") {
414
+ this.isFetching = true;
415
+ try {
416
+ const freshEvents = await this.fetchEvents(
417
+ this.currentDate,
418
+ this.currentView,
419
+ );
420
+ this.events = freshEvents || [];
421
+ } catch (error) {
422
+ console.error("EventResource: Failed to refetch events.", error);
423
+ } finally {
424
+ this.isFetching = false;
425
+ }
426
+ }
427
+
428
+ this._buildEventsMap();
429
+ this.render();
430
+ };
431
+
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
+ destroy = () => {
442
+ this.events = [];
443
+ this.rooms = [];
444
+ this.timeSlots = [];
445
+ this.holidays = [];
446
+ this.customButtons = [];
447
+ this.eventsMap.clear();
448
+ this.onCellClick = null;
449
+ this.onEventClick = null;
450
+ this.fetchEvents = null;
451
+ this.renderRoomHeader = null;
452
+ this.renderTimeSlotHeader = null;
453
+
454
+ if (this.container) {
455
+ const wrapper = this.container.querySelector(".er-container");
456
+ if (wrapper) {
457
+ this.container.removeChild(wrapper);
458
+ }
459
+ }
460
+ };
461
+
462
+ // --- DOM Creation & Rendering ---
463
+
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
+ _renderToolbar = (wrapper) => {
474
+ const toolbar = document.createElement("div");
475
+ toolbar.className = "er-toolbar";
476
+
477
+ // Left: Navigation Controls
478
+ const navGroup = document.createElement("div");
479
+ navGroup.className = "er-toolbar-group";
480
+
481
+ const btnPrev = document.createElement("button");
482
+ btnPrev.className = "er-btn";
483
+ btnPrev.textContent = "◀";
484
+ btnPrev.onclick = () => this.navigate("prev");
485
+
486
+ const btnToday = document.createElement("button");
487
+ btnToday.className = "er-btn";
488
+ btnToday.textContent = "Today";
489
+ btnToday.onclick = () => this.setDate(new Date());
490
+
491
+ const btnNext = document.createElement("button");
492
+ btnNext.className = "er-btn";
493
+ btnNext.textContent = "▶";
494
+ btnNext.onclick = () => this.navigate("next");
495
+
496
+ // Date Picker HTML5 Input Integration
497
+ const datePicker = document.createElement("input");
498
+ datePicker.type = "date";
499
+ datePicker.className = "er-date-picker";
500
+ datePicker.value = this._getNormalizedDateString(this.currentDate);
501
+ datePicker.onchange = (e) => {
502
+ if (e.target.value) {
503
+ this.setDate(e.target.value);
504
+ }
505
+ };
506
+
507
+ navGroup.append(btnPrev, btnToday, btnNext, datePicker);
508
+
509
+ // Right: View Framework Mode Toggles & Freezing Management Elements
510
+ const viewGroup = document.createElement("div");
511
+ viewGroup.className = "er-toolbar-group";
512
+
513
+ // Append Client Extensible Custom Action Elements
514
+ this.customButtons.forEach((btnConfig) => {
515
+ const customBtn = document.createElement("button");
516
+ customBtn.className = `er-btn ${btnConfig.className || ""}`.trim();
517
+ customBtn.textContent = btnConfig.label;
518
+ customBtn.onclick = (e) => btnConfig.onClick(e);
519
+ viewGroup.appendChild(customBtn);
520
+ });
521
+
522
+ const btnDaily = document.createElement("button");
523
+ btnDaily.className = `er-btn ${this.currentView === "daily" ? "active" : ""}`;
524
+ btnDaily.textContent = "Daily";
525
+ btnDaily.onclick = () => this.setView("daily");
526
+
527
+ const btnWeekly = document.createElement("button");
528
+ btnWeekly.className = `er-btn ${this.currentView === "weekly" ? "active" : ""}`;
529
+ btnWeekly.textContent = "Weekly";
530
+ btnWeekly.onclick = () => this.setView("weekly");
531
+
532
+ viewGroup.append(btnDaily, btnWeekly);
533
+
534
+ toolbar.append(navGroup, viewGroup);
535
+
536
+ const btnFreeze = document.createElement("button");
537
+ btnFreeze.className = `er-btn er-freeze-btn ${this.stickyHeaders ? "active" : ""}`;
538
+ btnFreeze.textContent = this.stickyHeaders ? "Freeze 📌" : "Unfreeze 🔓";
539
+ btnFreeze.onclick = () => {
540
+ this.stickyHeaders = !this.stickyHeaders;
541
+ this.render();
542
+ };
543
+ navGroup.appendChild(btnFreeze);
544
+
545
+ // Active Holiday Indicator Check
546
+ const currentHoliday = this._getHolidayForDate(this.currentDate);
547
+ if (currentHoliday) {
548
+ const holidayBadge = document.createElement("span");
549
+ holidayBadge.className = "er-holiday-badge";
550
+ holidayBadge.textContent = `🎉 ${currentHoliday.name}`;
551
+ navGroup.appendChild(holidayBadge);
552
+ }
553
+
554
+ wrapper.appendChild(toolbar);
555
+ };
556
+
557
+ /**
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.
560
+ * @returns {void}
561
+ * @private
562
+ * @example
563
+ * // Synchronous UI redraw
564
+ * this.render();
565
+ */
566
+ render = () => {
567
+ this.container.innerHTML = "";
568
+
569
+ const wrapper = document.createElement("div");
570
+ wrapper.className = "er-container";
571
+
572
+ if (this.showControls) {
573
+ this._renderToolbar(wrapper);
574
+ }
575
+
576
+ const gridWrapper = document.createElement("div");
577
+ gridWrapper.className = "er-grid-wrapper";
578
+
579
+ const grid = document.createElement("div");
580
+ grid.className = `er-grid ${this.stickyHeaders ? "er-sticky" : ""}`.trim();
581
+ grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length}, minmax(120px, 1fr))`;
582
+
583
+ const corner = document.createElement("div");
584
+ corner.className = "er-header-cell er-corner";
585
+ grid.appendChild(corner);
586
+
587
+ // Column Map Processing Loop
588
+ this.timeSlots.forEach((time) => {
589
+ const timeHeader = document.createElement("div");
590
+ timeHeader.className = "er-header-cell er-time-header";
591
+
592
+ if (typeof this.renderTimeSlotHeader === "function") {
593
+ timeHeader.innerHTML = this.renderTimeSlotHeader(time);
594
+ } else {
595
+ timeHeader.textContent = time.label;
596
+ }
597
+
598
+ grid.appendChild(timeHeader);
599
+ });
600
+
601
+ const activeHoliday = this._getHolidayForDate(this.currentDate);
602
+
603
+ // Row Matrix Layout Intercept Processing Loops
604
+ this.rooms.forEach((room, rowIndex) => {
605
+ const roomHeader = document.createElement("div");
606
+ roomHeader.className = "er-header-cell er-room-header";
607
+
608
+ if (typeof this.renderRoomHeader === "function") {
609
+ roomHeader.innerHTML = this.renderRoomHeader(room);
610
+ } else {
611
+ roomHeader.textContent = room.name;
612
+ }
613
+
614
+ grid.appendChild(roomHeader);
615
+
616
+ this.timeSlots.forEach((time, colIndex) => {
617
+ const cell = document.createElement("div");
618
+ cell.className = `er-grid-cell ${activeHoliday ? "er-holiday-cell" : ""}`;
619
+
620
+ const cellEvents = this.eventsMap.get(`${room.id}-${time.id}`) || [];
621
+
622
+ cell.addEventListener("click", () => {
623
+ if (this.onCellClick) {
624
+ this.onCellClick({
625
+ date: this.currentDate,
626
+ view: this.currentView,
627
+ holiday: activeHoliday,
628
+ row: { index: rowIndex, data: room },
629
+ col: { index: colIndex, data: time },
630
+ cell: { roomId: room.id, timeId: time.id, events: cellEvents },
631
+ });
632
+ }
633
+ });
634
+
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
+ grid.appendChild(cell);
670
+ });
671
+ });
672
+
673
+ gridWrapper.appendChild(grid);
674
+ wrapper.appendChild(gridWrapper);
675
+ this.container.appendChild(wrapper);
676
+ };
677
+
678
+ /**
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}
682
+ * @private
683
+ * @example
684
+ * // Automatic execution during construction
685
+ * this._injectStyles();
686
+ */
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);
843
+ };
844
+ }