@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.
|
|
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.
|
|
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 (
|
|
107
|
+
if (viewChanged) {
|
|
87
108
|
this.currentView = newState.view;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
|
-
//
|
|
91
|
-
|
|
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
|
-
|
|
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
|
`;
|