@forcecalendar/core 2.1.70 → 2.3.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.
@@ -47,6 +47,11 @@ export class TimezoneManager {
47
47
  // so formatters are cached per timezone and reused
48
48
  this.formatterCache = new Map();
49
49
 
50
+ // Discovered offset-transition instants per zone:
51
+ // Map<timezone, {from: number, to: number, transitions: number[]}>
52
+ // covering [from, to] with a sorted list of transition timestamps
53
+ this.transitionCache = new Map();
54
+
50
55
  // Cache size management
51
56
  this.maxCacheSize = 1000;
52
57
  // ~20k 15-minute buckets per zone (≈ a few hundred KB worst case) covers
@@ -165,7 +170,11 @@ export class TimezoneManager {
165
170
  }
166
171
  }
167
172
  const tzDate = new Date(year, month - 1, day, hour, minute, second);
168
- offset = -((tzDate.getTime() - date.getTime()) / (1000 * 60));
173
+ // formatToParts carries no milliseconds, so compare against the
174
+ // whole-second part of the input or sub-second noise leaks into
175
+ // the offset (e.g. 660.0042 instead of 660)
176
+ const wholeSecondMs = Math.floor(date.getTime() / 1000) * 1000;
177
+ offset = -((tzDate.getTime() - wholeSecondMs) / (1000 * 60));
169
178
  } catch (e) {
170
179
  // Fallback to database calculation
171
180
  }
@@ -193,6 +202,74 @@ export class TimezoneManager {
193
202
  return offset;
194
203
  }
195
204
 
205
+ /**
206
+ * Find the next instant at which the zone's UTC offset changes
207
+ * @param {string} timezone - Timezone identifier
208
+ * @param {number} fromMs - Search from this timestamp (exclusive)
209
+ * @param {number} toMs - Search up to this timestamp (inclusive)
210
+ * @returns {number} Timestamp of the first offset change after fromMs, or Infinity
211
+ */
212
+ getNextTransition(timezone, fromMs, toMs) {
213
+ if (fromMs >= toMs) {
214
+ return Infinity;
215
+ }
216
+ timezone = this.database.resolveAlias(timezone);
217
+ let cached = this.transitionCache.get(timezone);
218
+ if (!cached || fromMs < cached.from || toMs > cached.to) {
219
+ // Extend coverage generously so repeated expansions over the same
220
+ // span hit the cache
221
+ const from = Math.min(fromMs, cached ? cached.from : fromMs);
222
+ const to = Math.max(toMs, cached ? cached.to : toMs);
223
+ cached = { from, to, transitions: this._scanTransitions(timezone, from, to) };
224
+ this.transitionCache.set(timezone, cached);
225
+ }
226
+ for (const t of cached.transitions) {
227
+ if (t > fromMs) {
228
+ return t <= toMs ? t : Infinity;
229
+ }
230
+ }
231
+ return Infinity;
232
+ }
233
+
234
+ /**
235
+ * Scan a range for offset transitions. Probes in 7-day steps (shorter
236
+ * than any gap between real-world transitions, including Ramadan DST
237
+ * suspensions) and binary-searches each change to the exact instant.
238
+ * @param {string} timezone - Resolved timezone identifier
239
+ * @param {number} fromMs - Range start
240
+ * @param {number} toMs - Range end
241
+ * @returns {number[]} Sorted transition timestamps
242
+ * @private
243
+ */
244
+ _scanTransitions(timezone, fromMs, toMs) {
245
+ const WEEK = 7 * 86400000;
246
+ const transitions = [];
247
+ const offsetAt = ms => this.getTimezoneOffset(new Date(ms), timezone);
248
+ let lo = fromMs;
249
+ let loOffset = offsetAt(lo);
250
+ while (lo < toMs) {
251
+ const hi = Math.min(lo + WEEK, toMs);
252
+ const hiOffset = offsetAt(hi);
253
+ if (hiOffset !== loOffset) {
254
+ // Binary search for the first ms with the new offset
255
+ let a = lo;
256
+ let b = hi;
257
+ while (b - a > 1) {
258
+ const mid = Math.floor((a + b) / 2);
259
+ if (offsetAt(mid) === loOffset) {
260
+ a = mid;
261
+ } else {
262
+ b = mid;
263
+ }
264
+ }
265
+ transitions.push(b);
266
+ loOffset = hiOffset;
267
+ }
268
+ lo = hi;
269
+ }
270
+ return transitions;
271
+ }
272
+
196
273
  /**
197
274
  * Get a cached Intl.DateTimeFormat for a timezone
198
275
  * @param {string} timezone - Timezone identifier
@@ -471,6 +548,7 @@ export class TimezoneManager {
471
548
  clearCache() {
472
549
  this.offsetCache.clear();
473
550
  this.dstCache.clear();
551
+ this.transitionCache.clear();
474
552
  this.cacheHits = 0;
475
553
  this.cacheMisses = 0;
476
554
  }
package/core/types.js CHANGED
@@ -19,6 +19,9 @@
19
19
  * @property {boolean} [recurring=false] - Whether this is a recurring event
20
20
  * @property {RecurrenceRule|string} [recurrenceRule=null] - Recurrence rule (RRULE string or object)
21
21
  * @property {string} [timeZone=null] - IANA timezone for the event
22
+ * @property {string} [endTimeZone=null] - IANA timezone for the event end (cross-timezone events)
23
+ * @property {RecurrenceRule|string} [recurrence=null] - Backward-compatible alias for recurrenceRule
24
+ * @property {string} [category=null] - Single category (alias for a one-element categories array)
22
25
  * @property {EventStatus} [status='confirmed'] - Event status
23
26
  * @property {EventVisibility} [visibility='public'] - Event visibility
24
27
  * @property {Organizer} [organizer=null] - Event organizer
@@ -172,7 +175,7 @@
172
175
  * @property {boolean} isCurrentMonth - Whether this day is in the current month
173
176
  * @property {boolean} isToday - Whether this is today
174
177
  * @property {boolean} isWeekend - Whether this is a weekend day
175
- * @property {import('./core/events/Event.js').Event[]} events - Events for this day
178
+ * @property {import('./events/Event.js').Event[]} events - Events for this day
176
179
  */
