@chaaanito/event-resource-calendar 1.0.2 → 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/README.md CHANGED
@@ -1,17 +1,18 @@
1
1
  # EventResource
2
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.
3
+ A lightweight, high-performance vanilla JavaScript resource calendar library. Features skeleton-first asynchronous rendering, O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
4
4
 
5
5
  # Installation
6
6
 
7
7
  ```bash
8
- npm install event-resource-calendar
8
+ npm install @chaaanito/event-resource-calendar
9
9
  ```
10
10
 
11
11
  # Quick Start
12
12
 
13
13
  ```javascript
14
- import EventResource from "event-resource-calendar";
14
+ import EventResource from "@chaaanito/event-resource-calendar";
15
+ import "@chaaanito/event-resource-calendar/style.css"; // Required for grid structural styling
15
16
 
16
17
  const calendar = new EventResource({
17
18
  container: "#calendar-root",
@@ -50,27 +51,32 @@ Pass these properties into the `EventResource` constructor to customize your cal
50
51
 
51
52
  | Property | Type | Default | Description |
52
53
  | :---------------- | :------------- | :----------- | :-------------------------------------------------------------------- |
53
- | **container** | `string\|Node` | _Required_ | CSS selector or DOM pointer mount node. |
54
+ | **container** | `string\|Node` | _Required_ | CSS selector or explicit DOM pointer mount node. |
54
55
  | **rooms** | `Array` | `[]` | Master source list establishing rows along the vertical plane. |
55
56
  | **timeSlots** | `Array` | `[]` | Master source list defining columns mapped along the horizontal path. |
56
57
  | **initialEvents** | `Array` | `[]` | In-memory event array populating coordinates on load. |
57
58
  | **holidays** | `Array` | `[]` | Configuration identifying structural exceptions and global days. |
58
59
  | **customButtons** | `Array` | `[]` | Extensible collections rendering specialized tool structures. |
59
- | **showControls** | `boolean` | `false` | Visibility of structural management toolbars. |
60
- | **stickyHeaders** | `boolean` | `true` | Toggles CSS sticky double-axis tracking logic. |
61
- | **defaultView** | `string` | `'daily'` | Default presentation layout. Must be 'daily' or 'weekly'. |
60
+ | **showControls** | `boolean` | `false` | Visibility of structural management toolbars (Datepicker, arrows). |
61
+ | **stickyHeaders** | `boolean` | `true` | Toggles CSS sticky double-axis tracking logic across layout headers. |
62
+ | **defaultView** | `string` | `'daily'` | Default presentation layout mode. Must be 'daily' or 'weekly'. |
62
63
  | **defaultDate** | `Date\|string` | `new Date()` | Frame configuration locking starting lifecycle boundaries. |
63
64
 
64
65
  ### Callbacks & Renderers
65
66
 
66
- | Property | Type | Description |
67
- | :----------------------- | :--------- | :----------------------------------------------------------------- |
68
- | **onCellClick** | `Function` | Callback capturing clicks targeting empty coordinates. |
69
- | **onEventClick** | `Function` | Callback targeting allocated calendar card coordinates. |
70
- | **fetchEvents** | `Function` | Async method returning structural item sets based on date/view. |
71
- | **renderRoomHeader** | `Function` | HTML generator returning structural formatting for row slots. |
72
- | **renderTimeSlotHeader** | `Function` | HTML generator returning structural formatting for column slots. |
73
- | **renderEvent** | `Function` | HTML generator returning custom markup for individual event cards. |
67
+ | Property | Type | Description |
68
+ | :----------------------- | :--------- | :--------------------------------------------------------------------------- |
69
+ | **onCellClick** | `Function` | Callback capturing clicks targeting empty coordinates. |
70
+ | **onEventClick** | `Function` | Callback targeting allocated calendar card coordinates. |
71
+ | **fetchEvents** | `Function` | Async method resolving to an array of `CalendarEvent` objects. |
72
+ | **fetchRooms** | `Function` | Async method resolving to an array of `CalendarRoom` objects dynamically. |
73
+ | **fetchTimeSlots** | `Function` | Async method resolving to an array of `TimeSlot` objects dynamically. |
74
+ | **fetchHolidays** | `Function` | Async method resolving to an array of `CalendarHoliday` objects dynamically. |
75
+ | **renderRoomHeader** | `Function` | HTML generator returning structural formatting strings for row slots. |
76
+ | **renderTimeSlotHeader** | `Function` | HTML generator returning structural formatting strings for column slots. |
77
+ | **renderEvent** | `Function` | HTML generator returning custom markup for individual event cards. |
78
+
79
+ ---
74
80
 
75
81
  ## Data Models
76
82
 
@@ -92,7 +98,7 @@ Defines a timeline column structure partitioning the matrix grid workspace.
92
98
  | :-------- | :--------------- | :---------------------------------------------------------- |
93
99
  | **id** | `string\|number` | **Required.** Unique identifier for the chronological slot. |
94
100
  | **label** | `string` | **Required.** Fallback timeline display text. |
95
- | **[key]** | `any` | Optional custom properties (e.g., isLunchHour). |
101
+ | **[key]** | `any` | Optional extensible properties (e.g., isLunchHour). |
96
102
 
97
103
  ### CalendarEvent
98
104
 
@@ -105,28 +111,61 @@ Represents an allocated timeline event mapped directly into a specific intersect
105
111
  | **timeId** | `string\|number` | **Required.** Foreign key binding the item to a valid TimeSlot ID. |
106
112
  | **title** | `string` | **Required.** Plain-text title injected inside the card element. |
107
113
  | **color** | `string` | Optional CSS color value for the background card. Default: `#3b82f6`. |
