@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.
- package/README.md +119 -0
- package/package.json +9 -0
- 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
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
|
+
}
|