@forcecalendar/core 0.2.0 → 0.2.1
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/core/calendar/Calendar.js +715 -0
- package/core/calendar/DateUtils.js +553 -0
- package/core/conflicts/ConflictDetector.js +517 -0
- package/core/events/Event.js +914 -0
- package/core/events/EventStore.js +1198 -0
- package/core/events/RRuleParser.js +420 -0
- package/core/events/RecurrenceEngine.js +382 -0
- package/core/ics/ICSHandler.js +389 -0
- package/core/ics/ICSParser.js +475 -0
- package/core/performance/AdaptiveMemoryManager.js +333 -0
- package/core/performance/LRUCache.js +118 -0
- package/core/performance/PerformanceOptimizer.js +523 -0
- package/core/search/EventSearch.js +476 -0
- package/core/state/StateManager.js +546 -0
- package/core/timezone/TimezoneDatabase.js +294 -0
- package/core/timezone/TimezoneManager.js +419 -0
- package/core/types.js +366 -0
- package/package.json +11 -9
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { EventStore } from '../events/EventStore.js';
|
|
2
|
+
import { Event } from '../events/Event.js';
|
|
3
|
+
import { StateManager } from '../state/StateManager.js';
|
|
4
|
+
import { DateUtils } from './DateUtils.js';
|
|
5
|
+
import { TimezoneManager } from '../timezone/TimezoneManager.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calendar - Main calendar class with full timezone support
|
|
9
|
+
* Pure JavaScript, no DOM dependencies
|
|
10
|
+
* Framework agnostic, Locker Service compatible
|
|
11
|
+
*/
|
|
12
|
+
export class Calendar {
|
|
13
|
+
/**
|
|
14
|
+
* Create a new Calendar instance
|
|
15
|
+
* @param {import('../../types.js').CalendarConfig} [config={}] - Configuration options
|
|
16
|
+
*/
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
// Initialize timezone manager first
|
|
19
|
+
this.timezoneManager = new TimezoneManager();
|
|
20
|
+
|
|
21
|
+
// Initialize configuration
|
|
22
|
+
this.config = {
|
|
23
|
+
view: 'month',
|
|
24
|
+
date: new Date(),
|
|
25
|
+
weekStartsOn: 0, // 0 = Sunday
|
|
26
|
+
locale: 'en-US',
|
|
27
|
+
timeZone: config.timeZone || this.timezoneManager.getSystemTimezone(),
|
|
28
|
+
showWeekNumbers: false,
|
|
29
|
+
showWeekends: true,
|
|
30
|
+
fixedWeekCount: true,
|
|
31
|
+
businessHours: {
|
|
32
|
+
start: '09:00',
|
|
33
|
+
end: '17:00'
|
|
34
|
+
},
|
|
35
|
+
...config
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Initialize core components with timezone support
|
|
39
|
+
this.eventStore = new EventStore({ timezone: this.config.timeZone });
|
|
40
|
+
this.state = new StateManager({
|
|
41
|
+
view: this.config.view,
|
|
42
|
+
currentDate: this.config.date,
|
|
43
|
+
weekStartsOn: this.config.weekStartsOn,
|
|
44
|
+
locale: this.config.locale,
|
|
45
|
+
timeZone: this.config.timeZone,
|
|
46
|
+
showWeekNumbers: this.config.showWeekNumbers,
|
|
47
|
+
showWeekends: this.config.showWeekends,
|
|
48
|
+
fixedWeekCount: this.config.fixedWeekCount,
|
|
49
|
+
businessHours: this.config.businessHours
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Event emitter for calendar events
|
|
53
|
+
this.listeners = new Map();
|
|
54
|
+
|
|
55
|
+
// Plugins
|
|
56
|
+
this.plugins = new Set();
|
|
57
|
+
|
|
58
|
+
// View instances (lazy loaded)
|
|
59
|
+
this.views = new Map();
|
|
60
|
+
|
|
61
|
+
// Set up internal listeners
|
|
62
|
+
this._setupInternalListeners();
|
|
63
|
+
|
|
64
|
+
// Load initial events if provided
|
|
65
|
+
if (config.events) {
|
|
66
|
+
this.setEvents(config.events);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set the calendar view
|
|
72
|
+
* @param {import('../../types.js').ViewType} viewType - The view type ('month', 'week', 'day', 'list')
|
|
73
|
+
* @param {Date} [date=null] - Optional date to navigate to
|
|
74
|
+
*/
|
|
75
|
+
setView(viewType, date = null) {
|
|
76
|
+
this.state.setView(viewType);
|
|
77
|
+
|
|
78
|
+
if (date) {
|
|
79
|
+
this.state.setCurrentDate(date);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this._emit('viewChange', {
|
|
83
|
+
view: viewType,
|
|
84
|
+
date: date || this.state.get('currentDate')
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the current view type
|
|
90
|
+
* @returns {import('../../types.js').ViewType} The current view type
|
|
91
|
+
*/
|
|
92
|
+
getView() {
|
|
93
|
+
return this.state.get('view');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Navigate to the next period
|
|
98
|
+
*/
|
|
99
|
+
next() {
|
|
100
|
+
this.state.navigateNext();
|
|
101
|
+
this._emit('navigate', {
|
|
102
|
+
direction: 'next',
|
|
103
|
+
date: this.state.get('currentDate'),
|
|
104
|
+
view: this.state.get('view')
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Navigate to the previous period
|
|
110
|
+
*/
|
|
111
|
+
previous() {
|
|
112
|
+
this.state.navigatePrevious();
|
|
113
|
+
this._emit('navigate', {
|
|
114
|
+
direction: 'previous',
|
|
115
|
+
date: this.state.get('currentDate'),
|
|
116
|
+
view: this.state.get('view')
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Navigate to today
|
|
122
|
+
*/
|
|
123
|
+
today() {
|
|
124
|
+
this.state.navigateToday();
|
|
125
|
+
this._emit('navigate', {
|
|
126
|
+
direction: 'today',
|
|
127
|
+
date: this.state.get('currentDate'),
|
|
128
|
+
view: this.state.get('view')
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Navigate to a specific date
|
|
134
|
+
* @param {Date} date - The date to navigate to
|
|
135
|
+
*/
|
|
136
|
+
goToDate(date) {
|
|
137
|
+
this.state.setCurrentDate(date);
|
|
138
|
+
this._emit('navigate', {
|
|
139
|
+
direction: 'goto',
|
|
140
|
+
date: date,
|
|
141
|
+
view: this.state.get('view')
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the current date
|
|
147
|
+
* @returns {Date}
|
|
148
|
+
*/
|
|
149
|
+
getCurrentDate() {
|
|
150
|
+
return new Date(this.state.get('currentDate'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Add an event
|
|
155
|
+
* @param {import('../events/Event.js').Event|import('../../types.js').EventData} eventData - Event data or Event instance
|
|
156
|
+
* @returns {import('../events/Event.js').Event} The added event
|
|
157
|
+
*/
|
|
158
|
+
addEvent(eventData) {
|
|
159
|
+
// If eventData is not an Event instance and doesn't have a timezone, use calendar's timezone
|
|
160
|
+
if (!(eventData instanceof Event) && !eventData.timeZone) {
|
|
161
|
+
eventData = { ...eventData, timeZone: this.config.timeZone };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const event = this.eventStore.addEvent(eventData);
|
|
165
|
+
|
|
166
|
+
this._emit('eventAdd', { event });
|
|
167
|
+
|
|
168
|
+
return event;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update an event
|
|
173
|
+
* @param {string} eventId - The event ID
|
|
174
|
+
* @param {Object} updates - Properties to update
|
|
175
|
+
* @returns {Event} The updated event
|
|
176
|
+
*/
|
|
177
|
+
updateEvent(eventId, updates) {
|
|
178
|
+
const oldEvent = this.eventStore.getEvent(eventId);
|
|
179
|
+
const event = this.eventStore.updateEvent(eventId, updates);
|
|
180
|
+
|
|
181
|
+
this._emit('eventUpdate', { event, oldEvent });
|
|
182
|
+
|
|
183
|
+
return event;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Remove an event
|
|
188
|
+
* @param {string} eventId - The event ID
|
|
189
|
+
* @returns {boolean} True if removed
|
|
190
|
+
*/
|
|
191
|
+
removeEvent(eventId) {
|
|
192
|
+
const event = this.eventStore.getEvent(eventId);
|
|
193
|
+
const removed = this.eventStore.removeEvent(eventId);
|
|
194
|
+
|
|
195
|
+
if (removed) {
|
|
196
|
+
this._emit('eventRemove', { event });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return removed;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get an event by ID
|
|
204
|
+
* @param {string} eventId - The event ID
|
|
205
|
+
* @returns {Event|null}
|
|
206
|
+
*/
|
|
207
|
+
getEvent(eventId) {
|
|
208
|
+
return this.eventStore.getEvent(eventId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get all events
|
|
213
|
+
* @returns {Event[]}
|
|
214
|
+
*/
|
|
215
|
+
getEvents() {
|
|
216
|
+
return this.eventStore.getAllEvents();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Set all events (replaces existing)
|
|
221
|
+
* @param {Event[]} events - Array of events
|
|
222
|
+
*/
|
|
223
|
+
setEvents(events) {
|
|
224
|
+
this.eventStore.loadEvents(events);
|
|
225
|
+
this._emit('eventsSet', { events: this.getEvents() });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Query events with filters
|
|
230
|
+
* @param {Object} filters - Query filters
|
|
231
|
+
* @returns {Event[]}
|
|
232
|
+
*/
|
|
233
|
+
queryEvents(filters) {
|
|
234
|
+
return this.eventStore.queryEvents(filters);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get events for a specific date
|
|
239
|
+
* @param {Date} date - The date
|
|
240
|
+
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
241
|
+
* @returns {Event[]}
|
|
242
|
+
*/
|
|
243
|
+
getEventsForDate(date, timezone = null) {
|
|
244
|
+
return this.eventStore.getEventsForDate(date, timezone || this.config.timeZone);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get events in a date range
|
|
249
|
+
* @param {Date} start - Start date
|
|
250
|
+
* @param {Date} end - End date
|
|
251
|
+
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
252
|
+
* @returns {Event[]}
|
|
253
|
+
*/
|
|
254
|
+
getEventsInRange(start, end, timezone = null) {
|
|
255
|
+
return this.eventStore.getEventsInRange(start, end, true, timezone || this.config.timeZone);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set the calendar's timezone
|
|
260
|
+
* @param {string} timezone - IANA timezone identifier
|
|
261
|
+
*/
|
|
262
|
+
setTimezone(timezone) {
|
|
263
|
+
const parsedTimezone = this.timezoneManager.parseTimezone(timezone);
|
|
264
|
+
const previousTimezone = this.config.timeZone;
|
|
265
|
+
|
|
266
|
+
this.config.timeZone = parsedTimezone;
|
|
267
|
+
this.eventStore.defaultTimezone = parsedTimezone;
|
|
268
|
+
this.state.setState({ timeZone: parsedTimezone });
|
|
269
|
+
|
|
270
|
+
this._emit('timezoneChange', {
|
|
271
|
+
timezone: parsedTimezone,
|
|
272
|
+
previousTimezone: previousTimezone
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get the current timezone
|
|
278
|
+
* @returns {string} Current timezone
|
|
279
|
+
*/
|
|
280
|
+
getTimezone() {
|
|
281
|
+
return this.config.timeZone;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convert a date from one timezone to another
|
|
286
|
+
* @param {Date} date - Date to convert
|
|
287
|
+
* @param {string} fromTimezone - Source timezone
|
|
288
|
+
* @param {string} toTimezone - Target timezone
|
|
289
|
+
* @returns {Date} Converted date
|
|
290
|
+
*/
|
|
291
|
+
convertTimezone(date, fromTimezone, toTimezone) {
|
|
292
|
+
return this.timezoneManager.convertTimezone(date, fromTimezone, toTimezone);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Convert a date to the calendar's timezone
|
|
297
|
+
* @param {Date} date - Date to convert
|
|
298
|
+
* @param {string} fromTimezone - Source timezone
|
|
299
|
+
* @returns {Date} Date in calendar timezone
|
|
300
|
+
*/
|
|
301
|
+
toCalendarTimezone(date, fromTimezone) {
|
|
302
|
+
return this.timezoneManager.convertTimezone(date, fromTimezone, this.config.timeZone);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Convert a date from the calendar's timezone
|
|
307
|
+
* @param {Date} date - Date in calendar timezone
|
|
308
|
+
* @param {string} toTimezone - Target timezone
|
|
309
|
+
* @returns {Date} Converted date
|
|
310
|
+
*/
|
|
311
|
+
fromCalendarTimezone(date, toTimezone) {
|
|
312
|
+
return this.timezoneManager.convertTimezone(date, this.config.timeZone, toTimezone);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Format a date in a specific timezone
|
|
317
|
+
* @param {Date} date - Date to format
|
|
318
|
+
* @param {string} [timezone] - Timezone for formatting (defaults to calendar timezone)
|
|
319
|
+
* @param {Object} [options] - Formatting options
|
|
320
|
+
* @returns {string} Formatted date string
|
|
321
|
+
*/
|
|
322
|
+
formatInTimezone(date, timezone = null, options = {}) {
|
|
323
|
+
return this.timezoneManager.formatInTimezone(
|
|
324
|
+
date,
|
|
325
|
+
timezone || this.config.timeZone,
|
|
326
|
+
options
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get list of common timezones with offsets
|
|
332
|
+
* @returns {Array<{value: string, label: string, offset: string}>} Timezone list
|
|
333
|
+
*/
|
|
334
|
+
getTimezones() {
|
|
335
|
+
return this.timezoneManager.getCommonTimezones();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get overlapping event groups for a date
|
|
340
|
+
* @param {Date} date - The date to check
|
|
341
|
+
* @param {boolean} timedOnly - Only include timed events
|
|
342
|
+
* @returns {Array<Event[]>} Array of event groups that overlap
|
|
343
|
+
*/
|
|
344
|
+
getOverlapGroups(date, timedOnly = true) {
|
|
345
|
+
return this.eventStore.getOverlapGroups(date, timedOnly);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Calculate event positions for rendering
|
|
350
|
+
* @param {Event[]} events - Array of overlapping events
|
|
351
|
+
* @returns {Map<string, {column: number, totalColumns: number}>} Position data
|
|
352
|
+
*/
|
|
353
|
+
calculateEventPositions(events) {
|
|
354
|
+
return this.eventStore.calculateEventPositions(events);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get the current view's data
|
|
359
|
+
* @returns {import('../../types.js').MonthViewData|import('../../types.js').WeekViewData|import('../../types.js').DayViewData|import('../../types.js').ListViewData|null} View-specific data
|
|
360
|
+
*/
|
|
361
|
+
getViewData() {
|
|
362
|
+
const view = this.state.get('view');
|
|
363
|
+
const currentDate = this.state.get('currentDate');
|
|
364
|
+
|
|
365
|
+
switch (view) {
|
|
366
|
+
case 'month':
|
|
367
|
+
return this._getMonthViewData(currentDate);
|
|
368
|
+
case 'week':
|
|
369
|
+
return this._getWeekViewData(currentDate);
|
|
370
|
+
case 'day':
|
|
371
|
+
return this._getDayViewData(currentDate);
|
|
372
|
+
case 'list':
|
|
373
|
+
return this._getListViewData(currentDate);
|
|
374
|
+
default:
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get month view data
|
|
381
|
+
* @private
|
|
382
|
+
*/
|
|
383
|
+
_getMonthViewData(date) {
|
|
384
|
+
const year = date.getFullYear();
|
|
385
|
+
const month = date.getMonth();
|
|
386
|
+
const weekStartsOn = this.state.get('weekStartsOn');
|
|
387
|
+
const fixedWeekCount = this.state.get('fixedWeekCount');
|
|
388
|
+
|
|
389
|
+
// Get the first day of the month
|
|
390
|
+
const firstDay = new Date(year, month, 1);
|
|
391
|
+
|
|
392
|
+
// Get the last day of the month
|
|
393
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
394
|
+
|
|
395
|
+
// Calculate the start date (beginning of the week containing the first day)
|
|
396
|
+
const startDate = DateUtils.startOfWeek(firstDay, weekStartsOn);
|
|
397
|
+
|
|
398
|
+
// Calculate weeks
|
|
399
|
+
const weeks = [];
|
|
400
|
+
let currentDate = new Date(startDate);
|
|
401
|
+
|
|
402
|
+
// Generate weeks
|
|
403
|
+
const maxWeeks = fixedWeekCount ? 6 : Math.ceil((lastDay.getDate() + DateUtils.getDayOfWeek(firstDay, weekStartsOn)) / 7);
|
|
404
|
+
|
|
405
|
+
for (let weekIndex = 0; weekIndex < maxWeeks; weekIndex++) {
|
|
406
|
+
const week = {
|
|
407
|
+
weekNumber: DateUtils.getWeekNumber(currentDate),
|
|
408
|
+
days: []
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
412
|
+
const dayDate = new Date(currentDate);
|
|
413
|
+
const isCurrentMonth = dayDate.getMonth() === month;
|
|
414
|
+
const isToday = DateUtils.isToday(dayDate);
|
|
415
|
+
const isWeekend = dayDate.getDay() === 0 || dayDate.getDay() === 6;
|
|
416
|
+
|
|
417
|
+
week.days.push({
|
|
418
|
+
date: dayDate,
|
|
419
|
+
dayOfMonth: dayDate.getDate(),
|
|
420
|
+
isCurrentMonth,
|
|
421
|
+
isToday,
|
|
422
|
+
isWeekend,
|
|
423
|
+
events: this.getEventsForDate(dayDate)
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Use DateUtils.addDays to handle month boundaries correctly
|
|
427
|
+
currentDate = DateUtils.addDays(currentDate, 1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
weeks.push(week);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
type: 'month',
|
|
435
|
+
year,
|
|
436
|
+
month,
|
|
437
|
+
monthName: DateUtils.getMonthName(date, this.state.get('locale')),
|
|
438
|
+
weeks,
|
|
439
|
+
startDate,
|
|
440
|
+
endDate: new Date(currentDate.getTime() - 1) // Last moment of the view
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get week view data
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_getWeekViewData(date) {
|
|
449
|
+
const weekStartsOn = this.state.get('weekStartsOn');
|
|
450
|
+
const startDate = DateUtils.startOfWeek(date, weekStartsOn);
|
|
451
|
+
const endDate = DateUtils.endOfWeek(date, weekStartsOn);
|
|
452
|
+
|
|
453
|
+
const days = [];
|
|
454
|
+
const currentDate = new Date(startDate);
|
|
455
|
+
|
|
456
|
+
for (let i = 0; i < 7; i++) {
|
|
457
|
+
const dayDate = new Date(currentDate);
|
|
458
|
+
days.push({
|
|
459
|
+
date: dayDate,
|
|
460
|
+
dayOfMonth: dayDate.getDate(),
|
|
461
|
+
dayOfWeek: dayDate.getDay(),
|
|
462
|
+
dayName: DateUtils.getDayName(dayDate, this.state.get('locale')),
|
|
463
|
+
isToday: DateUtils.isToday(dayDate),
|
|
464
|
+
isWeekend: dayDate.getDay() === 0 || dayDate.getDay() === 6,
|
|
465
|
+
events: this.getEventsForDate(dayDate),
|
|
466
|
+
// Add overlap groups for positioning overlapping events
|
|
467
|
+
overlapGroups: this.eventStore.getOverlapGroups(dayDate, true),
|
|
468
|
+
getEventPositions: (events) => this.eventStore.calculateEventPositions(events)
|
|
469
|
+
});
|
|
470
|
+
// Move to next day
|
|
471
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
type: 'week',
|
|
476
|
+
weekNumber: DateUtils.getWeekNumber(startDate),
|
|
477
|
+
startDate,
|
|
478
|
+
endDate,
|
|
479
|
+
days
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get day view data
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
_getDayViewData(date) {
|
|
488
|
+
const events = this.getEventsForDate(date);
|
|
489
|
+
|
|
490
|
+
// Separate all-day and timed events
|
|
491
|
+
const allDayEvents = events.filter(e => e.allDay);
|
|
492
|
+
const timedEvents = events.filter(e => !e.allDay);
|
|
493
|
+
|
|
494
|
+
// Create hourly slots for timed events
|
|
495
|
+
const hours = [];
|
|
496
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
497
|
+
const hourDate = new Date(date);
|
|
498
|
+
hourDate.setHours(hour, 0, 0, 0);
|
|
499
|
+
const hourEnd = new Date(date);
|
|
500
|
+
hourEnd.setHours(hour + 1, 0, 0, 0);
|
|
501
|
+
|
|
502
|
+
hours.push({
|
|
503
|
+
hour,
|
|
504
|
+
time: DateUtils.formatTime(hourDate, this.state.get('locale')),
|
|
505
|
+
events: timedEvents.filter(event => {
|
|
506
|
+
// Check if event occurs during this hour (not just starts)
|
|
507
|
+
// Event occurs in this hour if it overlaps with the hour slot
|
|
508
|
+
return event.start < hourEnd && event.end > hourDate;
|
|
509
|
+
})
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
type: 'day',
|
|
515
|
+
date,
|
|
516
|
+
dayName: DateUtils.getDayName(date, this.state.get('locale')),
|
|
517
|
+
isToday: DateUtils.isToday(date),
|
|
518
|
+
allDayEvents,
|
|
519
|
+
hours
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get list view data
|
|
525
|
+
* @private
|
|
526
|
+
*/
|
|
527
|
+
_getListViewData(date) {
|
|
528
|
+
// Get events for the next 30 days
|
|
529
|
+
const startDate = new Date(date);
|
|
530
|
+
startDate.setHours(0, 0, 0, 0);
|
|
531
|
+
|
|
532
|
+
const endDate = new Date(startDate);
|
|
533
|
+
endDate.setDate(endDate.getDate() + 30);
|
|
534
|
+
|
|
535
|
+
const events = this.getEventsInRange(startDate, endDate);
|
|
536
|
+
|
|
537
|
+
// Group events by day
|
|
538
|
+
const groupedEvents = new Map();
|
|
539
|
+
|
|
540
|
+
events.forEach(event => {
|
|
541
|
+
const dateKey = event.start.toDateString();
|
|
542
|
+
if (!groupedEvents.has(dateKey)) {
|
|
543
|
+
groupedEvents.set(dateKey, {
|
|
544
|
+
date: new Date(event.start),
|
|
545
|
+
events: []
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
groupedEvents.get(dateKey).events.push(event);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Convert to sorted array
|
|
552
|
+
const days = Array.from(groupedEvents.values())
|
|
553
|
+
.sort((a, b) => a.date - b.date)
|
|
554
|
+
.map(day => ({
|
|
555
|
+
...day,
|
|
556
|
+
dayName: DateUtils.getDayName(day.date, this.state.get('locale')),
|
|
557
|
+
isToday: DateUtils.isToday(day.date)
|
|
558
|
+
}));
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
type: 'list',
|
|
562
|
+
startDate,
|
|
563
|
+
endDate,
|
|
564
|
+
days,
|
|
565
|
+
totalEvents: events.length
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Select an event
|
|
571
|
+
* @param {string} eventId - Event ID to select
|
|
572
|
+
*/
|
|
573
|
+
selectEvent(eventId) {
|
|
574
|
+
const event = this.getEvent(eventId);
|
|
575
|
+
if (event) {
|
|
576
|
+
this.state.selectEvent(eventId);
|
|
577
|
+
this._emit('eventSelect', { event });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Clear event selection
|
|
583
|
+
*/
|
|
584
|
+
clearEventSelection() {
|
|
585
|
+
const eventId = this.state.get('selectedEventId');
|
|
586
|
+
this.state.clearEventSelection();
|
|
587
|
+
|
|
588
|
+
if (eventId) {
|
|
589
|
+
this._emit('eventDeselect', { eventId });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Select a date
|
|
595
|
+
* @param {Date} date - Date to select
|
|
596
|
+
*/
|
|
597
|
+
selectDate(date) {
|
|
598
|
+
this.state.selectDate(date);
|
|
599
|
+
this._emit('dateSelect', { date });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Clear date selection
|
|
604
|
+
*/
|
|
605
|
+
clearDateSelection() {
|
|
606
|
+
const date = this.state.get('selectedDate');
|
|
607
|
+
this.state.clearDateSelection();
|
|
608
|
+
|
|
609
|
+
if (date) {
|
|
610
|
+
this._emit('dateDeselect', { date });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Subscribe to calendar events
|
|
616
|
+
* @param {string} eventName - Event name
|
|
617
|
+
* @param {Function} callback - Callback function
|
|
618
|
+
* @returns {Function} Unsubscribe function
|
|
619
|
+
*/
|
|
620
|
+
on(eventName, callback) {
|
|
621
|
+
if (!this.listeners.has(eventName)) {
|
|
622
|
+
this.listeners.set(eventName, new Set());
|
|
623
|
+
}
|
|
624
|
+
this.listeners.get(eventName).add(callback);
|
|
625
|
+
|
|
626
|
+
return () => this.off(eventName, callback);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Unsubscribe from calendar events
|
|
631
|
+
* @param {string} eventName - Event name
|
|
632
|
+
* @param {Function} callback - Callback function
|
|
633
|
+
*/
|
|
634
|
+
off(eventName, callback) {
|
|
635
|
+
const callbacks = this.listeners.get(eventName);
|
|
636
|
+
if (callbacks) {
|
|
637
|
+
callbacks.delete(callback);
|
|
638
|
+
if (callbacks.size === 0) {
|
|
639
|
+
this.listeners.delete(eventName);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Emit an event
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
648
|
+
_emit(eventName, data) {
|
|
649
|
+
const callbacks = this.listeners.get(eventName);
|
|
650
|
+
if (callbacks) {
|
|
651
|
+
callbacks.forEach(callback => {
|
|
652
|
+
try {
|
|
653
|
+
callback(data);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error(`Error in event listener for "${eventName}":`, error);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Set up internal listeners
|
|
663
|
+
* @private
|
|
664
|
+
*/
|
|
665
|
+
_setupInternalListeners() {
|
|
666
|
+
// Listen to state changes
|
|
667
|
+
this.state.subscribe((newState, oldState) => {
|
|
668
|
+
this._emit('stateChange', { newState, oldState });
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Listen to event store changes
|
|
672
|
+
this.eventStore.subscribe((change) => {
|
|
673
|
+
this._emit('eventStoreChange', change);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Install a plugin
|
|
679
|
+
* @param {Object} plugin - Plugin object with install method
|
|
680
|
+
*/
|
|
681
|
+
use(plugin) {
|
|
682
|
+
if (this.plugins.has(plugin)) {
|
|
683
|
+
console.warn('Plugin already installed');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (typeof plugin.install === 'function') {
|
|
688
|
+
plugin.install(this);
|
|
689
|
+
this.plugins.add(plugin);
|
|
690
|
+
} else {
|
|
691
|
+
throw new Error('Plugin must have an install method');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Destroy the calendar and clean up
|
|
697
|
+
*/
|
|
698
|
+
destroy() {
|
|
699
|
+
// Clear all listeners
|
|
700
|
+
this.listeners.clear();
|
|
701
|
+
|
|
702
|
+
// Clear stores
|
|
703
|
+
this.eventStore.clear();
|
|
704
|
+
|
|
705
|
+
// Clear plugins
|
|
706
|
+
this.plugins.forEach(plugin => {
|
|
707
|
+
if (typeof plugin.uninstall === 'function') {
|
|
708
|
+
plugin.uninstall(this);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
this.plugins.clear();
|
|
712
|
+
|
|
713
|
+
this._emit('destroy');
|
|
714
|
+
}
|
|
715
|
+
}// Test workflow
|