@forcecalendar/interface 1.0.16 → 1.0.18
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/dist/force-calendar-interface.esm.js +788 -533
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +200 -182
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EventForm.js +15 -10
- package/src/components/ForceCalendar.js +48 -451
- package/src/core/EventBus.js +66 -0
- package/src/core/StateManager.js +44 -3
- package/src/index.js +7 -1
- package/src/renderers/BaseViewRenderer.js +219 -0
- package/src/renderers/DayViewRenderer.js +199 -0
- package/src/renderers/MonthViewRenderer.js +125 -0
- package/src/renderers/WeekViewRenderer.js +171 -0
- package/src/renderers/index.js +11 -0
package/src/core/EventBus.js
CHANGED
|
@@ -58,6 +58,17 @@ class EventBus {
|
|
|
58
58
|
* Unsubscribe from an event
|
|
59
59
|
*/
|
|
60
60
|
off(eventName, handler) {
|
|
61
|
+
// Handle wildcard pattern removal
|
|
62
|
+
if (eventName.includes('*')) {
|
|
63
|
+
for (const sub of this.wildcardHandlers) {
|
|
64
|
+
if (sub.pattern === eventName && sub.handler === handler) {
|
|
65
|
+
this.wildcardHandlers.delete(sub);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
if (!this.events.has(eventName)) return;
|
|
62
73
|
|
|
63
74
|
const handlers = this.events.get(eventName);
|
|
@@ -71,6 +82,43 @@ class EventBus {
|
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Remove all wildcard handlers matching a pattern
|
|
87
|
+
* @param {string} pattern - Pattern to match (e.g., 'event:*')
|
|
88
|
+
*/
|
|
89
|
+
offWildcard(pattern) {
|
|
90
|
+
for (const sub of [...this.wildcardHandlers]) {
|
|
91
|
+
if (sub.pattern === pattern) {
|
|
92
|
+
this.wildcardHandlers.delete(sub);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove all handlers (regular and wildcard) for a specific handler function
|
|
99
|
+
* Useful for cleanup when a component is destroyed
|
|
100
|
+
* @param {Function} handler - Handler function to remove
|
|
101
|
+
*/
|
|
102
|
+
offAll(handler) {
|
|
103
|
+
// Remove from regular events
|
|
104
|
+
for (const [eventName, handlers] of this.events) {
|
|
105
|
+
const index = handlers.findIndex(sub => sub.handler === handler);
|
|
106
|
+
if (index > -1) {
|
|
107
|
+
handlers.splice(index, 1);
|
|
108
|
+
}
|
|
109
|
+
if (handlers.length === 0) {
|
|
110
|
+
this.events.delete(eventName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove from wildcard handlers
|
|
115
|
+
for (const sub of [...this.wildcardHandlers]) {
|
|
116
|
+
if (sub.handler === handler) {
|
|
117
|
+
this.wildcardHandlers.delete(sub);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
/**
|
|
75
123
|
* Emit an event
|
|
76
124
|
* @param {string} eventName - Event name
|
|
@@ -157,6 +205,24 @@ class EventBus {
|
|
|
157
205
|
getHandlerCount(eventName) {
|
|
158
206
|
return this.events.has(eventName) ? this.events.get(eventName).length : 0;
|
|
159
207
|
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get wildcard handler count
|
|
211
|
+
*/
|
|
212
|
+
getWildcardHandlerCount() {
|
|
213
|
+
return this.wildcardHandlers.size;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get total handler count (for debugging/monitoring)
|
|
218
|
+
*/
|
|
219
|
+
getTotalHandlerCount() {
|
|
220
|
+
let count = this.wildcardHandlers.size;
|
|
221
|
+
for (const handlers of this.events.values()) {
|
|
222
|
+
count += handlers.length;
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
160
226
|
}
|
|
161
227
|
|
|
162
228
|
// Create singleton instance
|
package/src/core/StateManager.js
CHANGED
|
@@ -85,13 +85,50 @@ class StateManager {
|
|
|
85
85
|
return this.state;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
subscribe(callback) {
|
|
88
|
+
subscribe(callback, subscriberId = null) {
|
|
89
89
|
this.subscribers.add(callback);
|
|
90
|
-
|
|
90
|
+
|
|
91
|
+
// Track subscriber ID for debugging/cleanup
|
|
92
|
+
if (subscriberId) {
|
|
93
|
+
if (!this._subscriberIds) {
|
|
94
|
+
this._subscriberIds = new Map();
|
|
95
|
+
}
|
|
96
|
+
this._subscriberIds.set(subscriberId, callback);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return () => this.unsubscribe(callback, subscriberId);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
unsubscribe(callback) {
|
|
102
|
+
unsubscribe(callback, subscriberId = null) {
|
|
94
103
|
this.subscribers.delete(callback);
|
|
104
|
+
|
|
105
|
+
// Clean up ID tracking
|
|
106
|
+
if (subscriberId && this._subscriberIds) {
|
|
107
|
+
this._subscriberIds.delete(subscriberId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Unsubscribe by subscriber ID
|
|
113
|
+
* @param {string} subscriberId - ID used when subscribing
|
|
114
|
+
*/
|
|
115
|
+
unsubscribeById(subscriberId) {
|
|
116
|
+
if (!this._subscriberIds) return false;
|
|
117
|
+
|
|
118
|
+
const callback = this._subscriberIds.get(subscriberId);
|
|
119
|
+
if (callback) {
|
|
120
|
+
this.subscribers.delete(callback);
|
|
121
|
+
this._subscriberIds.delete(subscriberId);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get subscriber count (for debugging/monitoring)
|
|
129
|
+
*/
|
|
130
|
+
getSubscriberCount() {
|
|
131
|
+
return this.subscribers.size;
|
|
95
132
|
}
|
|
96
133
|
|
|
97
134
|
notifySubscribers(oldState, newState) {
|
|
@@ -362,6 +399,10 @@ class StateManager {
|
|
|
362
399
|
// Destroy
|
|
363
400
|
destroy() {
|
|
364
401
|
this.subscribers.clear();
|
|
402
|
+
if (this._subscriberIds) {
|
|
403
|
+
this._subscriberIds.clear();
|
|
404
|
+
this._subscriberIds = null;
|
|
405
|
+
}
|
|
365
406
|
this.state = null;
|
|
366
407
|
this.calendar = null;
|
|
367
408
|
}
|
package/src/index.js
CHANGED
|
@@ -15,11 +15,17 @@ export { DateUtils } from './utils/DateUtils.js';
|
|
|
15
15
|
export { DOMUtils } from './utils/DOMUtils.js';
|
|
16
16
|
export { StyleUtils } from './utils/StyleUtils.js';
|
|
17
17
|
|
|
18
|
+
// View Renderers (pure JS classes, Locker Service compatible)
|
|
19
|
+
export { BaseViewRenderer } from './renderers/BaseViewRenderer.js';
|
|
20
|
+
export { MonthViewRenderer } from './renderers/MonthViewRenderer.js';
|
|
21
|
+
export { WeekViewRenderer } from './renderers/WeekViewRenderer.js';
|
|
22
|
+
export { DayViewRenderer } from './renderers/DayViewRenderer.js';
|
|
23
|
+
|
|
18
24
|
// Components
|
|
19
25
|
import './components/ForceCalendar.js';
|
|
20
26
|
export { ForceCalendar } from './components/ForceCalendar.js';
|
|
21
27
|
|
|
22
|
-
// Views
|
|
28
|
+
// Views (Web Components - for non-Salesforce usage)
|
|
23
29
|
export { MonthView } from './components/views/MonthView.js';
|
|
24
30
|
export { WeekView } from './components/views/WeekView.js';
|
|
25
31
|
export { DayView } from './components/views/DayView.js';
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseViewRenderer - Foundation for all view renderers
|
|
3
|
+
*
|
|
4
|
+
* Pure JavaScript class (no Web Components) for Salesforce Locker Service compatibility.
|
|
5
|
+
* Provides common functionality for rendering calendar views.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
9
|
+
import { DateUtils } from '../utils/DateUtils.js';
|
|
10
|
+
import { StyleUtils } from '../utils/StyleUtils.js';
|
|
11
|
+
|
|
12
|
+
export class BaseViewRenderer {
|
|
13
|
+
/**
|
|
14
|
+
* @param {HTMLElement} container - The DOM element to render into
|
|
15
|
+
* @param {StateManager} stateManager - The state manager instance
|
|
16
|
+
*/
|
|
17
|
+
constructor(container, stateManager) {
|
|
18
|
+
this.container = container;
|
|
19
|
+
this.stateManager = stateManager;
|
|
20
|
+
this._listeners = [];
|
|
21
|
+
this._scrolled = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the view into the container
|
|
26
|
+
* Must be implemented by subclasses
|
|
27
|
+
*/
|
|
28
|
+
render() {
|
|
29
|
+
throw new Error('render() must be implemented by subclass');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clean up event listeners
|
|
34
|
+
*/
|
|
35
|
+
cleanup() {
|
|
36
|
+
this._listeners.forEach(({ element, event, handler }) => {
|
|
37
|
+
element.removeEventListener(event, handler);
|
|
38
|
+
});
|
|
39
|
+
this._listeners = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add an event listener with automatic cleanup tracking
|
|
44
|
+
* @param {HTMLElement} element
|
|
45
|
+
* @param {string} event
|
|
46
|
+
* @param {Function} handler
|
|
47
|
+
*/
|
|
48
|
+
addListener(element, event, handler) {
|
|
49
|
+
const boundHandler = handler.bind(this);
|
|
50
|
+
element.addEventListener(event, boundHandler);
|
|
51
|
+
this._listeners.push({ element, event, handler: boundHandler });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Escape HTML to prevent XSS
|
|
56
|
+
* @param {string} str
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
escapeHTML(str) {
|
|
60
|
+
if (str == null) return '';
|
|
61
|
+
return DOMUtils.escapeHTML(String(str));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a date is today
|
|
66
|
+
* @param {Date} date
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
isToday(date) {
|
|
70
|
+
const today = new Date();
|
|
71
|
+
return date.getDate() === today.getDate() &&
|
|
72
|
+
date.getMonth() === today.getMonth() &&
|
|
73
|
+
date.getFullYear() === today.getFullYear();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if two dates are the same day
|
|
78
|
+
* @param {Date} date1
|
|
79
|
+
* @param {Date} date2
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
isSameDay(date1, date2) {
|
|
83
|
+
return date1.getDate() === date2.getDate() &&
|
|
84
|
+
date1.getMonth() === date2.getMonth() &&
|
|
85
|
+
date1.getFullYear() === date2.getFullYear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format hour for display (e.g., "9 AM", "2 PM")
|
|
90
|
+
* @param {number} hour - Hour in 24-hour format (0-23)
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
formatHour(hour) {
|
|
94
|
+
const period = hour >= 12 ? 'PM' : 'AM';
|
|
95
|
+
const displayHour = hour % 12 || 12;
|
|
96
|
+
return `${displayHour} ${period}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format time for display (e.g., "9 AM", "2:30 PM")
|
|
101
|
+
* @param {Date} date
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
formatTime(date) {
|
|
105
|
+
const hours = date.getHours();
|
|
106
|
+
const minutes = date.getMinutes();
|
|
107
|
+
const period = hours >= 12 ? 'PM' : 'AM';
|
|
108
|
+
const displayHour = hours % 12 || 12;
|
|
109
|
+
return minutes === 0
|
|
110
|
+
? `${displayHour} ${period}`
|
|
111
|
+
: `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get contrasting text color for a background color
|
|
116
|
+
* Uses WCAG luminance formula
|
|
117
|
+
* @param {string} bgColor - Hex color string
|
|
118
|
+
* @returns {string} 'black' or 'white'
|
|
119
|
+
*/
|
|
120
|
+
getContrastingTextColor(bgColor) {
|
|
121
|
+
if (!bgColor || typeof bgColor !== 'string') return 'white';
|
|
122
|
+
|
|
123
|
+
const color = (bgColor.charAt(0) === '#') ? bgColor.substring(1) : bgColor;
|
|
124
|
+
|
|
125
|
+
if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(color)) {
|
|
126
|
+
return 'white';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fullColor = color.length === 3
|
|
130
|
+
? color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
|
|
131
|
+
: color;
|
|
132
|
+
|
|
133
|
+
const r = parseInt(fullColor.substring(0, 2), 16);
|
|
134
|
+
const g = parseInt(fullColor.substring(2, 4), 16);
|
|
135
|
+
const b = parseInt(fullColor.substring(4, 6), 16);
|
|
136
|
+
|
|
137
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) {
|
|
138
|
+
return 'white';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const uicolors = [r / 255, g / 255, b / 255];
|
|
142
|
+
const c = uicolors.map((col) => {
|
|
143
|
+
if (col <= 0.03928) {
|
|
144
|
+
return col / 12.92;
|
|
145
|
+
}
|
|
146
|
+
return Math.pow((col + 0.055) / 1.055, 2.4);
|
|
147
|
+
});
|
|
148
|
+
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
|
149
|
+
return (L > 0.179) ? 'black' : 'white';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render the "now" indicator line for time-based views
|
|
154
|
+
* @returns {string} HTML string
|
|
155
|
+
*/
|
|
156
|
+
renderNowIndicator() {
|
|
157
|
+
const now = new Date();
|
|
158
|
+
const minutes = now.getHours() * 60 + now.getMinutes();
|
|
159
|
+
return `<div class="fc-now-indicator" style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Render a timed event block
|
|
164
|
+
* @param {Object} event - Event object
|
|
165
|
+
* @param {Object} options - Rendering options
|
|
166
|
+
* @returns {string} HTML string
|
|
167
|
+
*/
|
|
168
|
+
renderTimedEvent(event, options = {}) {
|
|
169
|
+
const { compact = true } = options;
|
|
170
|
+
const start = new Date(event.start);
|
|
171
|
+
const end = new Date(event.end);
|
|
172
|
+
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
|
173
|
+
const durationMinutes = Math.max((end - start) / (1000 * 60), compact ? 20 : 30);
|
|
174
|
+
const color = event.backgroundColor || '#2563eb';
|
|
175
|
+
|
|
176
|
+
const padding = compact ? '4px 8px' : '8px 12px';
|
|
177
|
+
const fontSize = compact ? '11px' : '13px';
|
|
178
|
+
const margin = compact ? '2px' : '12px';
|
|
179
|
+
const rightMargin = compact ? '2px' : '24px';
|
|
180
|
+
const borderRadius = compact ? '4px' : '6px';
|
|
181
|
+
|
|
182
|
+
return `
|
|
183
|
+
<div class="fc-event fc-timed-event" data-event-id="${this.escapeHTML(event.id)}"
|
|
184
|
+
style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px;
|
|
185
|
+
left: ${margin}; right: ${rightMargin};
|
|
186
|
+
background-color: ${color}; border-radius: ${borderRadius};
|
|
187
|
+
padding: ${padding}; font-size: ${fontSize};
|
|
188
|
+
font-weight: 500; color: white; overflow: hidden;
|
|
189
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
190
|
+
cursor: pointer; z-index: 5;">
|
|
191
|
+
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
192
|
+
${this.escapeHTML(event.title)}
|
|
193
|
+
</div>
|
|
194
|
+
<div style="font-size: ${compact ? '10px' : '11px'}; opacity: 0.9;">
|
|
195
|
+
${this.formatTime(start)}${compact ? '' : ' - ' + this.formatTime(end)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Attach common event handlers for day/event clicks
|
|
203
|
+
*/
|
|
204
|
+
attachCommonEventHandlers() {
|
|
205
|
+
// Event click handlers
|
|
206
|
+
this.container.querySelectorAll('.fc-event').forEach(eventEl => {
|
|
207
|
+
this.addListener(eventEl, 'click', (e) => {
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
const eventId = eventEl.dataset.eventId;
|
|
210
|
+
const event = this.stateManager.getEvents().find(ev => ev.id === eventId);
|
|
211
|
+
if (event) {
|
|
212
|
+
this.stateManager.selectEvent(event);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default BaseViewRenderer;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DayViewRenderer - Renders single day calendar view
|
|
3
|
+
*
|
|
4
|
+
* Pure JavaScript renderer for day view, compatible with Salesforce Locker Service.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseViewRenderer } from './BaseViewRenderer.js';
|
|
8
|
+
|
|
9
|
+
export class DayViewRenderer extends BaseViewRenderer {
|
|
10
|
+
constructor(container, stateManager) {
|
|
11
|
+
super(container, stateManager);
|
|
12
|
+
this.hourHeight = 60; // pixels per hour
|
|
13
|
+
this.totalHeight = 24 * this.hourHeight; // 1440px for 24 hours
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render() {
|
|
17
|
+
if (!this.container || !this.stateManager) return;
|
|
18
|
+
|
|
19
|
+
const viewData = this.stateManager.getViewData();
|
|
20
|
+
if (!viewData) {
|
|
21
|
+
this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No data available for day view.</div>';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.cleanup();
|
|
26
|
+
const config = this.stateManager.getState().config;
|
|
27
|
+
const html = this._renderDayView(viewData, config);
|
|
28
|
+
this.container.innerHTML = html;
|
|
29
|
+
this._attachEventHandlers();
|
|
30
|
+
this._scrollToCurrentTime();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_renderDayView(viewData, config) {
|
|
34
|
+
const currentDate = this.stateManager?.getState()?.currentDate || new Date();
|
|
35
|
+
const dayData = this._extractDayData(viewData, currentDate);
|
|
36
|
+
|
|
37
|
+
if (!dayData) {
|
|
38
|
+
return '<div style="padding: 20px; text-align: center; color: #666;">No data available for day view.</div>';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { dayDate, dayName, isToday, allDayEvents, timedEvents } = dayData;
|
|
42
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
43
|
+
|
|
44
|
+
return `
|
|
45
|
+
<div class="fc-day-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
|
|
46
|
+
${this._renderHeader(dayDate, dayName, isToday)}
|
|
47
|
+
${this._renderAllDayRow(allDayEvents, dayDate)}
|
|
48
|
+
${this._renderTimeGrid(timedEvents, isToday, dayDate, hours)}
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_extractDayData(viewData, currentDate) {
|
|
54
|
+
let dayDate, dayName, isToday, allDayEvents, timedEvents;
|
|
55
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
56
|
+
|
|
57
|
+
if (viewData.type === 'day' && viewData.date) {
|
|
58
|
+
// Core day view structure
|
|
59
|
+
dayDate = new Date(viewData.date);
|
|
60
|
+
dayName = viewData.dayName || dayNames[dayDate.getDay()];
|
|
61
|
+
isToday = viewData.isToday !== undefined ? viewData.isToday : this.isToday(dayDate);
|
|
62
|
+
allDayEvents = viewData.allDayEvents || [];
|
|
63
|
+
|
|
64
|
+
// Extract timed events from hours array
|
|
65
|
+
if (viewData.hours && Array.isArray(viewData.hours)) {
|
|
66
|
+
const eventMap = new Map();
|
|
67
|
+
viewData.hours.forEach(hour => {
|
|
68
|
+
(hour.events || []).forEach(evt => {
|
|
69
|
+
if (!eventMap.has(evt.id)) {
|
|
70
|
+
eventMap.set(evt.id, evt);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
timedEvents = Array.from(eventMap.values());
|
|
75
|
+
} else {
|
|
76
|
+
timedEvents = [];
|
|
77
|
+
}
|
|
78
|
+
} else if (viewData.days && viewData.days.length > 0) {
|
|
79
|
+
// Enriched structure with days array
|
|
80
|
+
const dayDataItem = viewData.days.find(d => this.isSameDay(new Date(d.date), currentDate)) || viewData.days[0];
|
|
81
|
+
dayDate = new Date(dayDataItem.date);
|
|
82
|
+
dayName = dayNames[dayDate.getDay()];
|
|
83
|
+
isToday = this.isToday(dayDate);
|
|
84
|
+
const events = dayDataItem.events || [];
|
|
85
|
+
allDayEvents = events.filter(e => e.allDay);
|
|
86
|
+
timedEvents = events.filter(e => !e.allDay);
|
|
87
|
+
} else {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { dayDate, dayName, isToday, allDayEvents, timedEvents };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_renderHeader(dayDate, dayName, isToday) {
|
|
95
|
+
return `
|
|
96
|
+
<div class="fc-day-header" style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
|
|
97
|
+
<div style="border-right: 1px solid #e5e7eb;"></div>
|
|
98
|
+
<div style="padding: 16px 24px;">
|
|
99
|
+
<div style="font-size: 12px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">
|
|
100
|
+
${dayName}
|
|
101
|
+
</div>
|
|
102
|
+
<div style="font-size: 24px; font-weight: 600; margin-top: 4px; ${isToday ? 'color: #dc2626;' : 'color: #111827;'}">
|
|
103
|
+
${dayDate.getDate()}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_renderAllDayRow(allDayEvents, dayDate) {
|
|
111
|
+
return `
|
|
112
|
+
<div class="fc-all-day-row" style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 36px; flex-shrink: 0;">
|
|
113
|
+
<div style="font-size: 9px; color: #6b7280; display: flex; align-items: center; justify-content: center; border-right: 1px solid #e5e7eb; text-transform: uppercase; font-weight: 700;">
|
|
114
|
+
All day
|
|
115
|
+
</div>
|
|
116
|
+
<div class="fc-all-day-cell" data-date="${dayDate.toISOString()}" style="padding: 6px 12px; display: flex; flex-wrap: wrap; gap: 4px;">
|
|
117
|
+
${allDayEvents.map(evt => `
|
|
118
|
+
<div class="fc-event fc-all-day-event" data-event-id="${this.escapeHTML(evt.id)}"
|
|
119
|
+
style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 12px; padding: 4px 8px; border-radius: 4px; color: white; cursor: pointer; font-weight: 500;">
|
|
120
|
+
${this.escapeHTML(evt.title)}
|
|
121
|
+
</div>
|
|
122
|
+
`).join('')}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_renderTimeGrid(timedEvents, isToday, dayDate, hours) {
|
|
129
|
+
return `
|
|
130
|
+
<div id="day-scroll-container" class="fc-time-grid-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
|
|
131
|
+
<div class="fc-time-grid" style="display: grid; grid-template-columns: 60px 1fr; position: relative; height: ${this.totalHeight}px;">
|
|
132
|
+
${this._renderTimeGutter(hours)}
|
|
133
|
+
${this._renderDayColumn(timedEvents, isToday, dayDate, hours)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_renderTimeGutter(hours) {
|
|
140
|
+
return `
|
|
141
|
+
<div class="fc-time-gutter" style="border-right: 1px solid #e5e7eb; background: #fafafa;">
|
|
142
|
+
${hours.map(h => `
|
|
143
|
+
<div style="height: ${this.hourHeight}px; font-size: 11px; color: #6b7280; text-align: right; padding-right: 12px; font-weight: 500;">
|
|
144
|
+
${h === 0 ? '' : this.formatHour(h)}
|
|
145
|
+
</div>
|
|
146
|
+
`).join('')}
|
|
147
|
+
</div>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_renderDayColumn(timedEvents, isToday, dayDate, hours) {
|
|
152
|
+
return `
|
|
153
|
+
<div class="fc-day-column" data-date="${dayDate.toISOString()}" style="position: relative; cursor: pointer;">
|
|
154
|
+
<!-- Hour grid lines -->
|
|
155
|
+
${hours.map(() => `<div style="height: ${this.hourHeight}px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
|
|
156
|
+
|
|
157
|
+
<!-- Now indicator for today -->
|
|
158
|
+
${isToday ? this.renderNowIndicator() : ''}
|
|
159
|
+
|
|
160
|
+
<!-- Timed events -->
|
|
161
|
+
${timedEvents.map(evt => this.renderTimedEvent(evt, { compact: false })).join('')}
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_attachEventHandlers() {
|
|
167
|
+
// Day column click handler
|
|
168
|
+
this.container.querySelectorAll('.fc-day-column').forEach(dayEl => {
|
|
169
|
+
this.addListener(dayEl, 'click', (e) => {
|
|
170
|
+
if (e.target.closest('.fc-event')) return;
|
|
171
|
+
|
|
172
|
+
const date = new Date(dayEl.dataset.date);
|
|
173
|
+
const rect = dayEl.getBoundingClientRect();
|
|
174
|
+
const scrollContainer = this.container.querySelector('#day-scroll-container');
|
|
175
|
+
const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
|
|
176
|
+
|
|
177
|
+
// Calculate time from click position
|
|
178
|
+
date.setHours(Math.floor(y / this.hourHeight), Math.floor((y % this.hourHeight) / (this.hourHeight / 60)), 0, 0);
|
|
179
|
+
this.stateManager.selectDate(date);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Common event handlers (event clicks)
|
|
184
|
+
this.attachCommonEventHandlers();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_scrollToCurrentTime() {
|
|
188
|
+
if (this._scrolled) return;
|
|
189
|
+
|
|
190
|
+
const scrollContainer = this.container.querySelector('#day-scroll-container');
|
|
191
|
+
if (scrollContainer) {
|
|
192
|
+
// Scroll to 8 AM, minus some offset for visibility
|
|
193
|
+
scrollContainer.scrollTop = 8 * this.hourHeight - 50;
|
|
194
|
+
this._scrolled = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default DayViewRenderer;
|