@forcecalendar/interface 1.0.27 → 1.0.28
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 +9 -0
- package/dist/force-calendar-interface.esm.js +102 -55
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +3 -1
- package/src/components/EventForm.js +180 -176
- package/src/components/ForceCalendar.js +414 -392
- package/src/core/BaseComponent.js +146 -144
- package/src/core/EventBus.js +197 -197
- package/src/core/StateManager.js +405 -399
- package/src/index.js +3 -3
- package/src/renderers/BaseViewRenderer.js +195 -192
- package/src/renderers/DayViewRenderer.js +133 -118
- package/src/renderers/MonthViewRenderer.js +74 -72
- package/src/renderers/WeekViewRenderer.js +118 -96
- package/src/utils/DOMUtils.js +277 -277
- package/src/utils/DateUtils.js +164 -164
- package/src/utils/StyleUtils.js +286 -249
|
@@ -19,228 +19,245 @@ import { DayViewRenderer } from '../renderers/DayViewRenderer.js';
|
|
|
19
19
|
// Import EventForm component
|
|
20
20
|
import { EventForm } from './EventForm.js';
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
export class ForceCalendar extends BaseComponent {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
static get observedAttributes() {
|
|
24
|
+
return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.stateManager = null;
|
|
30
|
+
this.currentView = null;
|
|
31
|
+
this._hasRendered = false; // Track if initial render is complete
|
|
32
|
+
this._cachedStyles = null; // Cache styles to avoid recreation
|
|
33
|
+
this._busUnsubscribers = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
initialize() {
|
|
37
|
+
// Initialize state manager with config from attributes
|
|
38
|
+
const config = {
|
|
39
|
+
view: this.getAttribute('view') || 'month',
|
|
40
|
+
date: this.getAttribute('date') ? new Date(this.getAttribute('date')) : new Date(),
|
|
41
|
+
locale: this.getAttribute('locale') || 'en-US',
|
|
42
|
+
timeZone: this.getAttribute('timezone') || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
43
|
+
weekStartsOn: parseInt(this.getAttribute('week-starts-on') || '0')
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.stateManager = new StateManager(config);
|
|
47
|
+
|
|
48
|
+
// Subscribe to state changes
|
|
49
|
+
this.stateManager.subscribe(this.handleStateChange.bind(this));
|
|
50
|
+
|
|
51
|
+
// Listen for events
|
|
52
|
+
this.setupEventListeners();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setupEventListeners() {
|
|
56
|
+
// Clean up any existing subscriptions before re-subscribing
|
|
57
|
+
this._busUnsubscribers.forEach(unsub => unsub());
|
|
58
|
+
this._busUnsubscribers = [];
|
|
59
|
+
|
|
60
|
+
// Navigation events
|
|
61
|
+
this._busUnsubscribers.push(
|
|
62
|
+
eventBus.on('navigation:*', (data, event) => {
|
|
63
|
+
this.emit('calendar-navigate', { action: event.split(':')[1], ...data });
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// View change events
|
|
68
|
+
this._busUnsubscribers.push(
|
|
69
|
+
eventBus.on('view:changed', data => {
|
|
70
|
+
this.emit('calendar-view-change', data);
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const forwardEventAction = (action, data) => {
|
|
75
|
+
this.emit(`calendar-event-${action}`, data);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Event management events (canonical + backward-compatible aliases)
|
|
79
|
+
this._busUnsubscribers.push(
|
|
80
|
+
eventBus.on('event:add', data => {
|
|
81
|
+
forwardEventAction('add', data);
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
this._busUnsubscribers.push(
|
|
85
|
+
eventBus.on('event:update', data => {
|
|
86
|
+
forwardEventAction('update', data);
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
this._busUnsubscribers.push(
|
|
90
|
+
eventBus.on('event:remove', data => {
|
|
91
|
+
forwardEventAction('remove', data);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
this._busUnsubscribers.push(
|
|
95
|
+
eventBus.on('event:added', data => {
|
|
96
|
+
forwardEventAction('add', data);
|
|
97
|
+
this.emit('calendar-event-added', data);
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
this._busUnsubscribers.push(
|
|
101
|
+
eventBus.on('event:updated', data => {
|
|
102
|
+
forwardEventAction('update', data);
|
|
103
|
+
this.emit('calendar-event-updated', data);
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
this._busUnsubscribers.push(
|
|
107
|
+
eventBus.on('event:deleted', data => {
|
|
108
|
+
forwardEventAction('remove', data);
|
|
109
|
+
this.emit('calendar-event-deleted', data);
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Date selection events
|
|
114
|
+
this._busUnsubscribers.push(
|
|
115
|
+
eventBus.on('date:selected', data => {
|
|
116
|
+
this.emit('calendar-date-select', data);
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
handleStateChange(newState, oldState) {
|
|
122
|
+
// If not yet rendered, do nothing (mount will handle initial render)
|
|
123
|
+
if (!this._hasRendered) {
|
|
124
|
+
return;
|
|
26
125
|
}
|
|
27
126
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
127
|
+
// Check what changed
|
|
128
|
+
const viewChanged = newState.view !== oldState?.view;
|
|
129
|
+
const dateChanged = newState.currentDate?.getTime() !== oldState?.currentDate?.getTime();
|
|
130
|
+
const eventsChanged = newState.events !== oldState?.events;
|
|
131
|
+
const loadingChanged = newState.loading !== oldState?.loading;
|
|
132
|
+
const errorChanged = newState.error !== oldState?.error;
|
|
133
|
+
|
|
134
|
+
// For loading/error state changes, do full re-render (rare)
|
|
135
|
+
if (errorChanged) {
|
|
136
|
+
this.render();
|
|
137
|
+
return;
|
|
35
138
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const config = {
|
|
40
|
-
view: this.getAttribute('view') || 'month',
|
|
41
|
-
date: this.getAttribute('date') ? new Date(this.getAttribute('date')) : new Date(),
|
|
42
|
-
locale: this.getAttribute('locale') || 'en-US',
|
|
43
|
-
timeZone: this.getAttribute('timezone') || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
44
|
-
weekStartsOn: parseInt(this.getAttribute('week-starts-on') || '0')
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
this.stateManager = new StateManager(config);
|
|
48
|
-
|
|
49
|
-
// Subscribe to state changes
|
|
50
|
-
this.stateManager.subscribe(this.handleStateChange.bind(this));
|
|
51
|
-
|
|
52
|
-
// Listen for events
|
|
53
|
-
this.setupEventListeners();
|
|
139
|
+
if (loadingChanged) {
|
|
140
|
+
this._updateLoadingState(newState.loading);
|
|
141
|
+
return;
|
|
54
142
|
}
|
|
55
143
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this._busUnsubscribers = [];
|
|
60
|
-
|
|
61
|
-
// Navigation events
|
|
62
|
-
this._busUnsubscribers.push(eventBus.on('navigation:*', (data, event) => {
|
|
63
|
-
this.emit('calendar-navigate', { action: event.split(':')[1], ...data });
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
// View change events
|
|
67
|
-
this._busUnsubscribers.push(eventBus.on('view:changed', (data) => {
|
|
68
|
-
this.emit('calendar-view-change', data);
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
const forwardEventAction = (action, data) => {
|
|
72
|
-
this.emit(`calendar-event-${action}`, data);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Event management events (canonical + backward-compatible aliases)
|
|
76
|
-
this._busUnsubscribers.push(eventBus.on('event:add', (data) => {
|
|
77
|
-
forwardEventAction('add', data);
|
|
78
|
-
}));
|
|
79
|
-
this._busUnsubscribers.push(eventBus.on('event:update', (data) => {
|
|
80
|
-
forwardEventAction('update', data);
|
|
81
|
-
}));
|
|
82
|
-
this._busUnsubscribers.push(eventBus.on('event:remove', (data) => {
|
|
83
|
-
forwardEventAction('remove', data);
|
|
84
|
-
}));
|
|
85
|
-
this._busUnsubscribers.push(eventBus.on('event:added', (data) => {
|
|
86
|
-
forwardEventAction('add', data);
|
|
87
|
-
this.emit('calendar-event-added', data);
|
|
88
|
-
}));
|
|
89
|
-
this._busUnsubscribers.push(eventBus.on('event:updated', (data) => {
|
|
90
|
-
forwardEventAction('update', data);
|
|
91
|
-
this.emit('calendar-event-updated', data);
|
|
92
|
-
}));
|
|
93
|
-
this._busUnsubscribers.push(eventBus.on('event:deleted', (data) => {
|
|
94
|
-
forwardEventAction('remove', data);
|
|
95
|
-
this.emit('calendar-event-deleted', data);
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
// Date selection events
|
|
99
|
-
this._busUnsubscribers.push(eventBus.on('date:selected', (data) => {
|
|
100
|
-
this.emit('calendar-date-select', data);
|
|
101
|
-
}));
|
|
144
|
+
// Update local view reference if needed
|
|
145
|
+
if (viewChanged) {
|
|
146
|
+
this.currentView = newState.view;
|
|
102
147
|
}
|
|
103
148
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// For loading/error state changes, do full re-render (rare)
|
|
118
|
-
if (errorChanged) {
|
|
119
|
-
this.render();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (loadingChanged) {
|
|
123
|
-
this._updateLoadingState(newState.loading);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Update local view reference if needed
|
|
128
|
-
if (viewChanged) {
|
|
129
|
-
this.currentView = newState.view;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Targeted updates based on what changed
|
|
133
|
-
if (viewChanged) {
|
|
134
|
-
// View changed: update title, buttons, and switch view
|
|
135
|
-
this._updateTitle();
|
|
136
|
-
this._updateViewButtons();
|
|
137
|
-
this._switchView();
|
|
138
|
-
} else if (dateChanged) {
|
|
139
|
-
// Date changed: update title and re-render view
|
|
140
|
-
this._updateTitle();
|
|
141
|
-
this._updateViewContent();
|
|
142
|
-
} else if (eventsChanged) {
|
|
143
|
-
// Events changed: only re-render view content
|
|
144
|
-
this._updateViewContent();
|
|
145
|
-
}
|
|
146
|
-
// Selection changes are handled by the view internally, no action needed here
|
|
149
|
+
// Targeted updates based on what changed
|
|
150
|
+
if (viewChanged) {
|
|
151
|
+
// View changed: update title, buttons, and switch view
|
|
152
|
+
this._updateTitle();
|
|
153
|
+
this._updateViewButtons();
|
|
154
|
+
this._switchView();
|
|
155
|
+
} else if (dateChanged) {
|
|
156
|
+
// Date changed: update title and re-render view
|
|
157
|
+
this._updateTitle();
|
|
158
|
+
this._updateViewContent();
|
|
159
|
+
} else if (eventsChanged) {
|
|
160
|
+
// Events changed: only re-render view content
|
|
161
|
+
this._updateViewContent();
|
|
147
162
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
// Selection changes are handled by the view internally, no action needed here
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update only the title text (no DOM recreation)
|
|
168
|
+
*/
|
|
169
|
+
_updateTitle() {
|
|
170
|
+
const titleEl = this.$('.fc-title');
|
|
171
|
+
if (titleEl) {
|
|
172
|
+
const state = this.stateManager.getState();
|
|
173
|
+
titleEl.textContent = this.getTitle(state.currentDate, state.view);
|
|
158
174
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update view button active states (no DOM recreation)
|
|
179
|
+
*/
|
|
180
|
+
_updateViewButtons() {
|
|
181
|
+
const state = this.stateManager.getState();
|
|
182
|
+
this.$$('[data-view]').forEach(button => {
|
|
183
|
+
const isActive = button.dataset.view === state.view;
|
|
184
|
+
button.classList.toggle('active', isActive);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Switch to a different view type
|
|
190
|
+
*/
|
|
191
|
+
_switchView() {
|
|
192
|
+
const container = this.$('#calendar-view-container');
|
|
193
|
+
if (!container) return;
|
|
194
|
+
|
|
195
|
+
// Clean up previous view
|
|
196
|
+
if (this._currentViewInstance) {
|
|
197
|
+
if (this._currentViewInstance.cleanup) {
|
|
198
|
+
this._currentViewInstance.cleanup();
|
|
199
|
+
}
|
|
169
200
|
}
|
|
170
201
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const renderers = {
|
|
188
|
-
month: MonthViewRenderer,
|
|
189
|
-
week: WeekViewRenderer,
|
|
190
|
-
day: DayViewRenderer
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
194
|
-
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
195
|
-
viewRenderer._viewType = this.currentView;
|
|
196
|
-
this._currentViewInstance = viewRenderer;
|
|
197
|
-
viewRenderer.render();
|
|
198
|
-
// Note: No subscription - handleStateChange manages all view updates
|
|
199
|
-
} catch (err) {
|
|
200
|
-
console.error('[ForceCalendar] Error switching view:', err);
|
|
201
|
-
}
|
|
202
|
+
// Create new view using renderer classes
|
|
203
|
+
try {
|
|
204
|
+
const renderers = {
|
|
205
|
+
month: MonthViewRenderer,
|
|
206
|
+
week: WeekViewRenderer,
|
|
207
|
+
day: DayViewRenderer
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
211
|
+
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
212
|
+
viewRenderer._viewType = this.currentView;
|
|
213
|
+
this._currentViewInstance = viewRenderer;
|
|
214
|
+
viewRenderer.render();
|
|
215
|
+
// Note: No subscription - handleStateChange manages all view updates
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('[ForceCalendar] Error switching view:', err);
|
|
202
218
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Re-render only the view content (not header)
|
|
223
|
+
*/
|
|
224
|
+
_updateViewContent() {
|
|
225
|
+
if (this._currentViewInstance && this._currentViewInstance.render) {
|
|
226
|
+
this._currentViewInstance.render();
|
|
211
227
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (viewContainer) {
|
|
223
|
-
viewContainer.style.display = isLoading ? 'none' : 'flex';
|
|
224
|
-
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Toggle loading overlay without rebuilding the component tree.
|
|
232
|
+
*/
|
|
233
|
+
_updateLoadingState(isLoading) {
|
|
234
|
+
const loadingEl = this.$('.fc-loading');
|
|
235
|
+
const viewContainer = this.$('.fc-view-container');
|
|
236
|
+
if (loadingEl) {
|
|
237
|
+
loadingEl.style.display = isLoading ? 'flex' : 'none';
|
|
225
238
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.currentView = this.stateManager.getView();
|
|
229
|
-
super.mount();
|
|
239
|
+
if (viewContainer) {
|
|
240
|
+
viewContainer.style.display = isLoading ? 'none' : 'flex';
|
|
230
241
|
}
|
|
242
|
+
}
|
|
231
243
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
mount() {
|
|
245
|
+
this.currentView = this.stateManager.getView();
|
|
246
|
+
super.mount();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
loadView(viewType) {
|
|
250
|
+
if (!viewType || this.currentView === viewType) return;
|
|
251
|
+
this.currentView = viewType;
|
|
252
|
+
this._switchView();
|
|
253
|
+
this._updateViewButtons();
|
|
254
|
+
this._updateTitle();
|
|
255
|
+
}
|
|
239
256
|
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
getStyles() {
|
|
258
|
+
const height = this.getAttribute('height') || '800px';
|
|
242
259
|
|
|
243
|
-
|
|
260
|
+
return `
|
|
244
261
|
${StyleUtils.getBaseStyles()}
|
|
245
262
|
${StyleUtils.getButtonStyles()}
|
|
246
263
|
${StyleUtils.getGridStyles()}
|
|
@@ -626,25 +643,25 @@ export class ForceCalendar extends BaseComponent {
|
|
|
626
643
|
background: var(--fc-background);
|
|
627
644
|
}
|
|
628
645
|
`;
|
|
629
|
-
|
|
646
|
+
}
|
|
630
647
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
648
|
+
template() {
|
|
649
|
+
const state = this.stateManager.getState();
|
|
650
|
+
const { currentDate, view, loading, error } = state;
|
|
634
651
|
|
|
635
|
-
|
|
636
|
-
|
|
652
|
+
if (error) {
|
|
653
|
+
return `
|
|
637
654
|
<div class="force-calendar">
|
|
638
655
|
<div class="fc-error">
|
|
639
656
|
<p><strong>Error:</strong> ${DOMUtils.escapeHTML(error.message || 'An error occurred')}</p>
|
|
640
657
|
</div>
|
|
641
658
|
</div>
|
|
642
659
|
`;
|
|
643
|
-
|
|
660
|
+
}
|
|
644
661
|
|
|
645
|
-
|
|
662
|
+
const title = this.getTitle(currentDate, view);
|
|
646
663
|
|
|
647
|
-
|
|
664
|
+
return `
|
|
648
665
|
<div class="force-calendar">
|
|
649
666
|
<header class="fc-header">
|
|
650
667
|
<div class="fc-header-left">
|
|
@@ -691,227 +708,232 @@ export class ForceCalendar extends BaseComponent {
|
|
|
691
708
|
<forcecal-event-form id="event-modal"></forcecal-event-form>
|
|
692
709
|
</div>
|
|
693
710
|
`;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Create view renderer using the appropriate renderer class
|
|
725
|
-
try {
|
|
726
|
-
const renderers = {
|
|
727
|
-
month: MonthViewRenderer,
|
|
728
|
-
week: WeekViewRenderer,
|
|
729
|
-
day: DayViewRenderer
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
733
|
-
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
734
|
-
viewRenderer._viewType = this.currentView;
|
|
735
|
-
this._currentViewInstance = viewRenderer;
|
|
736
|
-
viewRenderer.render();
|
|
737
|
-
// Note: No subscription here - handleStateChange manages all view updates
|
|
738
|
-
// via _updateViewContent(), _switchView(), or full re-render
|
|
739
|
-
} catch (err) {
|
|
740
|
-
console.error('[ForceCalendar] Error creating/rendering view:', err);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Add event listeners for buttons using tracked addListener
|
|
745
|
-
this.$$('[data-action]').forEach(button => {
|
|
746
|
-
this.addListener(button, 'click', this.handleNavigation);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
this.$$('[data-view]').forEach(button => {
|
|
750
|
-
this.addListener(button, 'click', this.handleViewChange);
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// Event Modal Handling
|
|
754
|
-
const modal = this.$('#event-modal');
|
|
755
|
-
const createBtn = this.$('#create-event-btn');
|
|
756
|
-
|
|
757
|
-
if (createBtn && modal) {
|
|
758
|
-
this.addListener(createBtn, 'click', () => {
|
|
759
|
-
modal.open(new Date());
|
|
760
|
-
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
renderView() {
|
|
714
|
+
// Use a plain div container - we'll manually instantiate view classes
|
|
715
|
+
// This bypasses Locker Service's custom element restrictions
|
|
716
|
+
return '<div id="calendar-view-container"></div>';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
afterRender() {
|
|
720
|
+
// Manually instantiate and mount view renderer (bypasses Locker Service)
|
|
721
|
+
const container = this.$('#calendar-view-container');
|
|
722
|
+
|
|
723
|
+
// Only create view once per view type change
|
|
724
|
+
if (container && this.stateManager && this.currentView) {
|
|
725
|
+
// Check if container actually has content (render() clears shadow DOM)
|
|
726
|
+
if (
|
|
727
|
+
this._currentViewInstance &&
|
|
728
|
+
this._currentViewInstance._viewType === this.currentView &&
|
|
729
|
+
container.children.length > 0
|
|
730
|
+
) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Clean up previous view if exists
|
|
735
|
+
if (this._currentViewInstance) {
|
|
736
|
+
if (this._currentViewInstance.cleanup) {
|
|
737
|
+
this._currentViewInstance.cleanup();
|
|
761
738
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (modal) {
|
|
766
|
-
modal.open(e.detail.date);
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// Handle event saving
|
|
771
|
-
if (modal) {
|
|
772
|
-
this.addListener(modal, 'save', (e) => {
|
|
773
|
-
const eventData = e.detail;
|
|
774
|
-
// Robust Safari support check for randomUUID
|
|
775
|
-
const id = (window.crypto && typeof window.crypto.randomUUID === 'function')
|
|
776
|
-
? window.crypto.randomUUID()
|
|
777
|
-
: Math.random().toString(36).substring(2, 15);
|
|
778
|
-
|
|
779
|
-
this.stateManager.addEvent({
|
|
780
|
-
id,
|
|
781
|
-
...eventData
|
|
782
|
-
});
|
|
783
|
-
});
|
|
739
|
+
if (this._viewUnsubscribe) {
|
|
740
|
+
this._viewUnsubscribe();
|
|
741
|
+
this._viewUnsubscribe = null;
|
|
784
742
|
}
|
|
743
|
+
}
|
|
785
744
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Create a view renderer instance for the given view type
|
|
792
|
-
* Uses pure JavaScript renderer classes for Salesforce Locker Service compatibility
|
|
793
|
-
* @param {string} viewName - 'month', 'week', or 'day'
|
|
794
|
-
* @returns {BaseViewRenderer} Renderer instance
|
|
795
|
-
*/
|
|
796
|
-
_createViewRenderer(viewName) {
|
|
745
|
+
// Create view renderer using the appropriate renderer class
|
|
746
|
+
try {
|
|
797
747
|
const renderers = {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
748
|
+
month: MonthViewRenderer,
|
|
749
|
+
week: WeekViewRenderer,
|
|
750
|
+
day: DayViewRenderer
|
|
801
751
|
};
|
|
802
752
|
|
|
803
|
-
const RendererClass = renderers[
|
|
804
|
-
|
|
753
|
+
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
754
|
+
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
755
|
+
viewRenderer._viewType = this.currentView;
|
|
756
|
+
this._currentViewInstance = viewRenderer;
|
|
757
|
+
viewRenderer.render();
|
|
758
|
+
// Note: No subscription here - handleStateChange manages all view updates
|
|
759
|
+
// via _updateViewContent(), _switchView(), or full re-render
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.error('[ForceCalendar] Error creating/rendering view:', err);
|
|
762
|
+
}
|
|
805
763
|
}
|
|
806
764
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
765
|
+
// Add event listeners for buttons using tracked addListener
|
|
766
|
+
this.$$('[data-action]').forEach(button => {
|
|
767
|
+
this.addListener(button, 'click', this.handleNavigation);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
this.$$('[data-view]').forEach(button => {
|
|
771
|
+
this.addListener(button, 'click', this.handleViewChange);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Event Modal Handling
|
|
775
|
+
const modal = this.$('#event-modal');
|
|
776
|
+
const createBtn = this.$('#create-event-btn');
|
|
777
|
+
|
|
778
|
+
if (createBtn && modal) {
|
|
779
|
+
this.addListener(createBtn, 'click', () => {
|
|
780
|
+
modal.open(new Date());
|
|
781
|
+
});
|
|
820
782
|
}
|
|
821
783
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
784
|
+
// Listen for day clicks from the view
|
|
785
|
+
this.addListener(this.shadowRoot, 'day-click', e => {
|
|
786
|
+
if (modal) {
|
|
787
|
+
modal.open(e.detail.date);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Handle event saving
|
|
792
|
+
if (modal) {
|
|
793
|
+
this.addListener(modal, 'save', e => {
|
|
794
|
+
const eventData = e.detail;
|
|
795
|
+
// Robust Safari support check for randomUUID
|
|
796
|
+
const id =
|
|
797
|
+
window.crypto && typeof window.crypto.randomUUID === 'function'
|
|
798
|
+
? window.crypto.randomUUID()
|
|
799
|
+
: Math.random().toString(36).substring(2, 15);
|
|
800
|
+
|
|
801
|
+
this.stateManager.addEvent({
|
|
802
|
+
id,
|
|
803
|
+
...eventData
|
|
804
|
+
});
|
|
805
|
+
});
|
|
825
806
|
}
|
|
826
807
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
808
|
+
// Mark initial render as complete for targeted updates
|
|
809
|
+
this._hasRendered = true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Create a view renderer instance for the given view type
|
|
814
|
+
* Uses pure JavaScript renderer classes for Salesforce Locker Service compatibility
|
|
815
|
+
* @param {string} viewName - 'month', 'week', or 'day'
|
|
816
|
+
* @returns {BaseViewRenderer} Renderer instance
|
|
817
|
+
*/
|
|
818
|
+
_createViewRenderer(viewName) {
|
|
819
|
+
const renderers = {
|
|
820
|
+
month: MonthViewRenderer,
|
|
821
|
+
week: WeekViewRenderer,
|
|
822
|
+
day: DayViewRenderer
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const RendererClass = renderers[viewName] || MonthViewRenderer;
|
|
826
|
+
return new RendererClass(null, null); // Container and stateManager set after creation
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
handleNavigation(event) {
|
|
830
|
+
const action = event.currentTarget.dataset.action;
|
|
831
|
+
switch (action) {
|
|
832
|
+
case 'today':
|
|
833
|
+
this.stateManager.today();
|
|
834
|
+
break;
|
|
835
|
+
case 'previous':
|
|
836
|
+
this.stateManager.previous();
|
|
837
|
+
break;
|
|
838
|
+
case 'next':
|
|
839
|
+
this.stateManager.next();
|
|
840
|
+
break;
|
|
842
841
|
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
handleViewChange(event) {
|
|
845
|
+
const view = event.currentTarget.dataset.view;
|
|
846
|
+
this.stateManager.setView(view);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
getTitle(date, view) {
|
|
850
|
+
const locale = this.stateManager.state.config.locale;
|
|
851
|
+
|
|
852
|
+
switch (view) {
|
|
853
|
+
case 'month':
|
|
854
|
+
return DateUtils.formatDate(date, 'month', locale);
|
|
855
|
+
case 'week':
|
|
856
|
+
const weekStart = DateUtils.startOfWeek(date);
|
|
857
|
+
const weekEnd = DateUtils.endOfWeek(date);
|
|
858
|
+
return DateUtils.formatDateRange(weekStart, weekEnd, locale);
|
|
859
|
+
case 'day':
|
|
860
|
+
return DateUtils.formatDate(date, 'long', locale);
|
|
861
|
+
default:
|
|
862
|
+
return DateUtils.formatDate(date, 'month', locale);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
843
865
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
866
|
+
getIcon(name) {
|
|
867
|
+
const icons = {
|
|
868
|
+
'chevron-left': `
|
|
847
869
|
<svg class="fc-icon" viewBox="0 0 24 24">
|
|
848
870
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
|
849
871
|
</svg>
|
|
850
872
|
`,
|
|
851
|
-
|
|
873
|
+
'chevron-right': `
|
|
852
874
|
<svg class="fc-icon" viewBox="0 0 24 24">
|
|
853
875
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
|
854
876
|
</svg>
|
|
855
877
|
`,
|
|
856
|
-
|
|
878
|
+
calendar: `
|
|
857
879
|
<svg class="fc-icon" viewBox="0 0 24 24">
|
|
858
880
|
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/>
|
|
859
881
|
</svg>
|
|
860
882
|
`
|
|
861
|
-
|
|
883
|
+
};
|
|
862
884
|
|
|
863
|
-
|
|
864
|
-
|
|
885
|
+
return icons[name] || '';
|
|
886
|
+
}
|
|
865
887
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
888
|
+
// Public API methods
|
|
889
|
+
addEvent(event) {
|
|
890
|
+
return this.stateManager.addEvent(event);
|
|
891
|
+
}
|
|
870
892
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
893
|
+
updateEvent(eventId, updates) {
|
|
894
|
+
return this.stateManager.updateEvent(eventId, updates);
|
|
895
|
+
}
|
|
874
896
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
897
|
+
deleteEvent(eventId) {
|
|
898
|
+
return this.stateManager.deleteEvent(eventId);
|
|
899
|
+
}
|
|
878
900
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
901
|
+
getEvents() {
|
|
902
|
+
return this.stateManager.getEvents();
|
|
903
|
+
}
|
|
882
904
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
905
|
+
setView(view) {
|
|
906
|
+
this.stateManager.setView(view);
|
|
907
|
+
}
|
|
886
908
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
909
|
+
setDate(date) {
|
|
910
|
+
this.stateManager.setDate(date);
|
|
911
|
+
}
|
|
890
912
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
913
|
+
next() {
|
|
914
|
+
this.stateManager.next();
|
|
915
|
+
}
|
|
894
916
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
917
|
+
previous() {
|
|
918
|
+
this.stateManager.previous();
|
|
919
|
+
}
|
|
898
920
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
921
|
+
today() {
|
|
922
|
+
this.stateManager.today();
|
|
923
|
+
}
|
|
902
924
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
925
|
+
destroy() {
|
|
926
|
+
this._busUnsubscribers.forEach(unsub => unsub());
|
|
927
|
+
this._busUnsubscribers = [];
|
|
906
928
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
super.cleanup();
|
|
929
|
+
if (this.stateManager) {
|
|
930
|
+
this.stateManager.destroy();
|
|
911
931
|
}
|
|
932
|
+
super.cleanup();
|
|
933
|
+
}
|
|
912
934
|
}
|
|
913
935
|
|
|
914
936
|
// Register component
|
|
915
937
|
if (!customElements.get('forcecal-main')) {
|
|
916
|
-
|
|
938
|
+
customElements.define('forcecal-main', ForceCalendar);
|
|
917
939
|
}
|