@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,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateManager - Central state management for the calendar
|
|
3
|
+
* Implements an immutable state pattern with change notifications
|
|
4
|
+
*/
|
|
5
|
+
export class StateManager {
|
|
6
|
+
/**
|
|
7
|
+
* Create a new StateManager instance
|
|
8
|
+
* @param {Partial<import('../../types.js').CalendarState>} [initialState={}] - Initial state values
|
|
9
|
+
*/
|
|
10
|
+
constructor(initialState = {}) {
|
|
11
|
+
this.state = {
|
|
12
|
+
// Current view configuration
|
|
13
|
+
view: 'month', // 'month', 'week', 'day', 'list'
|
|
14
|
+
currentDate: new Date(),
|
|
15
|
+
|
|
16
|
+
// UI state
|
|
17
|
+
selectedEventId: null,
|
|
18
|
+
selectedDate: null,
|
|
19
|
+
hoveredEventId: null,
|
|
20
|
+
hoveredDate: null,
|
|
21
|
+
|
|
22
|
+
// Display options
|
|
23
|
+
weekStartsOn: 0, // 0 = Sunday, 1 = Monday, etc.
|
|
24
|
+
showWeekNumbers: false,
|
|
25
|
+
showWeekends: true,
|
|
26
|
+
fixedWeekCount: true, // Always show 6 weeks in month view
|
|
27
|
+
|
|
28
|
+
// Time configuration
|
|
29
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
30
|
+
locale: 'en-US',
|
|
31
|
+
hourFormat: '12h', // '12h' or '24h'
|
|
32
|
+
|
|
33
|
+
// Business hours (for week/day views)
|
|
34
|
+
businessHours: {
|
|
35
|
+
start: '09:00',
|
|
36
|
+
end: '17:00'
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Filters
|
|
40
|
+
filters: {
|
|
41
|
+
searchTerm: '',
|
|
42
|
+
categories: [],
|
|
43
|
+
showAllDay: true,
|
|
44
|
+
showTimed: true
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Interaction flags
|
|
48
|
+
isDragging: false,
|
|
49
|
+
isResizing: false,
|
|
50
|
+
isCreating: false,
|
|
51
|
+
|
|
52
|
+
// Loading states
|
|
53
|
+
isLoading: false,
|
|
54
|
+
loadingMessage: '',
|
|
55
|
+
|
|
56
|
+
// Error state
|
|
57
|
+
error: null,
|
|
58
|
+
|
|
59
|
+
// Custom metadata
|
|
60
|
+
metadata: {},
|
|
61
|
+
|
|
62
|
+
// Apply initial state overrides
|
|
63
|
+
...initialState
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Observers for state changes
|
|
67
|
+
this.listeners = new Map();
|
|
68
|
+
this.globalListeners = new Set();
|
|
69
|
+
|
|
70
|
+
// History for undo/redo (optional)
|
|
71
|
+
this.history = [];
|
|
72
|
+
this.historyIndex = -1;
|
|
73
|
+
this.maxHistorySize = 50;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current state
|
|
78
|
+
* @returns {import('../../types.js').CalendarState} Current state (frozen)
|
|
79
|
+
*/
|
|
80
|
+
getState() {
|
|
81
|
+
return Object.freeze({ ...this.state });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get a specific state value
|
|
86
|
+
* @param {keyof import('../../types.js').CalendarState} key - The state key
|
|
87
|
+
* @returns {any} The state value
|
|
88
|
+
*/
|
|
89
|
+
get(key) {
|
|
90
|
+
return this.state[key];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update state with partial updates
|
|
95
|
+
* @param {Object|Function} updates - Object with updates or updater function
|
|
96
|
+
*/
|
|
97
|
+
setState(updates) {
|
|
98
|
+
const oldState = this.state;
|
|
99
|
+
|
|
100
|
+
// Support function updater pattern
|
|
101
|
+
if (typeof updates === 'function') {
|
|
102
|
+
updates = updates(oldState);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create new state with updates
|
|
106
|
+
const newState = {
|
|
107
|
+
...oldState,
|
|
108
|
+
...updates,
|
|
109
|
+
// Preserve nested objects
|
|
110
|
+
filters: updates.filters ? { ...oldState.filters, ...updates.filters } : oldState.filters,
|
|
111
|
+
businessHours: updates.businessHours ? { ...oldState.businessHours, ...updates.businessHours } : oldState.businessHours,
|
|
112
|
+
metadata: updates.metadata ? { ...oldState.metadata, ...updates.metadata } : oldState.metadata
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Check if state actually changed
|
|
116
|
+
if (this._hasChanged(oldState, newState)) {
|
|
117
|
+
this.state = newState;
|
|
118
|
+
|
|
119
|
+
// Add to history (store the new state)
|
|
120
|
+
this._addToHistory(newState);
|
|
121
|
+
|
|
122
|
+
// Notify listeners
|
|
123
|
+
this._notifyListeners(oldState, newState);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Set the current view
|
|
129
|
+
* @param {string} view - The view type
|
|
130
|
+
*/
|
|
131
|
+
setView(view) {
|
|
132
|
+
const validViews = ['month', 'week', 'day', 'list'];
|
|
133
|
+
if (!validViews.includes(view)) {
|
|
134
|
+
throw new Error(`Invalid view: ${view}. Must be one of: ${validViews.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
this.setState({ view });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set the current date
|
|
141
|
+
* @param {Date} date - The date to set
|
|
142
|
+
*/
|
|
143
|
+
setCurrentDate(date) {
|
|
144
|
+
if (!(date instanceof Date)) {
|
|
145
|
+
date = new Date(date);
|
|
146
|
+
}
|
|
147
|
+
if (isNaN(date.getTime())) {
|
|
148
|
+
throw new Error('Invalid date');
|
|
149
|
+
}
|
|
150
|
+
this.setState({ currentDate: date });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Navigate to the next period (month/week/day based on view)
|
|
155
|
+
*/
|
|
156
|
+
navigateNext() {
|
|
157
|
+
const { view, currentDate } = this.state;
|
|
158
|
+
const newDate = new Date(currentDate);
|
|
159
|
+
|
|
160
|
+
switch (view) {
|
|
161
|
+
case 'month':
|
|
162
|
+
newDate.setMonth(newDate.getMonth() + 1);
|
|
163
|
+
break;
|
|
164
|
+
case 'week':
|
|
165
|
+
newDate.setDate(newDate.getDate() + 7);
|
|
166
|
+
break;
|
|
167
|
+
case 'day':
|
|
168
|
+
newDate.setDate(newDate.getDate() + 1);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.setCurrentDate(newDate);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Navigate to the previous period
|
|
177
|
+
*/
|
|
178
|
+
navigatePrevious() {
|
|
179
|
+
const { view, currentDate } = this.state;
|
|
180
|
+
const newDate = new Date(currentDate);
|
|
181
|
+
|
|
182
|
+
switch (view) {
|
|
183
|
+
case 'month':
|
|
184
|
+
newDate.setMonth(newDate.getMonth() - 1);
|
|
185
|
+
break;
|
|
186
|
+
case 'week':
|
|
187
|
+
newDate.setDate(newDate.getDate() - 7);
|
|
188
|
+
break;
|
|
189
|
+
case 'day':
|
|
190
|
+
newDate.setDate(newDate.getDate() - 1);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.setCurrentDate(newDate);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Navigate to today
|
|
199
|
+
*/
|
|
200
|
+
navigateToday() {
|
|
201
|
+
this.setCurrentDate(new Date());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Select an event
|
|
206
|
+
* @param {string} eventId - The event ID to select
|
|
207
|
+
*/
|
|
208
|
+
selectEvent(eventId) {
|
|
209
|
+
this.setState({ selectedEventId: eventId });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clear event selection
|
|
214
|
+
*/
|
|
215
|
+
clearEventSelection() {
|
|
216
|
+
this.setState({ selectedEventId: null });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Select a date
|
|
221
|
+
* @param {Date} date - The date to select
|
|
222
|
+
*/
|
|
223
|
+
selectDate(date) {
|
|
224
|
+
if (!(date instanceof Date)) {
|
|
225
|
+
date = new Date(date);
|
|
226
|
+
}
|
|
227
|
+
this.setState({ selectedDate: date });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Clear date selection
|
|
232
|
+
*/
|
|
233
|
+
clearDateSelection() {
|
|
234
|
+
this.setState({ selectedDate: null });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Set loading state
|
|
239
|
+
* @param {boolean} isLoading - Loading state
|
|
240
|
+
* @param {string} message - Optional loading message
|
|
241
|
+
*/
|
|
242
|
+
setLoading(isLoading, message = '') {
|
|
243
|
+
this.setState({
|
|
244
|
+
isLoading,
|
|
245
|
+
loadingMessage: message
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set error state
|
|
251
|
+
* @param {Error|string|null} error - The error
|
|
252
|
+
*/
|
|
253
|
+
setError(error) {
|
|
254
|
+
this.setState({
|
|
255
|
+
error: error ? (error instanceof Error ? error.message : error) : null
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Update filters
|
|
261
|
+
* @param {Object} filters - Filter updates
|
|
262
|
+
*/
|
|
263
|
+
updateFilters(filters) {
|
|
264
|
+
this.setState({
|
|
265
|
+
filters: {
|
|
266
|
+
...this.state.filters,
|
|
267
|
+
...filters
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Subscribe to all state changes
|
|
274
|
+
* @param {Function} callback - Callback function
|
|
275
|
+
* @returns {Function} Unsubscribe function
|
|
276
|
+
*/
|
|
277
|
+
subscribe(callback) {
|
|
278
|
+
this.globalListeners.add(callback);
|
|
279
|
+
|
|
280
|
+
return () => {
|
|
281
|
+
this.globalListeners.delete(callback);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Subscribe to specific state key changes
|
|
287
|
+
* @param {string|string[]} keys - State key(s) to watch
|
|
288
|
+
* @param {Function} callback - Callback function
|
|
289
|
+
* @returns {Function} Unsubscribe function
|
|
290
|
+
*/
|
|
291
|
+
watch(keys, callback) {
|
|
292
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
293
|
+
|
|
294
|
+
keyArray.forEach(key => {
|
|
295
|
+
if (!this.listeners.has(key)) {
|
|
296
|
+
this.listeners.set(key, new Set());
|
|
297
|
+
}
|
|
298
|
+
this.listeners.get(key).add(callback);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
keyArray.forEach(key => {
|
|
303
|
+
const callbacks = this.listeners.get(key);
|
|
304
|
+
if (callbacks) {
|
|
305
|
+
callbacks.delete(callback);
|
|
306
|
+
if (callbacks.size === 0) {
|
|
307
|
+
this.listeners.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if undo is available
|
|
316
|
+
* @returns {boolean} True if undo is available
|
|
317
|
+
*/
|
|
318
|
+
canUndo() {
|
|
319
|
+
return this.historyIndex > 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if redo is available
|
|
324
|
+
* @returns {boolean} True if redo is available
|
|
325
|
+
*/
|
|
326
|
+
canRedo() {
|
|
327
|
+
return this.historyIndex < this.history.length - 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get the number of undo operations available
|
|
332
|
+
* @returns {number} Number of undo operations
|
|
333
|
+
*/
|
|
334
|
+
getUndoCount() {
|
|
335
|
+
return this.historyIndex;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the number of redo operations available
|
|
340
|
+
* @returns {number} Number of redo operations
|
|
341
|
+
*/
|
|
342
|
+
getRedoCount() {
|
|
343
|
+
return this.history.length - 1 - this.historyIndex;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Undo the last state change
|
|
348
|
+
* @returns {boolean} True if undo was performed
|
|
349
|
+
*/
|
|
350
|
+
undo() {
|
|
351
|
+
if (!this.canUndo()) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.historyIndex--;
|
|
356
|
+
const previousState = this.history[this.historyIndex];
|
|
357
|
+
const currentState = this.state;
|
|
358
|
+
|
|
359
|
+
// Update state without adding to history
|
|
360
|
+
this.state = { ...previousState };
|
|
361
|
+
|
|
362
|
+
// Notify listeners
|
|
363
|
+
this._notifyListeners(currentState, this.state);
|
|
364
|
+
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Redo the next state change
|
|
370
|
+
* @returns {boolean} True if redo was performed
|
|
371
|
+
*/
|
|
372
|
+
redo() {
|
|
373
|
+
if (!this.canRedo()) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.historyIndex++;
|
|
378
|
+
const nextState = this.history[this.historyIndex];
|
|
379
|
+
const currentState = this.state;
|
|
380
|
+
|
|
381
|
+
// Update state without adding to history
|
|
382
|
+
this.state = { ...nextState };
|
|
383
|
+
|
|
384
|
+
// Notify listeners
|
|
385
|
+
this._notifyListeners(currentState, this.state);
|
|
386
|
+
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Reset state to initial values
|
|
392
|
+
*/
|
|
393
|
+
reset() {
|
|
394
|
+
const initialState = this.history[0] || {};
|
|
395
|
+
this.setState(initialState);
|
|
396
|
+
this.history = [initialState];
|
|
397
|
+
this.historyIndex = 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if state has changed
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_hasChanged(oldState, newState) {
|
|
405
|
+
return !this._deepEqual(oldState, newState);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Deep equality check optimized for state comparison
|
|
410
|
+
* @private
|
|
411
|
+
* @param {*} a - First value
|
|
412
|
+
* @param {*} b - Second value
|
|
413
|
+
* @param {Set} seen - Track circular references
|
|
414
|
+
* @returns {boolean} True if values are deeply equal
|
|
415
|
+
*/
|
|
416
|
+
_deepEqual(a, b, seen = new Set()) {
|
|
417
|
+
// Same reference
|
|
418
|
+
if (a === b) return true;
|
|
419
|
+
|
|
420
|
+
// Different types or null/undefined
|
|
421
|
+
if (a == null || b == null) return a === b;
|
|
422
|
+
if (typeof a !== typeof b) return false;
|
|
423
|
+
|
|
424
|
+
// Primitives
|
|
425
|
+
if (typeof a !== 'object') return a === b;
|
|
426
|
+
|
|
427
|
+
// Check for circular references
|
|
428
|
+
if (seen.has(a) || seen.has(b)) return false;
|
|
429
|
+
seen.add(a);
|
|
430
|
+
seen.add(b);
|
|
431
|
+
|
|
432
|
+
// Arrays
|
|
433
|
+
if (Array.isArray(a)) {
|
|
434
|
+
if (!Array.isArray(b) || a.length !== b.length) {
|
|
435
|
+
seen.delete(a);
|
|
436
|
+
seen.delete(b);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (let i = 0; i < a.length; i++) {
|
|
441
|
+
if (!this._deepEqual(a[i], b[i], seen)) {
|
|
442
|
+
seen.delete(a);
|
|
443
|
+
seen.delete(b);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
seen.delete(a);
|
|
449
|
+
seen.delete(b);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Dates
|
|
454
|
+
if (a instanceof Date && b instanceof Date) {
|
|
455
|
+
const result = a.getTime() === b.getTime();
|
|
456
|
+
seen.delete(a);
|
|
457
|
+
seen.delete(b);
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Objects
|
|
462
|
+
const aKeys = Object.keys(a);
|
|
463
|
+
const bKeys = Object.keys(b);
|
|
464
|
+
|
|
465
|
+
if (aKeys.length !== bKeys.length) {
|
|
466
|
+
seen.delete(a);
|
|
467
|
+
seen.delete(b);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Sort keys for consistent comparison
|
|
472
|
+
aKeys.sort();
|
|
473
|
+
bKeys.sort();
|
|
474
|
+
|
|
475
|
+
// Compare keys
|
|
476
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
477
|
+
if (aKeys[i] !== bKeys[i]) {
|
|
478
|
+
seen.delete(a);
|
|
479
|
+
seen.delete(b);
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Compare values
|
|
485
|
+
for (const key of aKeys) {
|
|
486
|
+
if (!this._deepEqual(a[key], b[key], seen)) {
|
|
487
|
+
seen.delete(a);
|
|
488
|
+
seen.delete(b);
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
seen.delete(a);
|
|
494
|
+
seen.delete(b);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Add state to history
|
|
500
|
+
* @private
|
|
501
|
+
*/
|
|
502
|
+
_addToHistory(state) {
|
|
503
|
+
// Remove any future history if we're not at the end
|
|
504
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
505
|
+
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Add new state
|
|
509
|
+
this.history.push({ ...state });
|
|
510
|
+
this.historyIndex++;
|
|
511
|
+
|
|
512
|
+
// Limit history size
|
|
513
|
+
if (this.history.length > this.maxHistorySize) {
|
|
514
|
+
this.history.shift();
|
|
515
|
+
this.historyIndex--;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Notify listeners of state changes
|
|
521
|
+
* @private
|
|
522
|
+
*/
|
|
523
|
+
_notifyListeners(oldState, newState) {
|
|
524
|
+
// Notify global listeners
|
|
525
|
+
for (const callback of this.globalListeners) {
|
|
526
|
+
try {
|
|
527
|
+
callback(newState, oldState);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error('Error in state listener:', error);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Notify specific key listeners
|
|
534
|
+
for (const [key, callbacks] of this.listeners) {
|
|
535
|
+
if (oldState[key] !== newState[key]) {
|
|
536
|
+
for (const callback of callbacks) {
|
|
537
|
+
try {
|
|
538
|
+
callback(newState[key], oldState[key], newState, oldState);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
console.error(`Error in state listener for key "${key}":`, error);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|