@forcecalendar/interface 1.4.0 → 1.6.0
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 +219 -56
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +17 -17
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +4 -4
- package/src/components/ForceCalendar.js +150 -9
- package/src/core/BaseComponent.js +4 -0
- package/src/core/StateManager.js +235 -0
- package/src/index.js +1 -0
- package/src/utils/StyleUtils.js +28 -0
- package/types/components/ForceCalendar.d.ts +56 -3
- package/types/core/StateManager.d.ts +164 -16
- package/types/index.d.ts +1 -0
- package/types/utils/DateUtils.d.ts +1 -1
- package/types/utils/StyleUtils.d.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forcecalendar/interface",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"types/**/*.d.ts"
|
|
21
21
|
],
|
|
22
22
|
"sideEffects": [
|
|
23
|
+
"./dist/*.js",
|
|
23
24
|
"./src/index.js",
|
|
24
|
-
"./src/components
|
|
25
|
-
"./src/components/EventForm.js"
|
|
25
|
+
"./src/components/*.js"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"dev": "vite",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/core": "^7.28.5",
|
|
60
60
|
"@babel/preset-env": "^7.28.5",
|
|
61
|
-
"@forcecalendar/core": "^2.
|
|
61
|
+
"@forcecalendar/core": "^2.5.0",
|
|
62
62
|
"babel-jest": "^30.2.0",
|
|
63
63
|
"eslint": "^8.57.1",
|
|
64
64
|
"jest": "^30.2.0",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { BaseComponent } from '../core/BaseComponent.js';
|
|
8
8
|
import StateManager from '../core/StateManager.js';
|
|
9
|
-
import { StyleUtils } from '../utils/StyleUtils.js';
|
|
9
|
+
import { StyleUtils, THEME_PRESETS } from '../utils/StyleUtils.js';
|
|
10
10
|
import { DateUtils } from '../utils/DateUtils.js';
|
|
11
11
|
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ export class ForceCalendar extends BaseComponent {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
static get observedAttributes() {
|
|
29
|
-
return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height'];
|
|
29
|
+
return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height', 'theme'];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
constructor() {
|
|
@@ -35,6 +35,7 @@ export class ForceCalendar extends BaseComponent {
|
|
|
35
35
|
this.currentView = null;
|
|
36
36
|
this._hasRendered = false; // Track if initial render is complete
|
|
37
37
|
this._busUnsubscribers = [];
|
|
38
|
+
this._pendingEvents = null; // Snapshot assigned before the state manager exists
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -86,6 +87,30 @@ export class ForceCalendar extends BaseComponent {
|
|
|
86
87
|
|
|
87
88
|
// Listen for events
|
|
88
89
|
this.setupEventListeners();
|
|
90
|
+
|
|
91
|
+
// Frameworks may assign `events` before the element is upgraded (for
|
|
92
|
+
// example when the component is imported lazily); re-run the setter so
|
|
93
|
+
// the snapshot reaches the state manager instead of shadowing the accessor.
|
|
94
|
+
this._upgradeProperty('events');
|
|
95
|
+
if (this._pendingEvents) {
|
|
96
|
+
const pending = this._pendingEvents;
|
|
97
|
+
this._pendingEvents = null;
|
|
98
|
+
this.stateManager.setEvents(pending.events, pending.options);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Re-apply a property that was set as an own data property before the
|
|
104
|
+
* custom element was upgraded, so the class accessor sees the value.
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_upgradeProperty(name) {
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
110
|
+
const value = this[name];
|
|
111
|
+
delete this[name];
|
|
112
|
+
this[name] = value;
|
|
113
|
+
}
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
setupEventListeners() {
|
|
@@ -150,6 +175,21 @@ export class ForceCalendar extends BaseComponent {
|
|
|
150
175
|
})
|
|
151
176
|
);
|
|
152
177
|
|
|
178
|
+
// Snapshot loads (setEvents / events property) — one event per snapshot
|
|
179
|
+
this._busUnsubscribers.push(
|
|
180
|
+
bus.on('events:set', data => {
|
|
181
|
+
this.emit('calendar-events-set', data);
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Visible window changes (view, date or week start), emitted after the
|
|
186
|
+
// navigation / view-change events that caused them
|
|
187
|
+
this._busUnsubscribers.push(
|
|
188
|
+
bus.on('range:changed', data => {
|
|
189
|
+
this.emit('calendar-range-change', data);
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
153
193
|
// Date selection events
|
|
154
194
|
this._busUnsubscribers.push(
|
|
155
195
|
bus.on('date:selected', data => {
|
|
@@ -276,6 +316,17 @@ export class ForceCalendar extends BaseComponent {
|
|
|
276
316
|
}
|
|
277
317
|
|
|
278
318
|
mount() {
|
|
319
|
+
// Re-entrant: connectedCallback runs again whenever the element is moved
|
|
320
|
+
// or re-attached (React reconciliation/portals, LWC re-render, StrictMode
|
|
321
|
+
// double-mount). The StateManager survives unmount(), so only the bindings
|
|
322
|
+
// released there are restored here; after an explicit destroy() start over.
|
|
323
|
+
if (!this.stateManager || !this.stateManager.state) {
|
|
324
|
+
this.initialize();
|
|
325
|
+
}
|
|
326
|
+
if (!this._stateUnsubscribe) {
|
|
327
|
+
this._stateUnsubscribe = this.stateManager.subscribe(this.handleStateChange.bind(this));
|
|
328
|
+
this.setupEventListeners();
|
|
329
|
+
}
|
|
279
330
|
this.currentView = this.stateManager.getView();
|
|
280
331
|
super.mount();
|
|
281
332
|
}
|
|
@@ -801,7 +852,25 @@ export class ForceCalendar extends BaseComponent {
|
|
|
801
852
|
return '<div id="calendar-view-container"></div>';
|
|
802
853
|
}
|
|
803
854
|
|
|
855
|
+
/**
|
|
856
|
+
* Apply a named theme preset (e.g. theme="slds") as host-level custom
|
|
857
|
+
* properties so it cascades into the shadow DOM and stays overridable
|
|
858
|
+
* by page-level --fc-* variables.
|
|
859
|
+
*/
|
|
860
|
+
_applyTheme(name) {
|
|
861
|
+
const preset = THEME_PRESETS[name];
|
|
862
|
+
for (const token of Object.keys(THEME_PRESETS.slds)) {
|
|
863
|
+
if (preset && preset[token]) {
|
|
864
|
+
this.style.setProperty(token, preset[token]);
|
|
865
|
+
} else {
|
|
866
|
+
this.style.removeProperty(token);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
804
871
|
afterRender() {
|
|
872
|
+
this._applyTheme(this.getAttribute('theme'));
|
|
873
|
+
|
|
805
874
|
// Manually instantiate and mount view renderer (bypasses Locker Service)
|
|
806
875
|
const container = this.$('#calendar-view-container');
|
|
807
876
|
|
|
@@ -893,7 +962,19 @@ export class ForceCalendar extends BaseComponent {
|
|
|
893
962
|
}
|
|
894
963
|
|
|
895
964
|
// Mark initial render as complete for targeted updates
|
|
965
|
+
const firstRender = !this._hasRendered;
|
|
896
966
|
this._hasRendered = true;
|
|
967
|
+
|
|
968
|
+
// Announce the initial visible window once the first mount is complete so
|
|
969
|
+
// consumers can load data for it without waiting for a navigation
|
|
970
|
+
if (firstRender && this.stateManager) {
|
|
971
|
+
const state = this.stateManager.getState();
|
|
972
|
+
this.emit('calendar-range-change', {
|
|
973
|
+
...this.stateManager.getVisibleRange(),
|
|
974
|
+
view: state.view,
|
|
975
|
+
date: state.currentDate
|
|
976
|
+
});
|
|
977
|
+
}
|
|
897
978
|
}
|
|
898
979
|
|
|
899
980
|
handleNavigation(event) {
|
|
@@ -973,6 +1054,58 @@ export class ForceCalendar extends BaseComponent {
|
|
|
973
1054
|
return this.stateManager.getEvents();
|
|
974
1055
|
}
|
|
975
1056
|
|
|
1057
|
+
/**
|
|
1058
|
+
* Replace the calendar's events with a complete snapshot, applying only the
|
|
1059
|
+
* differences: unchanged events keep their instance, changed ones are
|
|
1060
|
+
* replaced, new ones are added and events missing from the snapshot are
|
|
1061
|
+
* removed unless `removeMissing` is false. The view re-renders at most once
|
|
1062
|
+
* and a single `calendar-events-set` event describes the change set; no
|
|
1063
|
+
* per-event `calendar-event-add`/`-remove` events are dispatched.
|
|
1064
|
+
*
|
|
1065
|
+
* Before the element is connected the snapshot is stored and applied on
|
|
1066
|
+
* initialisation, in which case `null` is returned.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {Iterable<object|import('../core/StateManager.js').CalendarEvent>} events - Complete snapshot of events
|
|
1069
|
+
* @param {import('../core/StateManager.js').EventsSetOptions} [options={}]
|
|
1070
|
+
* @returns {import('../core/StateManager.js').EventsSetResult|null}
|
|
1071
|
+
*/
|
|
1072
|
+
setEvents(events, options = {}) {
|
|
1073
|
+
if (!this.stateManager) {
|
|
1074
|
+
this._pendingEvents = { events: events ? Array.from(events) : [], options };
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
return this.stateManager.setEvents(events, options);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Declarative form of {@link ForceCalendar#setEvents}: assign a complete
|
|
1082
|
+
* snapshot and the calendar reconciles it with `removeMissing: true`.
|
|
1083
|
+
* Reading it returns the events currently held by the calendar.
|
|
1084
|
+
*
|
|
1085
|
+
* @returns {import('../core/StateManager.js').CalendarEvent[]}
|
|
1086
|
+
*/
|
|
1087
|
+
get events() {
|
|
1088
|
+
if (this.stateManager) return this.stateManager.getEvents();
|
|
1089
|
+
return this._pendingEvents ? this._pendingEvents.events : [];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* @param {Iterable<object|import('../core/StateManager.js').CalendarEvent>|null} events - Complete snapshot of events
|
|
1094
|
+
*/
|
|
1095
|
+
set events(events) {
|
|
1096
|
+
this.setEvents(events);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Get the window of dates the current view covers (leading and trailing
|
|
1101
|
+
* other-month days included). `end` is inclusive.
|
|
1102
|
+
*
|
|
1103
|
+
* @returns {import('../core/StateManager.js').VisibleRange|null} The range, or null before the element is initialised
|
|
1104
|
+
*/
|
|
1105
|
+
getVisibleRange() {
|
|
1106
|
+
return this.stateManager ? this.stateManager.getVisibleRange() : null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
976
1109
|
setView(view) {
|
|
977
1110
|
this.stateManager.setView(view);
|
|
978
1111
|
}
|
|
@@ -994,11 +1127,22 @@ export class ForceCalendar extends BaseComponent {
|
|
|
994
1127
|
}
|
|
995
1128
|
|
|
996
1129
|
unmount() {
|
|
997
|
-
// Called by disconnectedCallback
|
|
998
|
-
|
|
1130
|
+
// Called by disconnectedCallback. Release everything bound to the rendered
|
|
1131
|
+
// tree (subscriptions, view renderer and its timers, DOM listeners) but keep
|
|
1132
|
+
// the StateManager so the element keeps its view, date and events when it
|
|
1133
|
+
// is re-attached. Full teardown is opt-in via destroy().
|
|
1134
|
+
this._releaseBindings();
|
|
999
1135
|
}
|
|
1000
1136
|
|
|
1001
1137
|
destroy() {
|
|
1138
|
+
this._releaseBindings();
|
|
1139
|
+
|
|
1140
|
+
if (this.stateManager) {
|
|
1141
|
+
this.stateManager.destroy();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
_releaseBindings() {
|
|
1002
1146
|
this._busUnsubscribers.forEach(unsub => unsub());
|
|
1003
1147
|
this._busUnsubscribers = [];
|
|
1004
1148
|
|
|
@@ -1009,12 +1153,9 @@ export class ForceCalendar extends BaseComponent {
|
|
|
1009
1153
|
|
|
1010
1154
|
if (this._currentViewInstance && this._currentViewInstance.cleanup) {
|
|
1011
1155
|
this._currentViewInstance.cleanup();
|
|
1012
|
-
this._currentViewInstance = null;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (this.stateManager) {
|
|
1016
|
-
this.stateManager.destroy();
|
|
1017
1156
|
}
|
|
1157
|
+
this._currentViewInstance = null;
|
|
1158
|
+
this._hasRendered = false;
|
|
1018
1159
|
super.cleanup();
|
|
1019
1160
|
}
|
|
1020
1161
|
}
|
|
@@ -31,6 +31,10 @@ export class BaseComponent extends HTMLElement {
|
|
|
31
31
|
disconnectedCallback() {
|
|
32
32
|
this.unmount();
|
|
33
33
|
this.cleanup();
|
|
34
|
+
// Drop the rendered tree so a later re-attach starts from an empty shadow
|
|
35
|
+
// root instead of appending a second style/content pair next to the old one.
|
|
36
|
+
if (this._styleEl) this._styleEl.remove();
|
|
37
|
+
if (this._contentWrapper) this._contentWrapper.remove();
|
|
34
38
|
this._styleEl = null;
|
|
35
39
|
this._contentWrapper = null;
|
|
36
40
|
}
|
package/src/core/StateManager.js
CHANGED
|
@@ -8,6 +8,40 @@
|
|
|
8
8
|
import { Calendar } from '@forcecalendar/core';
|
|
9
9
|
import { EventBus } from './EventBus.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('@forcecalendar/core').Event} CalendarEvent
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} EventsSetOptions
|
|
17
|
+
* @property {boolean} [removeMissing=true] - Remove stored events that are absent from the snapshot
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} EventsSetUpdate
|
|
22
|
+
* @property {CalendarEvent} event - Event now held by the calendar
|
|
23
|
+
* @property {CalendarEvent} oldEvent - Event instance it replaced
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} EventsSetResult
|
|
28
|
+
* @property {CalendarEvent[]} events - All events after the snapshot was applied
|
|
29
|
+
* @property {CalendarEvent[]} added - Events that were not present before
|
|
30
|
+
* @property {EventsSetUpdate[]} updated - Events whose data changed
|
|
31
|
+
* @property {CalendarEvent[]} removed - Events dropped because they were missing from the snapshot
|
|
32
|
+
* @property {CalendarEvent[]} unchanged - Events left untouched (same instances as before)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} VisibleRange
|
|
37
|
+
* @property {Date} start - First instant shown by the current view
|
|
38
|
+
* @property {Date} end - Last instant shown by the current view (inclusive)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {VisibleRange & { view: string, date: Date }} VisibleRangeChange
|
|
43
|
+
*/
|
|
44
|
+
|
|
11
45
|
class StateManager {
|
|
12
46
|
constructor(config = {}) {
|
|
13
47
|
// Each StateManager gets its own EventBus to prevent cross-instance
|
|
@@ -46,6 +80,9 @@ class StateManager {
|
|
|
46
80
|
|
|
47
81
|
// Initial sync of events from Core (in case events were pre-loaded)
|
|
48
82
|
this._syncEventsFromCore({ silent: true });
|
|
83
|
+
|
|
84
|
+
// Remember the visible window so range:changed only fires on real changes
|
|
85
|
+
this._visibleRangeKey = this._rangeKey(this.getVisibleRange());
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
/**
|
|
@@ -183,6 +220,7 @@ class StateManager {
|
|
|
183
220
|
this.calendar.setView(view);
|
|
184
221
|
this.setState({ view });
|
|
185
222
|
this.eventBus.emit('view:changed', { view });
|
|
223
|
+
this._syncVisibleRange();
|
|
186
224
|
}
|
|
187
225
|
|
|
188
226
|
getView() {
|
|
@@ -193,6 +231,7 @@ class StateManager {
|
|
|
193
231
|
this.calendar.goToDate(date);
|
|
194
232
|
this.setState({ currentDate: this.calendar.getCurrentDate() });
|
|
195
233
|
this.eventBus.emit('date:changed', { date: this.state.currentDate });
|
|
234
|
+
this._syncVisibleRange();
|
|
196
235
|
}
|
|
197
236
|
|
|
198
237
|
getCurrentDate() {
|
|
@@ -204,24 +243,28 @@ class StateManager {
|
|
|
204
243
|
this.calendar.next();
|
|
205
244
|
this.setState({ currentDate: this.calendar.getCurrentDate() });
|
|
206
245
|
this.eventBus.emit('navigation:next', { date: this.state.currentDate });
|
|
246
|
+
this._syncVisibleRange();
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
previous() {
|
|
210
250
|
this.calendar.previous();
|
|
211
251
|
this.setState({ currentDate: this.calendar.getCurrentDate() });
|
|
212
252
|
this.eventBus.emit('navigation:previous', { date: this.state.currentDate });
|
|
253
|
+
this._syncVisibleRange();
|
|
213
254
|
}
|
|
214
255
|
|
|
215
256
|
today() {
|
|
216
257
|
this.calendar.today();
|
|
217
258
|
this.setState({ currentDate: this.calendar.getCurrentDate() });
|
|
218
259
|
this.eventBus.emit('navigation:today', { date: this.state.currentDate });
|
|
260
|
+
this._syncVisibleRange();
|
|
219
261
|
}
|
|
220
262
|
|
|
221
263
|
goToDate(date) {
|
|
222
264
|
this.calendar.goToDate(date);
|
|
223
265
|
this.setState({ currentDate: this.calendar.getCurrentDate() });
|
|
224
266
|
this.eventBus.emit('navigation:goto', { date: this.state.currentDate });
|
|
267
|
+
this._syncVisibleRange();
|
|
225
268
|
}
|
|
226
269
|
|
|
227
270
|
// Event management
|
|
@@ -285,6 +328,133 @@ class StateManager {
|
|
|
285
328
|
return this.calendar.getEvents() || [];
|
|
286
329
|
}
|
|
287
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Replace the calendar's events with a complete snapshot, applying only the
|
|
333
|
+
* differences.
|
|
334
|
+
*
|
|
335
|
+
* Unchanged events keep their existing instance, changed ones are replaced,
|
|
336
|
+
* new ones are added and events missing from the snapshot are removed
|
|
337
|
+
* (unless `removeMissing` is false). The state is updated at most once and a
|
|
338
|
+
* single `events:set` bus event carries the change set. The per-event
|
|
339
|
+
* `event:add`/`event:added`/`event:remove`/`event:deleted` events are NOT
|
|
340
|
+
* emitted, so listeners that persist user edits are not triggered by a
|
|
341
|
+
* snapshot load.
|
|
342
|
+
*
|
|
343
|
+
* Uses `Calendar#reconcileEvents` when the installed core provides it
|
|
344
|
+
* (2.4.0+) and falls back to an id-based diff on older cores.
|
|
345
|
+
*
|
|
346
|
+
* @param {Iterable<object|CalendarEvent>} events - Complete snapshot of events
|
|
347
|
+
* @param {EventsSetOptions} [options={}]
|
|
348
|
+
* @returns {EventsSetResult} The applied change set
|
|
349
|
+
* @throws {Error} If an entry fails validation or two entries share an id (an `event:error` bus event is emitted first)
|
|
350
|
+
*/
|
|
351
|
+
setEvents(events, options = {}) {
|
|
352
|
+
const { removeMissing = true } = options;
|
|
353
|
+
const snapshot = events ? Array.from(events) : [];
|
|
354
|
+
|
|
355
|
+
let result;
|
|
356
|
+
try {
|
|
357
|
+
result =
|
|
358
|
+
typeof this.calendar.reconcileEvents === 'function'
|
|
359
|
+
? this.calendar.setEvents(snapshot, { reconcile: true, removeMissing })
|
|
360
|
+
: this._reconcileFallback(snapshot, removeMissing);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
// Nothing has been applied (core rolls the batch back), so surface the
|
|
363
|
+
// problem to the caller instead of leaving the snapshot half-loaded
|
|
364
|
+
this.eventBus.emit('event:error', { action: 'set', events: snapshot, error });
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { added, updated, removed, unchanged } = result;
|
|
369
|
+
/** @type {EventsSetResult} */
|
|
370
|
+
const payload = { events: this.getEvents(), added, updated, removed, unchanged };
|
|
371
|
+
|
|
372
|
+
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
|
|
373
|
+
this.setState({ events: [...payload.events] });
|
|
374
|
+
}
|
|
375
|
+
this.eventBus.emit('events:set', payload);
|
|
376
|
+
return payload;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Id-based diff for cores that predate `Calendar#reconcileEvents`.
|
|
381
|
+
* Works on the core calendar directly so no per-event bus events fire.
|
|
382
|
+
* Equivalence is approximated by comparing the fields present in the
|
|
383
|
+
* snapshot entry, so an entry that only omits fields is treated as unchanged.
|
|
384
|
+
*
|
|
385
|
+
* @param {Array<object|CalendarEvent>} snapshot
|
|
386
|
+
* @param {boolean} removeMissing
|
|
387
|
+
* @returns {{ added: CalendarEvent[], updated: EventsSetUpdate[], removed: CalendarEvent[], unchanged: CalendarEvent[] }}
|
|
388
|
+
* @private
|
|
389
|
+
*/
|
|
390
|
+
_reconcileFallback(snapshot, removeMissing) {
|
|
391
|
+
const incoming = new Map();
|
|
392
|
+
snapshot.forEach(entry => {
|
|
393
|
+
const id = entry && entry.id;
|
|
394
|
+
if (id === undefined || id === null || id === '') {
|
|
395
|
+
throw new Error('Every event in a snapshot must have an id');
|
|
396
|
+
}
|
|
397
|
+
if (incoming.has(id)) {
|
|
398
|
+
throw new Error(`Duplicate event id in snapshot: ${id}`);
|
|
399
|
+
}
|
|
400
|
+
incoming.set(id, entry);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const result = { added: [], updated: [], removed: [], unchanged: [] };
|
|
404
|
+
const existingById = new Map(this.getEvents().map(event => [event.id, event]));
|
|
405
|
+
|
|
406
|
+
if (removeMissing) {
|
|
407
|
+
existingById.forEach((existing, id) => {
|
|
408
|
+
if (!incoming.has(id) && this.calendar.removeEvent(id)) {
|
|
409
|
+
result.removed.push(existing);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
incoming.forEach((entry, id) => {
|
|
415
|
+
const existing = existingById.get(id);
|
|
416
|
+
const data = typeof entry.toObject === 'function' ? entry.toObject() : entry;
|
|
417
|
+
if (!existing) {
|
|
418
|
+
const event = this.calendar.addEvent(entry);
|
|
419
|
+
if (!event) throw new Error(`Failed to add event: ${id}`);
|
|
420
|
+
result.added.push(event);
|
|
421
|
+
} else if (existing === entry || this._isEquivalentFallback(existing, data)) {
|
|
422
|
+
result.unchanged.push(existing);
|
|
423
|
+
} else {
|
|
424
|
+
const event = this.calendar.updateEvent(id, data);
|
|
425
|
+
if (!event) throw new Error(`Failed to update event: ${id}`);
|
|
426
|
+
result.updated.push({ event, oldEvent: existing });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Field-wise comparison of a stored event against snapshot data.
|
|
435
|
+
* @param {CalendarEvent} existing
|
|
436
|
+
* @param {object} data
|
|
437
|
+
* @returns {boolean}
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
_isEquivalentFallback(existing, data) {
|
|
441
|
+
const toTime = value => {
|
|
442
|
+
if (value instanceof Date) return value.getTime();
|
|
443
|
+
if (value === undefined || value === null) return NaN;
|
|
444
|
+
return new Date(value).getTime();
|
|
445
|
+
};
|
|
446
|
+
return Object.keys(data).every(key => {
|
|
447
|
+
const stored = existing[key];
|
|
448
|
+
const incoming = data[key];
|
|
449
|
+
if (key === 'start' || key === 'end') return toTime(stored) === toTime(incoming);
|
|
450
|
+
if (stored === incoming) return true;
|
|
451
|
+
if (stored === undefined || stored === null || incoming === undefined || incoming === null) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
return JSON.stringify(stored) === JSON.stringify(incoming);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
288
458
|
/**
|
|
289
459
|
* Force sync state.events from Core calendar
|
|
290
460
|
* Use this if you've modified events directly on the Core calendar
|
|
@@ -351,6 +521,70 @@ class StateManager {
|
|
|
351
521
|
return enriched;
|
|
352
522
|
}
|
|
353
523
|
|
|
524
|
+
// Visible range
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get the window of dates the current view covers, including leading and
|
|
528
|
+
* trailing days from adjacent months in the month view.
|
|
529
|
+
*
|
|
530
|
+
* `end` is the last millisecond of the window (inclusive), so the pair can
|
|
531
|
+
* be passed straight to {@link StateManager#getEventsInRange}.
|
|
532
|
+
*
|
|
533
|
+
* @returns {VisibleRange}
|
|
534
|
+
*/
|
|
535
|
+
getVisibleRange() {
|
|
536
|
+
const view = this.calendar.getView();
|
|
537
|
+
const date = this.calendar.getCurrentDate();
|
|
538
|
+
|
|
539
|
+
if (view !== 'day') {
|
|
540
|
+
const viewData = this.calendar.getViewData() || {};
|
|
541
|
+
if (viewData.startDate instanceof Date && viewData.endDate instanceof Date) {
|
|
542
|
+
const start = new Date(viewData.startDate);
|
|
543
|
+
let end = new Date(viewData.endDate);
|
|
544
|
+
// The list view reports an exclusive end; normalise it to inclusive
|
|
545
|
+
if (viewData.type === 'list') {
|
|
546
|
+
end = new Date(end.getTime() - 1);
|
|
547
|
+
}
|
|
548
|
+
return { start, end };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Day view (or a view without an explicit window): midnight to midnight
|
|
553
|
+
const start = new Date(date);
|
|
554
|
+
start.setHours(0, 0, 0, 0);
|
|
555
|
+
const end = new Date(start);
|
|
556
|
+
end.setDate(end.getDate() + 1);
|
|
557
|
+
end.setMilliseconds(-1);
|
|
558
|
+
return { start, end };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @param {VisibleRange} range
|
|
563
|
+
* @returns {string}
|
|
564
|
+
* @private
|
|
565
|
+
*/
|
|
566
|
+
_rangeKey(range) {
|
|
567
|
+
return `${range.start.getTime()}:${range.end.getTime()}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Single choke point for `range:changed`: recompute the visible window and
|
|
572
|
+
* emit only when it differs from the last one that was announced.
|
|
573
|
+
* Called after view, date and week-start changes, once their own bus events
|
|
574
|
+
* have been emitted, so listeners see navigation before the range update.
|
|
575
|
+
* @private
|
|
576
|
+
*/
|
|
577
|
+
_syncVisibleRange() {
|
|
578
|
+
if (!this.calendar) return;
|
|
579
|
+
const range = this.getVisibleRange();
|
|
580
|
+
const key = this._rangeKey(range);
|
|
581
|
+
if (key === this._visibleRangeKey) return;
|
|
582
|
+
this._visibleRangeKey = key;
|
|
583
|
+
/** @type {VisibleRangeChange} */
|
|
584
|
+
const payload = { ...range, view: this.state.view, date: this.state.currentDate };
|
|
585
|
+
this.eventBus.emit('range:changed', payload);
|
|
586
|
+
}
|
|
587
|
+
|
|
354
588
|
// Selection management
|
|
355
589
|
selectEvent(event) {
|
|
356
590
|
this.setState({ selectedEvent: event });
|
|
@@ -420,6 +654,7 @@ class StateManager {
|
|
|
420
654
|
// Update calendar configuration if needed
|
|
421
655
|
if (config.weekStartsOn !== undefined) {
|
|
422
656
|
this.calendar.setWeekStartsOn(config.weekStartsOn);
|
|
657
|
+
this._syncVisibleRange();
|
|
423
658
|
}
|
|
424
659
|
if (config.locale !== undefined) {
|
|
425
660
|
this.calendar.setLocale(config.locale);
|
package/src/index.js
CHANGED
package/src/utils/StyleUtils.js
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
* StyleUtils - Styling utilities and theme management
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Named theme presets applied via the <forcecal-main theme="..."> attribute.
|
|
7
|
+
* Each maps design tokens to a platform's visual language.
|
|
8
|
+
*/
|
|
9
|
+
export const THEME_PRESETS = {
|
|
10
|
+
// Salesforce Lightning Design System: looks native on the platform
|
|
11
|
+
slds: {
|
|
12
|
+
'--fc-primary-color': '#0176d3',
|
|
13
|
+
'--fc-primary-hover': '#014486',
|
|
14
|
+
'--fc-primary-light': '#eef4ff',
|
|
15
|
+
'--fc-accent-color': '#0b5cab',
|
|
16
|
+
'--fc-text-color': '#181818',
|
|
17
|
+
'--fc-text-secondary': '#706e6b',
|
|
18
|
+
'--fc-text-light': '#939393',
|
|
19
|
+
'--fc-border-color': '#e5e5e5',
|
|
20
|
+
'--fc-border-color-hover': '#c9c9c9',
|
|
21
|
+
'--fc-background': '#ffffff',
|
|
22
|
+
'--fc-background-alt': '#f3f3f3',
|
|
23
|
+
'--fc-background-hover': '#f3f3f3',
|
|
24
|
+
'--fc-background-active': '#d8e6fe',
|
|
25
|
+
'--fc-danger-color': '#ea001e',
|
|
26
|
+
'--fc-success-color': '#2e844a',
|
|
27
|
+
'--fc-border-radius': '0.25rem',
|
|
28
|
+
'--fc-border-radius-sm': '0.125rem',
|
|
29
|
+
'--fc-font-family': "'Salesforce Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
5
33
|
export class StyleUtils {
|
|
6
34
|
/**
|
|
7
35
|
* Default theme colors
|