177
180
 
178
181
  /**
@@ -191,9 +194,9 @@
191
194
  * @property {string} dayName - Localized day name
192
195
  * @property {boolean} isToday - Whether this is today
193
196
  * @property {boolean} isWeekend - Whether this is a weekend day
194
- * @property {import('./core/events/Event.js').Event[]} events - All events for this day
195
- * @property {Array<import('./core/events/Event.js').Event[]>} overlapGroups - Groups of overlapping events
196
- * @property {function(import('./core/events/Event.js').Event[]): Map<string, EventPosition>} getEventPositions - Function to calculate positions
197
+ * @property {import('./events/Event.js').Event[]} events - All events for this day
198
+ * @property {Array<import('./events/Event.js').Event[]>} overlapGroups - Groups of overlapping events
199
+ * @property {(events: import('./events/Event.js').Event[]) => Map<string, EventPosition>} getEventPositions - Function to calculate positions
197
200
  */
198
201
 
199
202
  /**
@@ -208,7 +211,7 @@
208
211
  * @property {Date} date - Date being displayed
209
212
  * @property {string} dayName - Localized day name
210
213
  * @property {boolean} isToday - Whether this is today
211
- * @property {import('./core/events/Event.js').Event[]} allDayEvents - All-day events
214
+ * @property {import('./events/Event.js').Event[]} allDayEvents - All-day events
212
215
  * @property {HourSlot[]} hours - Hourly time slots
213
216
  */
214
217
 
@@ -216,7 +219,7 @@
216
219
  * @typedef {Object} HourSlot
217
220
  * @property {number} hour - Hour (0-23)
218
221
  * @property {string} time - Formatted time string
219
- * @property {import('./core/events/Event.js').Event[]} events - Events in this hour
222
+ * @property {import('./events/Event.js').Event[]} events - Events in this hour
220
223
  */
221
224
 