108
- | **[key]** | `any` | Optional custom meta data attributes. |
114
+ | **[key]** | `any` | Optional custom meta data attributes parsed down to click payloads. |
115
+
116
+ ### CalendarHoliday
117
+
118
+ Maps specific dates to holiday statuses, shifting backgrounds and appending context data.
119
+
120
+ | Property | Type | Description |
121
+ | :-------- | :--------------------- | :--------------------------------------------------------------------- |
122
+ | **date** | `string\|Date\|number` | **Required.** Parsable temporal timestamp mapping the milestone. |
123
+ | **name** | `string` | **Required.** Human-readable label injected into global notice badges. |
124
+ | **[key]** | `any` | Optional open-ended customer specific holiday data. |
125
+
126
+ ---
109
127
 
110
128
  ## Interaction Payloads
111
129
 
112
- When interacting with the grid, the library fires callbacks with contextual payloads.
130
+ When interacting with the grid, the library fires callbacks with comprehensive contextual payloads.
113
131
 
114
132
  ### ClickContextPayload (Empty Cell Click)
115
133
 
134
+ Shared universally across grid click response lifecycles.
135
+
116
136
  | Property | Type | Description |
117
137
  | :---------- | :------------- | :----------------------------------------------------------------- |
118
138
  | **date** | `Date` | Chronological state baseline actively mounted inside the viewport. |
119
- | **view** | `string` | Current structural mode index ('daily' or 'weekly'). |
120
- | **holiday** | `Object\|null` | Associated holiday data object if applicable. |
121
- | **row** | `Object` | Track metadata coordinates (`index` and `data`). |
122
- | **col** | `Object` | Timeline metadata coordinates (`index` and `data`). |
139
+ | **view** | `string` | Current structural mode index configuration ('daily' or 'weekly'). |
140
+ | **holiday** | `Object\|null` | Associated holiday data object if applicable to current date. |
141
+ | **row** | `Object` | Track metadata coordinates (`index` and `data` objects). |
142
+ | **col** | `Object` | Timeline metadata coordinates (`index` and `data` objects). |
123
143
  | **cell** | `Object` | Target cell contents (`roomId`, `timeId`, and `events` array). |
124
144
 
125
145
  ### EventClickPayload (Event Card Click)
126
146
 
127
- Inherits everything from `ClickContextPayload` and adds:
147
+ Inherits all properties from `ClickContextPayload` and adds:
128
148
 
129
149
  | Property | Type | Description |
130
150
  | :-------------- | :----------- | :-------------------------------------------------------------------------- |
131
- | **event** | `Object` | **Required.** The unique target event parameters bound to the clicked card. |
132
- | **nativeEvent** | `MouseEvent` | **Required.** Raw browser click interaction data. |
151
+ | **event** | `Object` | **Required.** The explicit, unique target event parameters clicked. |
152
+ | **nativeEvent** | `MouseEvent` | **Required.** Raw browser click interaction data used for element tracking. |
153
+
154
+ ---
155
+
156
+ ## Public API Methods
157
+
158
+ Once instantiated, the calendar instance exposes methods to control the grid programmatically.
159
+
160
+ | Method | Description |
161
+ | :------------------------- | :---------------------------------------------------------------------------------------------- |
162
+ | **`addEvent(event)`** | Injects a new event and performs an isolated, high-speed DOM append without layout thrashing. |
163
+ | **`removeEvent(eventId)`** | Purges a specific event by ID from memory and removes it from the UI instantly. |
164
+ | **`clearAllEvents()`** | Wipes all events from the board while preserving the layout skeleton rules. |
165
+ | **`setDate(newDate)`** | Jumps the calendar to a specific date. Automatically triggers `fetch` hooks if configured. |
166
+ | **`setView(newView)`** | Mutates the layout granularity between daily and weekly modes. |
167
+ | **`Maps(direction)`** | Steps the timeline forward or backward based on the current view multiplier. |
168
+ | **`forceRender()`** | Triggers a manual full data replenishment cycle, drawing the skeleton and resolving all asyncs. |
169
+ | **`destroy()`** | Unmounts the DOM, clears memory caches, and drops listener closures to prevent leaks. |
170
+
171
+ ---
package/jsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "allowJs": true,
6
+ "declaration": true,
7
+ "emitDeclarationOnly": true,
8
+ "outDir": "./dist/types",
9
+ "strict": false
10
+ },
11
+ "include": ["src/**/*"]
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaaanito/event-resource-calendar",
3
- "version": "1.0.2",
3
+ "version": "1.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/chaaanito/event-resource-calendar.git"
@@ -11,7 +11,14 @@
11
11
  "homepage": "https://github.com/chaaanito/event-resource-calendar#readme",
12
12
  "type": "module",
13
13
  "main": "src/index.js",
14
+ "types": "dist/types/index.d.ts",
15
+ "style": "src/styles.css",
14
16
  "exports": {
15
- ".": "./src/index.js"
17
+ ".": {
18
+ "types": "./dist/types/index.d.ts",
19
+ "import": "./src/index.js",
20
+ "default": "./src/index.js"
21
+ },
22
+ "./style.css": "./src/styles.css"
16
23
  }
17
24
  }
