@forcecalendar/interface 1.0.13 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/interface",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "type": "module",
5
5
  "description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
6
6
  "main": "dist/force-calendar-interface.umd.js",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "homepage": "https://interface.forcecalendar.org",
46
46
  "dependencies": {
47
- "@forcecalendar/core": "^1.0.0"
47
+ "@forcecalendar/core": "^2.1.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@babel/core": "^7.28.5",
@@ -9,6 +9,7 @@ import StateManager from '../core/StateManager.js';
9
9
  import eventBus from '../core/EventBus.js';
10
10
  import { StyleUtils } from '../utils/StyleUtils.js';
11
11
  import { DateUtils } from '../utils/DateUtils.js';
12
+ import { DOMUtils } from '../utils/DOMUtils.js';
12
13
 
13
14
  // Import view components
14
15
  import { MonthView } from './views/MonthView.js';
@@ -38,6 +39,8 @@ export class ForceCalendar extends BaseComponent {
38
39
  super();
39
40
  this.stateManager = null;
40
41
  this.currentView = null;
42
+ this._hasRendered = false; // Track if initial render is complete
43
+ this._cachedStyles = null; // Cache styles to avoid recreation
41
44
  }
42
45
 
43
46
  initialize() {
@@ -82,13 +85,105 @@ export class ForceCalendar extends BaseComponent {
82
85
  }
83
86
 
84
87
  handleStateChange(newState, oldState) {
88
+ // If not yet rendered, do nothing (mount will handle initial render)
89
+ if (!this._hasRendered) {
90
+ return;
91
+ }
92
+
93
+ // Check what changed
94
+ const viewChanged = newState.view !== oldState?.view;
95
+ const dateChanged = newState.currentDate?.getTime() !== oldState?.currentDate?.getTime();
96
+ const eventsChanged = newState.events !== oldState?.events;
97
+ const loadingChanged = newState.loading !== oldState?.loading;
98
+ const errorChanged = newState.error !== oldState?.error;
99
+
100
+ // For loading/error state changes, do full re-render (rare)
101
+ if (loadingChanged || errorChanged) {
102
+ this.render();
103
+ return;
104
+ }
105
+
85
106
  // Update local view reference if needed
86
- if (newState.view !== oldState?.view) {
107
+ if (viewChanged) {
87
108
  this.currentView = newState.view;
88
109
  }
89
110
 
90
- // Re-render to update header title, active buttons, and child view
91
- this.render();
111
+ // Targeted updates based on what changed
112
+ if (viewChanged) {
113
+ // View changed: update title, buttons, and switch view
114
+ this._updateTitle();
115
+ this._updateViewButtons();
116
+ this._switchView();
117
+ } else if (dateChanged) {
118
+ // Date changed: update title and re-render view
119
+ this._updateTitle();
120
+ this._updateViewContent();
121
+ } else if (eventsChanged) {
122
+ // Events changed: only re-render view content
123
+ this._updateViewContent();
124
+ }
125
+ // Selection changes are handled by the view internally, no action needed here
126
+ }
127
+
128
+ /**
129
+ * Update only the title text (no DOM recreation)
130
+ */
131
+ _updateTitle() {
132
+ const titleEl = this.$('.fc-title');
133
+ if (titleEl) {
134
+ const state = this.stateManager.getState();
135
+ titleEl.textContent = this.getTitle(state.currentDate, state.view);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Update view button active states (no DOM recreation)
141
+ */
142
+ _updateViewButtons() {
143
+ const state = this.stateManager.getState();
144
+ this.$$('[data-view]').forEach(button => {
145
+ const isActive = button.dataset.view === state.view;
146
+ button.classList.toggle('active', isActive);
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Switch to a different view type
152
+ */
153
+ _switchView() {
154
+ const container = this.$('#calendar-view-container');
155
+ if (!container) return;
156
+
157
+ // Clean up previous view
158
+ if (this._currentViewInstance) {
159
+ if (this._currentViewInstance.cleanup) {
160
+ this._currentViewInstance.cleanup();
161
+ }
162
+ }
163
+
164
+ // Create new view
165
+ try {
166
+ const viewRenderer = this._createViewRenderer(this.currentView);
167
+ if (viewRenderer) {
168
+ viewRenderer._viewType = this.currentView;
169
+ this._currentViewInstance = viewRenderer;
170
+ viewRenderer.stateManager = this.stateManager;
171
+ viewRenderer.container = container;
172
+ viewRenderer.render();
173
+ // Note: No subscription - handleStateChange manages all view updates
174
+ }
175
+ } catch (err) {
176
+ console.error('[ForceCalendar] Error switching view:', err);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Re-render only the view content (not header)
182
+ */
183
+ _updateViewContent() {
184
+ if (this._currentViewInstance && this._currentViewInstance.render) {
185
+ this._currentViewInstance.render();
186
+ }
92
187
  }
93
188
 
94
189
  mount() {
@@ -501,7 +596,7 @@ export class ForceCalendar extends BaseComponent {
501
596
  return `
502
597
  <div class="force-calendar">
503
598
  <div class="fc-error">
504
- <p><strong>Error:</strong> ${error.message || 'An error occurred'}</p>
599
+ <p><strong>Error:</strong> ${DOMUtils.escapeHTML(error.message || 'An error occurred')}</p>
505
600
  </div>
506
601
  </div>
507
602
  `;
@@ -570,13 +665,11 @@ export class ForceCalendar extends BaseComponent {
570
665
  afterRender() {
571
666
  // Manually instantiate and mount view component (bypasses Locker Service)
572
667
  const container = this.$('#calendar-view-container');
573
- console.log('[ForceCalendar] afterRender - container:', !!container, 'stateManager:', !!this.stateManager, 'currentView:', this.currentView);
574
668
 
575
669
  // Only create view once per view type change
576
670
  if (container && this.stateManager && this.currentView) {
577
671
  // Check if container actually has content (render() clears shadow DOM)
578
672
  if (this._currentViewInstance && this._currentViewInstance._viewType === this.currentView && container.children.length > 0) {
579
- console.log('[ForceCalendar] View already exists with content, skipping creation');
580
673
  return;
581
674
  }
582
675
 
@@ -591,8 +684,6 @@ export class ForceCalendar extends BaseComponent {
591
684
  }
592
685
  }
593
686
 
594
- console.log('[ForceCalendar] Creating view for:', this.currentView);
595
-
596
687
  // Create a simple view renderer that doesn't use custom elements
597
688
  try {
598
689
  const viewRenderer = this._createViewRenderer(this.currentView);
@@ -601,21 +692,9 @@ export class ForceCalendar extends BaseComponent {
601
692
  this._currentViewInstance = viewRenderer;
602
693
  viewRenderer.stateManager = this.stateManager;
603
694
  viewRenderer.container = container;
604
-
605
- console.log('[ForceCalendar] Calling viewRenderer.render()');
606
695
  viewRenderer.render();
607
- console.log('[ForceCalendar] viewRenderer.render() completed');
608
-
609
- // Subscribe to state changes (store unsubscribe function)
610
- this._viewUnsubscribe = this.stateManager.subscribe((newState, oldState) => {
611
- // Only re-render on data changes, not view changes
612
- if (newState.events !== oldState?.events ||
613
- newState.currentDate !== oldState?.currentDate) {
614
- if (viewRenderer && viewRenderer.render) {
615
- viewRenderer.render();
616
- }
617
- }
618
- });
696
+ // Note: No subscription here - handleStateChange manages all view updates
697
+ // via _updateViewContent(), _switchView(), or full re-render
619
698
  }
620
699
  } catch (err) {
621
700
  console.error('[ForceCalendar] Error creating/rendering view:', err);
@@ -663,6 +742,9 @@ export class ForceCalendar extends BaseComponent {
663
742
  });
664
743
  });
665
744
  }
745
+
746
+ // Mark initial render as complete for targeted updates
747
+ this._hasRendered = true;
666
748
  }
667
749
 
668
750
  _createViewRenderer(viewName) {
@@ -677,6 +759,11 @@ export class ForceCalendar extends BaseComponent {
677
759
  _listeners: [],
678
760
  _scrolled: false,
679
761
 
762
+ _escapeHTML(str) {
763
+ if (str == null) return '';
764
+ return DOMUtils.escapeHTML(String(str));
765
+ },
766
+
680
767
  cleanup() {
681
768
  this._listeners.forEach(({ element, event, handler }) => {
682
769
  element.removeEventListener(event, handler);
@@ -758,8 +845,8 @@ export class ForceCalendar extends BaseComponent {
758
845
  <div class="fc-day-number" style="font-size: 13px; font-weight: 500; color: ${dayNumColor}; padding: 2px 4px; margin-bottom: 4px; ${todayStyle}">${day.dayOfMonth}</div>
759
846
  <div class="fc-day-events" style="display: flex; flex-direction: column; gap: 2px;">
760
847
  ${visibleEvents.map(evt => `
761
- <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 11px; padding: 2px 6px; border-radius: 3px; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
762
- ${evt.title}
848
+ <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 11px; padding: 2px 6px; border-radius: 3px; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
849
+ ${this._escapeHTML(evt.title)}
763
850
  </div>
764
851
  `).join('')}
765
852
  ${moreCount > 0 ? `<div class="fc-more-events" style="font-size: 10px; color: #6b7280; padding: 2px 4px; font-weight: 500;">+${moreCount} more</div>` : ''}
@@ -818,8 +905,8 @@ export class ForceCalendar extends BaseComponent {
818
905
  ${processedDays.map(day => `
819
906
  <div style="border-right: 1px solid #e5e7eb; padding: 4px; display: flex; flex-direction: column; gap: 2px;">
820
907
  ${day.allDayEvents.map(evt => `
821
- <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 10px; padding: 2px 4px; border-radius: 2px; color: white; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
822
- ${evt.title}
908
+ <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 10px; padding: 2px 4px; border-radius: 2px; color: white; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
909
+ ${this._escapeHTML(evt.title)}
823
910
  </div>
824
911
  `).join('')}
825
912
  </div>
@@ -919,8 +1006,8 @@ export class ForceCalendar extends BaseComponent {
919
1006
  <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;">All day</div>
920
1007
  <div style="padding: 6px 12px; display: flex; flex-wrap: wrap; gap: 4px;">
921
1008
  ${allDayEvents.map(evt => `
922
- <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 12px; padding: 4px 8px; border-radius: 4px; color: white; cursor: pointer; font-weight: 500;">
923
- ${evt.title}
1009
+ <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 12px; padding: 4px 8px; border-radius: 4px; color: white; cursor: pointer; font-weight: 500;">
1010
+ ${this._escapeHTML(evt.title)}
924
1011
  </div>
925
1012
  `).join('')}
926
1013
  </div>
@@ -965,12 +1052,12 @@ export class ForceCalendar extends BaseComponent {
965
1052
  const color = event.backgroundColor || '#2563eb';
966
1053
 
967
1054
  return `
968
- <div class="fc-event" data-event-id="${event.id}"
1055
+ <div class="fc-event" data-event-id="${this._escapeHTML(event.id)}"
969
1056
  style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 2px; right: 2px;
970
1057
  background-color: ${color}; border-radius: 4px; padding: 4px 8px; font-size: 11px;
971
1058
  font-weight: 500; color: white; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
972
1059
  cursor: pointer; z-index: 5;">
973
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${event.title}</div>
1060
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${this._escapeHTML(event.title)}</div>
974
1061
  <div style="font-size: 10px; opacity: 0.9;">${this._formatTime(start)}</div>
975
1062
  </div>
976
1063
  `;
@@ -984,12 +1071,12 @@ export class ForceCalendar extends BaseComponent {
984
1071
  const color = event.backgroundColor || '#2563eb';
985
1072
 
986
1073
  return `
987
- <div class="fc-event" data-event-id="${event.id}"
1074
+ <div class="fc-event" data-event-id="${this._escapeHTML(event.id)}"
988
1075
  style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 12px; right: 24px;
989
1076
  background-color: ${color}; border-radius: 6px; padding: 8px 12px; font-size: 13px;
990
1077
  font-weight: 500; color: white; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
991
1078
  cursor: pointer; z-index: 5;">
992
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${event.title}</div>
1079
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${this._escapeHTML(event.title)}</div>
993
1080
  <div style="font-size: 11px; opacity: 0.9;">${this._formatTime(start)} - ${this._formatTime(end)}</div>
994
1081
  </div>
995
1082
  `;