222
225
  /**
@@ -233,7 +236,7 @@
233
236
  * @property {Date} date - Date object
234
237
  * @property {string} dayName - Localized day name
235
238
  * @property {boolean} isToday - Whether this is today
236
- * @property {import('./core/events/Event.js').Event[]} events - Events for this day
239
+ * @property {import('./events/Event.js').Event[]} events - Events for this day
237
240
  */
238
241
 
239
242
  /**
@@ -273,9 +276,9 @@
273
276
  /**
274
277
  * @typedef {Object} EventStoreChange
275
278
  * @property {('add'|'update'|'remove'|'clear')} type - Type of change
276
- * @property {import('./core/events/Event.js').Event} [event] - Affected event
277
- * @property {import('./core/events/Event.js').Event} [oldEvent] - Previous event state (for updates)
278
- * @property {import('./core/events/Event.js').Event[]} [oldEvents] - Previous events (for clear)
279
+ * @property {import('./events/Event.js').Event} [event] - Affected event
280
+ * @property {import('./events/Event.js').Event} [oldEvent] - Previous event state (for updates)
281
+ * @property {import('./events/Event.js').Event[]} [oldEvents] - Previous events (for clear)
279
282
  * @property {number} version - Store version number
280
283
  */
281
284
 
@@ -305,16 +308,16 @@
305
308
 
306
309
  /**
307
310
  * @typedef {Object} CalendarPlugin
308
- * @property {function(import('./core/calendar/Calendar.js').Calendar): void} install - Installation function
309
- * @property {function(import('./core/calendar/Calendar.js').Calendar): void} [uninstall] - Cleanup function
311
+ * @property {(calendar: import('./calendar/Calendar.js').Calendar) => void} install - Installation function
312
+ * @property {(calendar: import('./calendar/Calendar.js').Calendar) => void} [uninstall] - Cleanup function
310
313
  */
311
314
 
312
315
  /**
313
- * @typedef {function(any): void} EventListener
316
+ * @typedef {(payload: any) => void} EventListener
314
317
  */
315
318
 
316
319
  /**
317
- * @typedef {function(): void} UnsubscribeFn
320
+ * @typedef {() => void} UnsubscribeFn
318
321
  */
319
322
 
320
323
  /**
package/package.json CHANGED
@@ -1,32 +1,55 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.70",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",
7
7
  "main": "core/index.js",
8
8
  "module": "core/index.js",
9
- "types": "core/types.js",
9
+ "types": "./types/index.d.ts",
10
10
  "sideEffects": false,
11
11
  "scripts": {
12
12
  "test": "node tests/run-all.js",
13
13
  "test:ics": "node tests/integration/test-ics.js",
14
14
  "test:search": "node tests/integration/test-search.js",
15
- "lint": "eslint core/ --ext .js",
16
- "lint:fix": "eslint core/ --ext .js --fix",
15
+ "lint": "eslint core/",
16
+ "lint:fix": "eslint core/ --fix",
17
17
  "format": "prettier --write \"core/**/*.js\"",
18
18
  "format:check": "prettier --check \"core/**/*.js\"",
19
19
  "quality": "npm run lint && npm run format:check",
20
- "sync:version": "node scripts/sync-version.js"
20
+ "sync:version": "node scripts/sync-version.js",
21
+ "build:types": "tsc -p tsconfig.types.json",
22
+ "prepublishOnly": "npm run build:types"
21
23
  },
22
24
  "exports": {
23
- ".": "./core/index.js",
24
- "./calendar": "./core/calendar/Calendar.js",
25
- "./events": "./core/events/Event.js",
26
- "./state": "./core/state/StateManager.js",
27
- "./search": "./core/search/EventSearch.js",
28
- "./ics": "./core/ics/ICSHandler.js",
29
- "./types": "./core/types.js"
25
+ ".": {
26
+ "types": "./types/index.d.ts",
27
+ "default": "./core/index.js"
28
+ },
29
+ "./calendar": {
30
+ "types": "./types/calendar/Calendar.d.ts",
31
+ "default": "./core/calendar/Calendar.js"
32
+ },
33
+ "./events": {
34
+ "types": "./types/events/Event.d.ts",
35
+ "default": "./core/events/Event.js"
36
+ },
37
+ "./state": {
38
+ "types": "./types/state/StateManager.d.ts",
39
+ "default": "./core/state/StateManager.js"
40
+ },
41
+ "./search": {
42
+ "types": "./types/search/EventSearch.d.ts",
43
+ "default": "./core/search/EventSearch.js"
44
+ },
45
+ "./ics": {
46
+ "types": "./types/ics/ICSHandler.d.ts",
47
+ "default": "./core/ics/ICSHandler.js"
48
+ },
49
+ "./types": {
50
+ "types": "./types/types.d.ts",
51
+ "default": "./core/types.js"
52
+ }
30
53
  },
