@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.
@@ -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
- static get observedAttributes() {
25
- return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height'];
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
- constructor() {
29
- super();
30
- this.stateManager = null;
31
- this.currentView = null;
32
- this._hasRendered = false; // Track if initial render is complete
33
- this._cachedStyles = null; // Cache styles to avoid recreation
34
- this._busUnsubscribers = [];
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
- initialize() {
38
- // Initialize state manager with config from attributes
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
- setupEventListeners() {
57
- // Clean up any existing subscriptions before re-subscribing
58
- this._busUnsubscribers.forEach(unsub => unsub());
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
- handleStateChange(newState, oldState) {
105
- // If not yet rendered, do nothing (mount will handle initial render)
106
- if (!this._hasRendered) {
107
- return;
108
- }
109
-
110
- // Check what changed
111
- const viewChanged = newState.view !== oldState?.view;
112
- const dateChanged = newState.currentDate?.getTime() !== oldState?.currentDate?.getTime();
113
- const eventsChanged = newState.events !== oldState?.events;
114
- const loadingChanged = newState.loading !== oldState?.loading;
115
- const errorChanged = newState.error !== oldState?.error;
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
- * Update only the title text (no DOM recreation)
151
- */
152
- _updateTitle() {
153
- const titleEl = this.$('.fc-title');
154
- if (titleEl) {
155
- const state = this.stateManager.getState();
156
- titleEl.textContent = this.getTitle(state.currentDate, state.view);
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
- * Update view button active states (no DOM recreation)
162
- */
163
- _updateViewButtons() {
164
- const state = this.stateManager.getState();
165
- this.$$('[data-view]').forEach(button => {
166
- const isActive = button.dataset.view === state.view;
167
- button.classList.toggle('active', isActive);
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
- * Switch to a different view type
173
- */
174
- _switchView() {
175
- const container = this.$('#calendar-view-container');
176
- if (!container) return;
177
-
178
- // Clean up previous view
179
- if (this._currentViewInstance) {
180
- if (this._currentViewInstance.cleanup) {
181
- this._currentViewInstance.cleanup();
182
- }
183
- }
184
-
185
- // Create new view using renderer classes
186
- try {
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
- * Re-render only the view content (not header)
206
- */
207
- _updateViewContent() {
208
- if (this._currentViewInstance && this._currentViewInstance.render) {
209
- this._currentViewInstance.render();
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
- * Toggle loading overlay without rebuilding the component tree.
215
- */
216
- _updateLoadingState(isLoading) {
217
- const loadingEl = this.$('.fc-loading');
218
- const viewContainer = this.$('.fc-view-container');
219
- if (loadingEl) {
220
- loadingEl.style.display = isLoading ? 'flex' : 'none';
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
- mount() {
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
- loadView(viewType) {
233
- if (!viewType || this.currentView === viewType) return;
234
- this.currentView = viewType;
235
- this._switchView();
236
- this._updateViewButtons();
237
- this._updateTitle();
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
- getStyles() {
241
- const height = this.getAttribute('height') || '800px';
257
+ getStyles() {
258
+ const height = this.getAttribute('height') || '800px';
242
259
 
243
- return `
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
- template() {
632
- const state = this.stateManager.getState();
633
- const { currentDate, view, loading, error } = state;
648
+ template() {
649
+ const state = this.stateManager.getState();
650
+ const { currentDate, view, loading, error } = state;
634
651
 
635
- if (error) {
636
- return `
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
- const title = this.getTitle(currentDate, view);
662
+ const title = this.getTitle(currentDate, view);
646
663
 
647
- return `
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
- renderView() {
697
- // Use a plain div container - we'll manually instantiate view classes
698
- // This bypasses Locker Service's custom element restrictions
699
- return '<div id="calendar-view-container"></div>';
700
- }
701
-
702
- afterRender() {
703
- // Manually instantiate and mount view renderer (bypasses Locker Service)
704
- const container = this.$('#calendar-view-container');
705
-
706
- // Only create view once per view type change
707
- if (container && this.stateManager && this.currentView) {
708
- // Check if container actually has content (render() clears shadow DOM)
709
- if (this._currentViewInstance && this._currentViewInstance._viewType === this.currentView && container.children.length > 0) {
710
- return;
711
- }
712
-
713
- // Clean up previous view if exists
714
- if (this._currentViewInstance) {
715
- if (this._currentViewInstance.cleanup) {
716
- this._currentViewInstance.cleanup();
717
- }
718
- if (this._viewUnsubscribe) {
719
- this._viewUnsubscribe();
720
- this._viewUnsubscribe = null;
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
- // Listen for day clicks from the view
764
- this.addListener(this.shadowRoot, 'day-click', (e) => {
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
- // Mark initial render as complete for targeted updates
787
- this._hasRendered = true;
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
- month: MonthViewRenderer,
799
- week: WeekViewRenderer,
800
- day: DayViewRenderer
748
+ month: MonthViewRenderer,
749
+ week: WeekViewRenderer,
750
+ day: DayViewRenderer
801
751
  };
802
752
 
803
- const RendererClass = renderers[viewName] || MonthViewRenderer;
804
- return new RendererClass(null, null); // Container and stateManager set after creation
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
- handleNavigation(event) {
808
- const action = event.currentTarget.dataset.action;
809
- switch (action) {
810
- case 'today':
811
- this.stateManager.today();
812
- break;
813
- case 'previous':
814
- this.stateManager.previous();
815
- break;
816
- case 'next':
817
- this.stateManager.next();
818
- break;
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
- handleViewChange(event) {
823
- const view = event.currentTarget.dataset.view;
824
- this.stateManager.setView(view);
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
- getTitle(date, view) {
828
- const locale = this.stateManager.state.config.locale;
829
-
830
- switch (view) {
831
- case 'month':
832
- return DateUtils.formatDate(date, 'month', locale);
833
- case 'week':
834
- const weekStart = DateUtils.startOfWeek(date);
835
- const weekEnd = DateUtils.endOfWeek(date);
836
- return DateUtils.formatDateRange(weekStart, weekEnd, locale);
837
- case 'day':
838
- return DateUtils.formatDate(date, 'long', locale);
839
- default:
840
- return DateUtils.formatDate(date, 'month', locale);
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
- getIcon(name) {
845
- const icons = {
846
- 'chevron-left': `
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
- 'chevron-right': `
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
- 'calendar': `
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
- return icons[name] || '';
864
- }
885
+ return icons[name] || '';
886
+ }
865
887
 
866
- // Public API methods
867
- addEvent(event) {
868
- return this.stateManager.addEvent(event);
869
- }
888
+ // Public API methods
889
+ addEvent(event) {
890
+ return this.stateManager.addEvent(event);
891
+ }
870
892
 
871
- updateEvent(eventId, updates) {
872
- return this.stateManager.updateEvent(eventId, updates);
873
- }
893
+ updateEvent(eventId, updates) {
894
+ return this.stateManager.updateEvent(eventId, updates);
895
+ }
874
896
 
875
- deleteEvent(eventId) {
876
- return this.stateManager.deleteEvent(eventId);
877
- }
897
+ deleteEvent(eventId) {
898
+ return this.stateManager.deleteEvent(eventId);
899
+ }
878
900
 
879
- getEvents() {
880
- return this.stateManager.getEvents();
881
- }
901
+ getEvents() {
902
+ return this.stateManager.getEvents();
903
+ }
882
904
 
883
- setView(view) {
884
- this.stateManager.setView(view);
885
- }
905
+ setView(view) {
906
+ this.stateManager.setView(view);
907
+ }
886
908
 
887
- setDate(date) {
888
- this.stateManager.setDate(date);
889
- }
909
+ setDate(date) {
910
+ this.stateManager.setDate(date);
911
+ }
890
912
 
891
- next() {
892
- this.stateManager.next();
893
- }
913
+ next() {
914
+ this.stateManager.next();
915
+ }
894
916
 
895
- previous() {
896
- this.stateManager.previous();
897
- }
917
+ previous() {
918
+ this.stateManager.previous();
919
+ }
898
920
 
899
- today() {
900
- this.stateManager.today();
901
- }
921
+ today() {
922
+ this.stateManager.today();
923
+ }
902
924
 
903
- destroy() {
904
- this._busUnsubscribers.forEach(unsub => unsub());
905
- this._busUnsubscribers = [];
925
+ destroy() {
926
+ this._busUnsubscribers.forEach(unsub => unsub());
927
+ this._busUnsubscribers = [];
906
928
 
907
- if (this.stateManager) {
908
- this.stateManager.destroy();
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
- customElements.define('forcecal-main', ForceCalendar);
938
+ customElements.define('forcecal-main', ForceCalendar);
917
939
  }