@@ -0,0 +1,98 @@
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
+ */
8
+
9
+ /**
10
+ * @typedef {Object} TimeSlot
11
+ * @description Defines a timeline column structure partitioning the matrix grid workspace.
12
+ * @property {string|number} id - REQUIRED: Unique identifier for the chronological slot. Must be unique across all columns.
13
+ * @property {string} label - REQUIRED: Fallback timeline display text used if `renderTimeSlotHeader` is omitted.
14
+ * @property {*} [key: string] - OPTIONAL: Any additional extensible properties used for custom filtering or rich rendering templates.
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} CalendarEvent
19
+ * @description Represents an allocated timeline event mapped directly into a specific intersection cell.
20
+ * @property {string|number} id - REQUIRED: Unique identifier for the scheduled event element.
21
+ * @property {string|number} roomId - REQUIRED: Relational foreign key binding the item to a valid {@link CalendarRoom.id}.
22
+ * @property {string|number} timeId - REQUIRED: Relational foreign key binding the item to a valid {@link TimeSlot.id}.
23
+ * @property {string} title - REQUIRED: Plain-text title injected inside the DOM node element card.
24
+ * @property {string} [color='#3b82f6'] - OPTIONAL: Valid CSS color value (hex, rgb, hsl, keyword) for the background tracking card.
25
+ * @property {*} [key: string] - OPTIONAL: Custom meta data attributes parsed down to click event callback streams.
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} CalendarHoliday
30
+ * @description Maps specific dates to holiday statuses, shifting backgrounds and appending context data to interaction payloads.
31
+ * @property {string|Date|number} date - REQUIRED: Parsable temporal timestamp mapping the holiday milestone. Must be resolvable by `new Date()`.
32
+ * @property {string} name - REQUIRED: The human-readable label injected into global notice badges and cell descriptors.
33
+ * @property {*} [key: string] - OPTIONAL: Open-ended customer specific holiday data.
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} CustomButton
38
+ * @description Injectable client control appended directly onto the right-hand toolbar grouping matrix.
39
+ * @property {string} label - REQUIRED: Text descriptor rendered inside the interactive control button element frame.
40
+ * @property {function(MouseEvent): void} onClick - REQUIRED: Action handler fired immediately upon client interactions.
41
+ * @property {string} [className] - OPTIONAL: Space-delimited functional CSS style modifiers for custom visual overrides.
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} ClickContextPayload
46
+ * @description Consolidated operational telemetry shared universally across grid click response lifecycles.
47
+ * @property {Date} date - Chronological state baseline actively mounted inside the viewport template frame.
48
+ * @property {'daily'|'weekly'} view - Current structural mode index configuration.
49
+ * @property {CalendarHoliday|null} holiday - Associated holiday data object if the current frame falls on a configured day milestone.
50
+ * @property {Object} row - Track metadata coordinates.
51
+ * @property {number} row.index - Vertical index array placement coordinate.
52
+ * @property {CalendarRoom} row.data - Complete root object data context passed down from initialization.
53
+ * @property {Object} col - Timeline metadata coordinates.
54
+ * @property {number} col.index - Horizontal layout coordinate tracking indexes.
55
+ * @property {TimeSlot} col.data - Complete root chronological object parameters.
56
+ * @property {Object} cell - Operational target cell contents.
57
+ * @property {string|number} cell.roomId - Unique cell row lookup index.
58
+ * @property {string|number} cell.timeId - Unique cell column chronological coordinate index.
59
+ * @property {CalendarEvent[]} cell.events - Contextual array containing all events currently occupying this grid location.
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} EventClickPayload
64
+ * @description Multi-layered payload shared exclusively with the `onEventClick` subscriber method.
65
+ * @extends ClickContextPayload
66
+ * @property {CalendarEvent} event - REQUIRED: The explicit, unique target event parameters bound to the clicked card element.
67
+ * @property {MouseEvent} nativeEvent - REQUIRED: Raw browser click interaction data used for analytical intercept positioning or element tracking.
68
+ */
69
+
70
+ /**
71
+ * @typedef {Object} EventResourceOptions
72
+ * @description Input operational context map parsing standard parameters through class factories.
73
+ * @property {string|HTMLElement|Node} container - REQUIRED: CSS selector engine target or explicit DOM pointer mount node.
74
+ * @property {CalendarRoom[]} [rooms=[]] - OPTIONAL: Master source list establishing rows along the vertical plane matrix mapping layout.
75
+ * @property {TimeSlot[]} [timeSlots=[]] - OPTIONAL: Master source list defining columns mapped along the horizontal path lane.
76
+ * @property {CalendarEvent[]} [initialEvents=[]] - OPTIONAL: In-memory event array populating coordinates during setup initialization workflows.
77
+ * @property {CalendarHoliday[]} [holidays=[]] - OPTIONAL: Array configuration identifying structural exceptions and specialized global days.
78
+ * @property {CustomButton[]} [customButtons=[]] - OPTIONAL: Extensible collections rendering specialized tool structures within toolbars.
79
+ * @property {boolean} [showControls=false] - OPTIONAL: Flag managing initialization visibility of structural management toolbars.
80
+ * @property {boolean} [stickyHeaders=true] - OPTIONAL: Toggles CSS sticky double-axis tracking logic across layout headers on load.
81
+ * @property {'daily'|'weekly'} [defaultView='daily'] - OPTIONAL: Default presentation structure layout selection state. Must be 'daily' or 'weekly'.
82
+ * @property {Date|string|number} [defaultDate=new Date()] - OPTIONAL: Frame configuration locking starting lifecycle boundaries.
83
+ * @property {function(ClickContextPayload): void} [onCellClick] - OPTIONAL: Interaction callback capturing clicks targeting empty coordinates.
84
+ * @property {function(EventClickPayload): void} [onEventClick] - OPTIONAL: Interaction callback targeting allocated calendar card coordinates.
85
+ * @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Data fetching intercept. Async method returning structural item sets.
86
+ * @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for row slots.
87
+ * @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for column slots.
88
+ * @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept returning custom markup for individual event cards. Overrides default title text.
89
+ * @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Async method returning events.
90
+ * @property {function(Date, 'daily'|'weekly'): Promise<CalendarRoom[]>} [fetchRooms] - OPTIONAL: Async method returning rooms dynamically.
91
+ * @property {function(Date, 'daily'|'weekly'): Promise<TimeSlot[]>} [fetchTimeSlots] - OPTIONAL: Async method returning timeline columns dynamically.
92
+ * @property {function(Date, 'daily'|'weekly'): Promise<CalendarHoliday[]>} [fetchHolidays] - OPTIONAL: Async method returning holidays dynamically.
93
+ * * @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept.
94
+ * @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept.
95
+ * @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept.
96
+ */
97
+
98
+ export {};
package/src/index.js CHANGED
@@ -1,114 +1,16 @@
1
1
  /**
2
- * @typedef {Object} CalendarRoom
3
- * @description Defines a resource row within the calendar matrix grid layout.
4
- * @property {string|number} id - REQUIRED: Unique identifier for the room or resource. Must be absolutely unique across all room rows.
5
- * @property {string} name - REQUIRED: Fallback display title of the room/resource used if `renderRoomHeader` is omitted.
6
- * @property {*} [key: string] - OPTIONAL: Any additional open-ended properties used for custom filtering or rich rendering templates.
7
- * @example
8
- * // Example of a valid CalendarRoom object
9
- * { id: 'room-101', name: 'Conference Room A', capacity: 12, hasProjector: true }
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
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
105
8
  */