31
54
  "repository": {
32
55
  "type": "git",
@@ -50,20 +73,17 @@
50
73
  },
51
74
  "homepage": "https://github.com/forceCalendar/core#readme",
52
75
  "devDependencies": {
53
- "@babel/core": "^7.29.6",
54
- "@babel/preset-env": "^7.29.7",
55
- "@rollup/plugin-babel": "^7.0.0",
56
- "@rollup/plugin-node-resolve": "^16.0.3",
57
- "@rollup/plugin-terser": "^1.0.0",
58
- "eslint": "^8.50.0",
76
+ "@eslint/js": "^10.0.1",
77
+ "eslint": "^10.6.0",
78
+ "globals": "^17.7.0",
59
79
  "prettier": "^3.0.3",
60
- "rollup": "^4.61.1",
61
- "vite": "^6.4.3"
80
+ "typescript": "^7.0.2"
62
81
  },
63
82
  "files": [
64
83
  "core/**/*.js",
65
84
  "!core/**/*.test.js",
66
85
  "!core/**/*.spec.js",
86
+ "types/**/*.d.ts",
67
87
  "README.md",
68
88
  "LICENSE"
69
89
  ]
@@ -0,0 +1,287 @@
1
+ import { EventStore } from '../events/EventStore.js';
2
+ import { Event } from '../events/Event.js';
3
+ import { StateManager } from '../state/StateManager.js';
4
+ import { TimezoneManager } from '../timezone/TimezoneManager.js';
5
+ /**
6
+ * Calendar - Main calendar class with full timezone support
7
+ * Pure JavaScript, no DOM dependencies
8
+ * Framework agnostic, Locker Service compatible
9
+ */
10
+ export declare class Calendar {
11
+ timezoneManager: TimezoneManager;
12
+ config: {
13
+ events?: import("../types.js").EventData[];
14
+ view: string;
15
+ date: Date;
16
+ weekStartsOn: number;
17
+ locale: string;
18
+ timeZone: string;
19
+ showWeekNumbers: boolean;
20
+ showWeekends: boolean;
21
+ fixedWeekCount: boolean;
22
+ businessHours: {
23
+ start: string;
24
+ end: string;
25
+ };
26
+ };
27
+ eventStore: EventStore;
28
+ state: StateManager;
29
+ listeners: Map<any, any>;
30
+ plugins: Set<any>;
31
+ views: Map<any, any>;
32
+ /**
33
+ * Create a new Calendar instance
34
+ * @param {import('../types.js').CalendarConfig} [config={}] - Configuration options
35
+ */
36
+ constructor(config?: import('../types.js').CalendarConfig);
37
+ /**
38
+ * Set the calendar view
39
+ * @param {import('../types.js').ViewType} viewType - The view type ('month', 'week', 'day', 'list')
40
+ * @param {Date} [date=null] - Optional date to navigate to
41
+ */
42
+ setView(viewType: import('../types.js').ViewType, date?: Date): void;
43
+ /**
44
+ * Get the current view type
45
+ * @returns {import('../types.js').ViewType} The current view type
46
+ */
47
+ getView(): import('../types.js').ViewType;
48
+ /**
49
+ * Navigate to the next period
50
+ */
51
+ next(): void;
52
+ /**
53
+ * Navigate to the previous period
54
+ */
55
+ previous(): void;
56
+ /**
57
+ * Navigate to today
58
+ */
59
+ today(): void;
60
+ /**
61
+ * Navigate to a specific date
62
+ * @param {Date} date - The date to navigate to
63
+ */
64
+ goToDate(date: Date): void;
65
+ /**
66
+ * Alias for goToDate (compat)
67
+ * @param {Date} date - The date to navigate to
68
+ */
69
+ setDate(date: Date): void;
70
+ /**
71
+ * Get the current date
72
+ * @returns {Date}
73
+ */
74
+ getCurrentDate(): Date;
75
+ /**
76
+ * Add an event
77
+ * @param {import('../events/Event.js').Event|import('../types.js').EventData} eventData - Event data or Event instance
78
+ * @returns {import('../events/Event.js').Event} The added event
79
+ */
80
+ addEvent(eventData: import('../events/Event.js').Event | import('../types.js').EventData): import('../events/Event.js').Event;
81
+ /**
82
+ * Update an event
83
+ * @param {string} eventId - The event ID
84
+ * @param {Object} updates - Properties to update
85
+ * @returns {Event} The updated event
86
+ */
87
+ updateEvent(eventId: string, updates: Object): Event;
88
+ /**
89
+ * Remove an event
90
+ * @param {string} eventId - The event ID
91
+ * @returns {boolean} True if removed
92
+ */
93
+ removeEvent(eventId: string): boolean;
94
+ /**
95
+ * Alias for removeEvent (compat)
96
+ * @param {string} eventId - The event ID
97
+ * @returns {boolean} True if removed
98
+ */
99
+ deleteEvent(eventId: string): boolean;
100
+ /**
101
+ * Get an event by ID
102
+ * @param {string} eventId - The event ID
103
+ * @returns {Event|null}
104
+ */
105
+ getEvent(eventId: string): Event | null;
106
+ /**
107
+ * Get all events
108
+ * @returns {Event[]}
109
+ */
110
+ getEvents(): Event[];
111
+ /**
112
+ * Set all events (replaces existing)
113
+ * @param {Event[]} events - Array of events
114
+ */
115
+ setEvents(events: Event[]): void;
116
+ /**
117
+ * Query events with filters
118
+ * @param {Object} filters - Query filters
119
+ * @returns {Event[]}
120
+ */
121
+ queryEvents(filters: Object): Event[];
122
+ /**
123
+ * Get events for a specific date
124
+ * @param {Date} date - The date
125
+ * @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
126
+ * @returns {Event[]}
127
+ */
128
+ getEventsForDate(date: Date, timezone?: string): Event[];
129
+ /**
130
+ * Get events in a date range
131
+ * @param {Date} start - Start date
132
+ * @param {Date} end - End date
133
+ * @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
134
+ * @returns {Event[]}
135
+ */
136
+ getEventsInRange(start: Date, end: Date, timezone?: string): Event[];
137
+ /**
138
+ * Set the calendar's timezone
139
+ * @param {string} timezone - IANA timezone identifier
140
+ */
141
+ setTimezone(timezone: string): void;
142
+ /**
143
+ * Get the current timezone
144
+ * @returns {string} Current timezone
145
+ */
146
+ getTimezone(): string;
147
+ /**
148
+ * Set the calendar locale
149
+ * @param {string} locale - Locale identifier (e.g. 'en-US')
150
+ */
151
+ setLocale(locale: string): void;
152
+ /**
153
+ * Set the week start day
154
+ * @param {number} weekStartsOn - 0 = Sunday, 1 = Monday, etc.
155
+ */
156
+ setWeekStartsOn(weekStartsOn: number): void;
157
+ /**
158
+ * Convert a date from one timezone to another
159
+ * @param {Date} date - Date to convert
160
+ * @param {string} fromTimezone - Source timezone
161
+ * @param {string} toTimezone - Target timezone
162
+ * @returns {Date} Converted date
163
+ */
164
+ convertTimezone(date: Date, fromTimezone: string, toTimezone: string): Date;
165
+ /**
166
+ * Convert a date to the calendar's timezone
167
+ * @param {Date} date - Date to convert
168
+ * @param {string} fromTimezone - Source timezone
169
+ * @returns {Date} Date in calendar timezone
170
+ */
171
+ toCalendarTimezone(date: Date, fromTimezone: string): Date;
172
+ /**
173
+ * Convert a date from the calendar's timezone
174
+ * @param {Date} date - Date in calendar timezone
175
+ * @param {string} toTimezone - Target timezone
176
+ * @returns {Date} Converted date
177
+ */
178
+ fromCalendarTimezone(date: Date, toTimezone: string): Date;
179
+ /**
180
+ * Format a date in a specific timezone
181
+ * @param {Date} date - Date to format
182
+ * @param {string} [timezone] - Timezone for formatting (defaults to calendar timezone)
183
+ * @param {Object} [options] - Formatting options
184
+ * @returns {string} Formatted date string
185
+ */
186
+ formatInTimezone(date: Date, timezone?: string, options?: Object): string;
187
+ /**
188
+ * Get list of common timezones with offsets
189
+ * @returns {Array<{value: string, label: string, offset: string}>} Timezone list
190
+ */
191
+ getTimezones(): Array<{
192
+ value: string;
193
+ label: string;
194
+ offset: string;
195
+ }>;
196
+ /**
197
+ * Get overlapping event groups for a date
198
+ * @param {Date} date - The date to check
199
+ * @param {boolean} timedOnly - Only include timed events
200
+ * @returns {Array<Event[]>} Array of event groups that overlap
201
+ */
202
+ getOverlapGroups(date: Date, timedOnly?: boolean): Array<Event[]>;
203
+ /**
204
+ * Calculate event positions for rendering
205
+ * @param {Event[]} events - Array of overlapping events
206
+ * @returns {Map<string, {column: number, totalColumns: number}>} Position data
207
+ */
208
+ calculateEventPositions(events: Event[]): Map<string, {
209
+ column: number;
210
+ totalColumns: number;
211
+ }>;
212
+ /**
213
+ * Get the current view's data
214
+ * @returns {import('../types.js').MonthViewData|import('../types.js').WeekViewData|import('../types.js').DayViewData|import('../types.js').ListViewData|null} View-specific data
215
+ */
216
+ getViewData(): import('../types.js').MonthViewData | import('../types.js').WeekViewData | import('../types.js').DayViewData | import('../types.js').ListViewData | null;
217
+ /**
218
+ * Get month view data
219
+ * @private
220
+ */
221
+ private _getMonthViewData;
222
+ /**
223
+ * Get week view data
224
+ * @private
225
+ */
226
+ private _getWeekViewData;
227
+ /**
228
+ * Get day view data
229
+ * @private
230
+ */
231
+ private _getDayViewData;
232
+ /**
233
+ * Get list view data
234
+ * @private
235
+ */
236
+ private _getListViewData;
237
+ /**
238
+ * Select an event
239
+ * @param {string} eventId - Event ID to select
240
+ */
241
+ selectEvent(eventId: string): void;
242
+ /**
243
+ * Clear event selection
244
+ */
245
+ clearEventSelection(): void;
246
+ /**
247
+ * Select a date
248
+ * @param {Date} date - Date to select
249
+ */
250
+ selectDate(date: Date): void;
251
+ /**
252
+ * Clear date selection
253
+ */
254
+ clearDateSelection(): void;
255
+ /**
256
+ * Subscribe to calendar events
257
+ * @param {string} eventName - Event name
258
+ * @param {Function} callback - Callback function
259
+ * @returns {Function} Unsubscribe function
260
+ */
261
+ on(eventName: string, callback: Function): Function;
262
+ /**
263
+ * Unsubscribe from calendar events
264
+ * @param {string} eventName - Event name
265
+ * @param {Function} callback - Callback function
266
+ */
267
+ off(eventName: string, callback: Function): void;
268
+ /**
269
+ * Emit an event
270
+ * @private
271
+ */
272
+ private _emit;
273
+ /**
274
+ * Set up internal listeners
275
+ * @private
276
+ */
277
+ private _setupInternalListeners;
278
+ /**
279
+ * Install a plugin
280
+ * @param {Object} plugin - Plugin object with install method
281
+ */
282
+ use(plugin: Object): void;
283
+ /**
284
+ * Destroy the calendar and clean up
285
+ */
286
+ destroy(): void;
287
+ }