106
9
 
107
10
  /**
108
11
  * EventResource
109
12
  * A lightweight, high-performance vanilla JavaScript resource calendar library.
110
13
  * Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
111
- * @class
112
14
  */
113
15
  export default class EventResource {
114
16
  /**
@@ -124,7 +26,7 @@ export default class EventResource {
124
26
  * * // 2. Structural UI Toggles
125
27
  * showControls: true,
126
28
  * stickyHeaders: true,
127
- * * // 3. Grid Definitions (Rows, Columns, and Exceptions)
29
+ * * // 3. Grid Definitions (Static Fallbacks)
128
30
  * rooms: [{ id: 'r1', name: 'Studio A', capacity: 10 }],
129
31
  * timeSlots: [{ id: 't1', label: '09:00 AM' }],
130
32
  * holidays: [{ date: '2026-12-25', name: 'Christmas Day' }],
@@ -139,38 +41,40 @@ export default class EventResource {
139
41
  * * // 5. Toolbar Extensions
140
42
  * customButtons: [{
141
43
  * label: 'Export PDF',
142
- * className: 'bg-red-500 text-white hover:bg-red-600',
143
- * onClick: (e) => console.log('Triggering PDF generation...', e)
44
+ * className: 'bg-red-500 text-white',
45
+ * onClick: (e) => console.log('Exporting...', e)
144
46
  * }],
145
47
  * * // 6. Interaction Event Hooks
146
- * onCellClick: (payload) => {
147
- * console.log(`Empty slot clicked! Room: ${payload.cell.roomId}, Time: ${payload.cell.timeId}`);
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();
148
58
  * },
149
- * onEventClick: (payload) => {
150
- * alert(`Opening details for: ${payload.event.title}`);
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();
151
66
  * },
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
67
  * fetchEvents: async (date, view) => {
157
68
  * const res = await fetch(`/api/events?date=${date.toISOString()}&view=${view}`);
158
69
  * return await res.json();
159
70
  * }
160
71
  * });
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
72
  */
168
73
  constructor(options) {
169
74
  let resolvedContainer = null;
170
75
 
171
76
  if (typeof options.container === "string") {
172
77
  resolvedContainer = document.querySelector(options.container);
173
-
174
78
  if (!resolvedContainer) {
175
79
  resolvedContainer =
176
80
  document.getElementById(options.container) ||
@@ -190,93 +94,54 @@ export default class EventResource {
190
94
 
191
95
  if (!resolvedContainer) {
192
96
  throw new Error(
193
- "EventResource: A valid container (CSS selector string, Node, or HTMLElement) is required and must exist in the DOM.",
97
+ "EventResource: A valid container is required and must exist in the DOM.",
194
98
  );
195
99
  }
196
100
 
197
- /**
198
- * @type {HTMLElement}
199
- * @description The verified root DOM node where the calendar is mounted.
200
- */
201
101
  this.container = resolvedContainer;
202
102
 
203
- // 2. Data Options
204
- /** @type {CalendarRoom[]} */
103
+ // Data Options
205
104
  this.rooms = options.rooms || [];
206
- /** @type {TimeSlot[]} */
207
105
  this.timeSlots = options.timeSlots || [];
208
- /** @type {CalendarEvent[]} */
209
106
  this.events = options.initialEvents || [];
210
- /** @type {CalendarHoliday[]} */
211
107
  this.holidays = options.holidays || [];
212
- /** @type {CustomButton[]} */
213
108
  this.customButtons = options.customButtons || [];
214
109
 
215
- // 3. UI Controls & State
216
- /** @type {boolean} */
110
+ // UI Controls & State
217
111
  this.showControls = options.showControls || false;
218
- /** @type {boolean} */
219
112
  this.stickyHeaders = options.stickyHeaders !== false;
220
- /** @type {'daily'|'weekly'} */
221
113
  this.currentView = options.defaultView || "daily";
222
- /** @type {Date} */
223
114
  this.currentDate = options.defaultDate
224
115
  ? new Date(options.defaultDate)
225
116
  : new Date();
226
- /** @type {boolean} */
227
117
  this.isFetching = false;
228
118
 
229
- // 4. Callbacks & Rich Formatting Helpers
230
- /** @type {function|null} */
119
+ // Callbacks & Async Fetchers
231
120
  this.onCellClick = options.onCellClick || null;
232
- /** @type {function|null} */
233
121
  this.onEventClick = options.onEventClick || null;
234
- /** @type {function|null} */
122
+
235
123
  this.fetchEvents = options.fetchEvents || null;
236
- /** @type {function|null} */
124
+ this.fetchRooms = options.fetchRooms || null;
125
+ this.fetchTimeSlots = options.fetchTimeSlots || null;
126
+ this.fetchHolidays = options.fetchHolidays || null;
127
+
237
128
  this.renderRoomHeader = options.renderRoomHeader || null;
238
- /** @type {function|null} */
239
129
  this.renderTimeSlotHeader = options.renderTimeSlotHeader || null;
240
- /** @type {function|null} */
241
130
  this.renderEvent = options.renderEvent || null;
242
131
 
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
132
  this.eventsMap = new Map();
250
- this._injectStyles();
251
133
  this._buildEventsMap();
134
+
252
135
  this.forceRender();
253
136
  }
254
137
 
255
138
  // --- State & Date Management ---
256
139
 
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
140
  _getNormalizedDateString = (dateObj) => {
267
141
  const d = new Date(dateObj);
268
142
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
269
143
  };
270
144
 
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
145
  _getHolidayForDate = (dateObj) => {
281
146
  const targetDate = this._getNormalizedDateString(dateObj);
282
147
  return (
@@ -286,16 +151,10 @@ export default class EventResource {
286
151
  );
287
152
  };
288
153
 
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
154
  _buildEventsMap = () => {
296
155
  this.eventsMap.clear();
297
156
  for (const ev of this.events) {
298
- const key = `${ev.roomId}-${ev.timeId}`;
157
+ const key = `${ev.roomId}::${ev.timeId}`;
299
158
  if (!this.eventsMap.has(key)) {
300
159
  this.eventsMap.set(key, []);
301
160
  }
@@ -304,11 +163,10 @@ export default class EventResource {
304
163
  };
305
164
 
306
165
  /**
307
- * Forces a chronological state shift, rewiring internal date parameters and firing synchronous asynchronous re-render loops.
166
+ * Forces a chronological state shift, rewiring internal date parameters and firing synchronous/asynchronous re-render loops.
308
167
  * @param {Date|string|number} newDate - REQUIRED: The new calendar baseline target date parameter.
309
168
  * @returns {Promise<void>} Resolves automatically when the async data refetch and complete re-render lifecycle are complete.
310
169
  * @example
311
- * // Jump directly to Halloween 2026
312
170
  * await calendar.setDate('2026-10-31');
313
171
  */
314
172
  setDate = async (newDate) => {
@@ -321,7 +179,6 @@ export default class EventResource {
321
179
  * @param {'daily'|'weekly'} newView - REQUIRED: The strict target structural mode selection string.
322
180
  * @returns {Promise<void>} Resolves when the refetch, DOM teardown, and structural framework update complete.
323
181
  * @example
324
- * // Swap calendar into weekly perspective mode
325
182
  * await calendar.setView('weekly');
326
183
  */
327
184
  setView = async (newView) => {
@@ -331,12 +188,10 @@ export default class EventResource {
331
188
  };
332
189
 
333
190
  /**
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.
191
+ * Calculates timeline vector offsets based on the active view mode, steps the internal date parameter, and triggers reconciliations.
336
192
  * @param {'prev'|'next'} direction - REQUIRED: The chronological vector direction keyword.
337
193
  * @returns {Promise<void>} Resolves upon successful navigation and canvas redraw.
338
194
  * @example
339
- * // Step forward in time based on current view step sizes
340
195
  * await calendar.navigate('next');
341
196
  */
342
197
  navigate = async (direction) => {
@@ -352,74 +207,85 @@ export default class EventResource {
352
207
  // --- Public API ---
353
208
 
354
209
  /**
355
- * Injects a raw configuration event into the application data state. Recompiles the collision map and forces a synchronous DOM update instantly.
210
+ * Injects a raw configuration event into the application data state. Recompiles the collision map and forces a high-speed DOM update.
356
211
  * @param {CalendarEvent} newEvent - REQUIRED: A valid object data map matching configuration structural specs exactly.
357
212
  * @returns {void}
358
213
  * @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
- * });
214
+ * calendar.addEvent({ id: 'evt-999', roomId: 'r1', timeId: 't1', title: 'Emergency Sync' });
367
215
  */
368
216
  addEvent = (newEvent) => {
369
217
  this.events.push(newEvent);
370
218
  this._buildEventsMap();
371
- this.render();
219
+ this._renderEvents();
372
220
  };
373
221
 
374
222
  /**
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}.
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.
377
225
  * @returns {void}
378
226
  * @example
379
- * // Purge a specific event from the DOM and memory
380
227
  * calendar.removeEvent('evt-999');
381
228
  */
382
229
  removeEvent = (eventId) => {
383
230
  this.events = this.events.filter((e) => e.id !== eventId);
384
231
  this._buildEventsMap();
385
- this.render();
232
+ this._renderEvents();
386
233
  };
387
234
 
388
235
  /**
389
236
  * 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
237
  * @returns {void}
392
238
  * @example
393
- * // Clean the board completely
394
239
  * calendar.clearAllEvents();
395
240
  */
396
241
  clearAllEvents = () => {
397
242
  this.events = [];
398
243
  this.eventsMap.clear();
399
- this.render();
244
+ this._renderEvents();
400
245
  };
401
246
 
402
247
  /**
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.
248
+ * Triggers a comprehensive data replenishment cycle. Evaluates external fetch connectors, updates map caches, and rebuilds the visual DOM.
405
249
  * @returns {Promise<void>} Resolves once all external data resolves and the DOM reconciliation concludes successfully.
406
250
  * @example
407
- * // Force a data refresh from the server
408
251
  * await calendar.forceRender();
409
252
  */
410
253
  forceRender = async () => {
411
254
  if (this.isFetching) return;
412
255
 
413
- if (typeof this.fetchEvents === "function") {
256
+ this.render();
257
+
258
+ const hasAsyncSources =
259
+ typeof this.fetchEvents === "function" ||
260
+ typeof this.fetchRooms === "function" ||
261
+ typeof this.fetchTimeSlots === "function" ||
262
+ typeof this.fetchHolidays === "function";
263
+
264
+ if (hasAsyncSources) {
414
265
  this.isFetching = true;
415
266
  try {
416
- const freshEvents = await this.fetchEvents(
417
- this.currentDate,
418
- this.currentView,
419
- );
267
+ const [freshEvents, freshRooms, freshTimeSlots, freshHolidays] =
268
+ await Promise.all([
269
+ typeof this.fetchEvents === "function"
270
+ ? this.fetchEvents(this.currentDate, this.currentView)
271
+ : Promise.resolve(this.events),
272
+ typeof this.fetchRooms === "function"
273
+ ? this.fetchRooms(this.currentDate, this.currentView)
274
+ : Promise.resolve(this.rooms),
275
+ typeof this.fetchTimeSlots === "function"
276
+ ? this.fetchTimeSlots(this.currentDate, this.currentView)
277
+ : Promise.resolve(this.timeSlots),
278
+ typeof this.fetchHolidays === "function"
279
+ ? this.fetchHolidays(this.currentDate, this.currentView)
280
+ : Promise.resolve(this.holidays),
281
+ ]);
282
+
420
283
  this.events = freshEvents || [];
284
+ this.rooms = freshRooms || [];
285
+ this.timeSlots = freshTimeSlots || [];
286
+ this.holidays = freshHolidays || [];
421
287
  } catch (error) {
422
- console.error("EventResource: Failed to refetch events.", error);
288
+ console.error("EventResource: Failed to refetch calendar data.", error);
423
289
  } finally {
424
290
  this.isFetching = false;
425
291
  }
@@ -431,10 +297,9 @@ export default class EventResource {
431
297
 
432
298
  /**
433
299
  * 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.
300
+ * Wipes parameters, drops listener closures, clears map caches, and securely unmounts UI sub-trees.
435
301
  * @returns {void}
436
302
  * @example
437
- * // Unmount calendar gracefully
438
303
  * calendar.destroy();
439
304
  * calendar = null;
440
305
  */
@@ -448,6 +313,9 @@ export default class EventResource {
448
313
  this.onCellClick = null;
449
314
  this.onEventClick = null;
450
315
  this.fetchEvents = null;
316
+ this.fetchRooms = null;
317
+ this.fetchTimeSlots = null;
318
+ this.fetchHolidays = null;
451
319
  this.renderRoomHeader = null;
452
320
  this.renderTimeSlotHeader = null;
453
321
 
@@ -461,20 +329,10 @@ export default class EventResource {
461
329
 
462
330
  // --- DOM Creation & Rendering ---
463
331
 
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
332
  _renderToolbar = (wrapper) => {
474
333
  const toolbar = document.createElement("div");
475
334
  toolbar.className = "er-toolbar";
476
335
 
477
- // Left: Navigation Controls
478
336
  const navGroup = document.createElement("div");
479
337
  navGroup.className = "er-toolbar-group";
480
338
 
@@ -493,7 +351,6 @@ export default class EventResource {
493
351
  btnNext.textContent = "▶";
494
352
  btnNext.onclick = () => this.navigate("next");
495
353
 
496
- // Date Picker HTML5 Input Integration
497
354
  const datePicker = document.createElement("input");
498
355
  datePicker.type = "date";
499
356
  datePicker.className = "er-date-picker";
@@ -506,11 +363,9 @@ export default class EventResource {
506
363
 
507
364
  navGroup.append(btnPrev, btnToday, btnNext, datePicker);
508
365
 
509
- // Right: View Framework Mode Toggles & Freezing Management Elements
510
366
  const viewGroup = document.createElement("div");
511
367
  viewGroup.className = "er-toolbar-group";
512
368
 
513
- // Append Client Extensible Custom Action Elements
514
369
  this.customButtons.forEach((btnConfig) => {
515
370
  const customBtn = document.createElement("button");
516
371
  customBtn.className = `er-btn ${btnConfig.className || ""}`.trim();
@@ -530,7 +385,6 @@ export default class EventResource {
530
385
  btnWeekly.onclick = () => this.setView("weekly");
531
386
 
532
387
  viewGroup.append(btnDaily, btnWeekly);
533
-
534
388
  toolbar.append(navGroup, viewGroup);
535
389
 
536
390
  const btnFreeze = document.createElement("button");
@@ -542,7 +396,6 @@ export default class EventResource {
542
396
  };
543
397
  navGroup.appendChild(btnFreeze);
544
398
 
545
- // Active Holiday Indicator Check
546
399
  const currentHoliday = this._getHolidayForDate(this.currentDate);
547
400
  if (currentHoliday) {
548
401
  const holidayBadge = document.createElement("span");
@@ -555,15 +408,17 @@ export default class EventResource {
555
408
  };
556
409
 
557
410
  /**
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.
411
+ * Manual render hook. Triggers a complete synchronization of the Skeleton UI and the Data Layer.
560
412
  * @returns {void}
561
- * @private
562
413
  * @example
563
- * // Synchronous UI redraw
564
- * this.render();
414
+ * calendar.render();
565
415
  */
566
416
  render = () => {
417
+ this._renderSkeleton();
418
+ this._renderEvents();
419
+ };
420
+
421
+ _renderSkeleton = () => {
567
422
  this.container.innerHTML = "";
568
423
 
569
424
  const wrapper = document.createElement("div");
@@ -578,13 +433,12 @@ export default class EventResource {
578
433
 
579
434
  const grid = document.createElement("div");
580
435
  grid.className = `er-grid ${this.stickyHeaders ? "er-sticky" : ""}`.trim();
581
- grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length}, minmax(120px, 1fr))`;
436
+ grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length || 1}, minmax(120px, 1fr))`;
582
437
 
583
438
  const corner = document.createElement("div");
584
439
  corner.className = "er-header-cell er-corner";
585
440
  grid.appendChild(corner);
586
441
 
587
- // Column Map Processing Loop
588
442
  this.timeSlots.forEach((time) => {
589
443
  const timeHeader = document.createElement("div");
590
444
  timeHeader.className = "er-header-cell er-time-header";
@@ -600,7 +454,6 @@ export default class EventResource {
600
454
 
601
455
  const activeHoliday = this._getHolidayForDate(this.currentDate);
602
456
 
603
- // Row Matrix Layout Intercept Processing Loops
604
457
  this.rooms.forEach((room, rowIndex) => {
605
458
  const roomHeader = document.createElement("div");
606
459
  roomHeader.className = "er-header-cell er-room-header";
@@ -617,228 +470,101 @@ export default class EventResource {
617
470
  const cell = document.createElement("div");
618
471
  cell.className = `er-grid-cell ${activeHoliday ? "er-holiday-cell" : ""}`;
619
472
 
620
- const cellEvents = this.eventsMap.get(`${room.id}-${time.id}`) || [];
473
+ cell.dataset.roomId = room.id;
474
+ cell.dataset.timeId = time.id;
621
475
 
622
476
  cell.addEventListener("click", () => {
623
477
  if (this.onCellClick) {
478
+ const currentEvents =
479
+ this.eventsMap.get(`${room.id}::${time.id}`) || [];
480
+
624
481
  this.onCellClick({
625
482
  date: this.currentDate,
626
483
  view: this.currentView,
627
484
  holiday: activeHoliday,
628
485
  row: { index: rowIndex, data: room },
629
486
  col: { index: colIndex, data: time },
630
- cell: { roomId: room.id, timeId: time.id, events: cellEvents },
487
+ cell: { roomId: room.id, timeId: time.id, events: currentEvents },
631
488
  });
632
489
  }
633
490
  });
634
491
 
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
492
  grid.appendChild(cell);
670
493
  });
671
494
  });
672
495
 
496
+ if (
497
+ this.rooms.length === 0 &&
498
+ this.timeSlots.length === 0 &&
499
+ this.isFetching
500
+ ) {
501
+ const loadingIndicator = document.createElement("div");
502
+ loadingIndicator.style.padding = "20px";
503
+ loadingIndicator.style.color = "#6b7280";
504
+ loadingIndicator.style.textAlign = "center";
505
+ loadingIndicator.textContent = "Loading grid data...";
506
+ grid.appendChild(loadingIndicator);
507
+ }
508
+
673
509
  gridWrapper.appendChild(grid);
674
510
  wrapper.appendChild(gridWrapper);
675
511
  this.container.appendChild(wrapper);
676
512
  };
677
513
 
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);
514
+ _renderEvents = () => {
515
+ const existingEvents = this.container.querySelectorAll(".er-event");
516
+ existingEvents.forEach((el) => el.remove());
517
+
518
+ const activeHoliday = this._getHolidayForDate(this.currentDate);
519
+
520
+ this.events.forEach((ev) => {
521
+ const cell = this.container.querySelector(
522
+ `[data-room-id="${ev.roomId}"][data-time-id="${ev.timeId}"]`,
523
+ );
524
+
525
+ if (!cell) return;
526
+
527
+ const eventDiv = document.createElement("div");
528
+ eventDiv.className = "er-event";
529
+ eventDiv.style.backgroundColor = ev.color || "#3b82f6";
530
+
531
+ if (typeof this.renderEvent === "function") {
532
+ eventDiv.innerHTML = this.renderEvent(ev);
533
+ } else {
534
+ eventDiv.textContent = ev.title;
535
+ }
536
+
537
+ eventDiv.addEventListener("click", (e) => {
538
+ e.stopPropagation();
539
+ if (this.onEventClick) {
540
+ const sharedEvents =
541
+ this.eventsMap.get(`${ev.roomId}::${ev.timeId}`) || [];
542
+
543
+ const roomIndex = this.rooms.findIndex(
544
+ (r) => String(r.id) === String(ev.roomId),
545
+ );
546
+ const timeIndex = this.timeSlots.findIndex(
547
+ (t) => String(t.id) === String(ev.timeId),
548
+ );
549
+
550
+ this.onEventClick({
551
+ event: ev,
552
+ nativeEvent: e,
553
+ date: this.currentDate,
554
+ view: this.currentView,
555
+ holiday: activeHoliday,
556
+ row: { index: roomIndex, data: this.rooms[roomIndex] },
557
+ col: { index: timeIndex, data: this.timeSlots[timeIndex] },
558
+ cell: {
559
+ roomId: ev.roomId,
560
+ timeId: ev.timeId,
561
+ events: sharedEvents,
562
+ },
563
+ });
564
+ }
565
+ });
566
+
567
+ cell.appendChild(eventDiv);
568
+ });
843
569
  };
844
570
  }
package/src/styles.css ADDED
@@ -0,0 +1,171 @@
1
+ .er-container {
2
+ font-family:
3
+ system-ui,
4
+ -apple-system,
5
+ sans-serif;
6
+ border: 1px solid #e5e7eb;
7
+ border-radius: 8px;
8
+ background: #fff;
9
+ width: 100%;
10
+ display: flex;
11
+ flex-direction: column;
12
+ }
13
+ .er-toolbar {
14
+ display: flex;
15
+ justify-content: space-between;
16
+ align-items: center;
17
+ padding: 12px 16px;
18
+ border-bottom: 1px solid #e5e7eb;
19
+ background: #f9fafb;
20
+ border-radius: 8px 8px 0 0;
21
+ flex-wrap: wrap;
22
+ gap: 12px;
23
+ }
24
+ .er-toolbar-group {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 8px;
28
+ }
29
+ .er-btn {
30
+ padding: 6px 12px;
31
+ background: #fff;
32
+ border: 1px solid #d1d5db;
33
+ border-radius: 6px;
34
+ cursor: pointer;
35
+ font-size: 0.875rem;
36
+ font-weight: 500;
37
+ color: #374151;
38
+ transition: all 0.2s;
39
+ }
40
+ .er-btn:hover {
41
+ background: #f3f4f6;
42
+ }
43
+ .er-btn.active {
44
+ background: #e0e7ff;
45
+ color: #4f46e5;
46
+ border-color: #c7d2fe;
47
+ }
48
+ .er-date-picker {
49
+ padding: 5px 10px;
50
+ border: 1px solid #d1d5db;
51
+ border-radius: 6px;
52
+ font-family: inherit;
53
+ font-size: 0.875rem;
54
+ color: #374151;
55
+ background: #fff;
56
+ cursor: pointer;
57
+ }
58
+ .er-holiday-badge {
59
+ font-size: 0.875rem;
60
+ font-weight: 600;
61
+ color: #059669;
62
+ background: #d1fae5;
63
+ padding: 4px 10px;
64
+ border-radius: 9999px;
65
+ margin-left: 8px;
66
+ }
67
+ .er-grid-wrapper {
68
+ overflow: auto;
69
+ max-height: 65vh;
70
+ width: 100%;
71
+ }
72
+ .er-grid {
73
+ display: grid;
74
+ grid-auto-rows: minmax(60px, auto);
75
+ }
76
+ .er-header-cell {
77
+ padding: 12px;
78
+ font-size: 0.875rem;
79
+ background: #f9fafb;
80
+ border-bottom: 1px solid #e5e7eb;
81
+ border-right: 1px solid #e5e7eb;
82
+ display: flex;
83
+ align-items: center;
84
+ box-sizing: border-box;
85
+ }
86
+
87
+ .er-grid.er-sticky .er-corner {
88
+ position: sticky;
89
+ top: 0;
90
+ left: 0;
91
+ z-index: 3;
92
+ background: #f9fafb;
93
+ }
94
+ .er-grid.er-sticky .er-time-header {
95
+ position: sticky;
96
+ top: 0;
97
+ z-index: 2;
98
+ background: #f9fafb;
99
+ justify-content: center;
100
+ color: #4b5563;
101
+ }
102
+ .er-grid.er-sticky .er-room-header {
103
+ position: sticky;
104
+ left: 0;
105
+ z-index: 2;
106
+ background: #f9fafb;
107
+ justify-content: flex-start;
108
+ }
109
+
110
+ .er-grid:not(.er-sticky) .er-corner,
111
+ .er-grid:not(.er-sticky) .er-time-header,
112
+ .er-grid:not(.er-sticky) .er-room-header {
113
+ position: static;
114
+ background: #f9fafb;
115
+ }
116
+ .er-grid:not(.er-sticky) .er-time-header {
117
+ justify-content: center;
118
+ }
119
+
120
+ .er-grid-cell {
121
+ border-bottom: 1px solid #e5e7eb;
122
+ border-right: 1px solid #e5e7eb;
123
+ padding: 4px;
124
+ transition: background-color 0.2s;
125
+ cursor: pointer;
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 4px;
129
+ box-sizing: border-box;
130
+ }
131
+ .er-grid-cell:hover {
132
+ background-color: #f3f4f6;
133
+ }
134
+ .er-holiday-cell {
135
+ background-color: #fdfbf7;
136
+ }
137
+ .er-holiday-cell:hover {
138
+ background-color: #fef3c7;
139
+ }
140
+ .er-event {
141
+ padding: 4px 8px;
142
+ border-radius: 4px;
143
+ color: white;
144
+ font-size: 0.75rem;
145
+ font-weight: 500;
146
+ white-space: nowrap;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ cursor: pointer;
150
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
151
+ }
152
+ .er-event:hover {
153
+ transform: scale(1.02);
154
+ z-index: 10;
155
+ }
156
+
157
+ .er-rich-wrapper {
158
+ display: flex;
159
+ flex-direction: column;
160
+ width: 100%;
161
+ gap: 2px;
162
+ }
163
+ .er-rich-title {
164
+ font-weight: 600;
165
+ color: #111827;
166
+ }
167
+ .er-rich-subtitle {
168
+ font-size: 0.75rem;
169
+ color: #6b7280;
170
+ font-weight: 400;
171
+ }