UNPKG

91.5 kBJavaScriptView Raw
1import { m as mergeProps, g as guid, i as isArraysEqual, T as Theme, a as mapHash, B as BaseComponent, V as ViewContextType, C as ContentContainer, b as buildViewClassNames, c as greatestDurationDenominator, d as createDuration, e as BASE_OPTION_DEFAULTS, f as arrayToHash, h as filterHash, j as buildEventSourceRefiners, p as parseEventSource, k as formatWithOrdinals, u as unpromisify, l as buildRangeApiWithTimeZone, n as identity, r as requestJson, s as subtractDurations, o as intersectRanges, q as startOfDay, t as addDays, v as hashValuesToArray, w as buildEventApis, D as DelayedRunner, x as createFormatter, y as diffWholeDays, z as memoize, A as memoizeObjArg, E as isPropsEqual, F as Emitter, G as getInitialDate, H as rangeContainsMarker, I as createEmptyEventStore, J as reduceCurrentDate, K as reduceEventStore, L as rezoneEventStoreDates, M as mergeRawOptions, N as BASE_OPTION_REFINERS, O as CALENDAR_LISTENER_REFINERS, P as CALENDAR_OPTION_REFINERS, Q as COMPLEX_OPTION_COMPARATORS, R as VIEW_OPTION_REFINERS, S as DateEnv, U as DateProfileGenerator, W as createEventUi, X as parseBusinessHours, Y as setRef, Z as Interaction, _ as getElSeg, $ as elementClosest, a0 as EventImpl, a1 as listenBySelector, a2 as listenToHoverBySelector, a3 as PureComponent, a4 as buildViewContext, a5 as getUniqueDomId, a6 as parseInteractionSettings, a7 as interactionSettingsStore, a8 as getNow, a9 as CalendarImpl, aa as flushSync, ab as CalendarRoot, ac as RenderId, ad as ensureElHasStyles, ae as applyStyleProp, af as sliceEventStore } from './internal-common.js';
2export { ag as JsonRequestError } from './internal-common.js';
3import { createElement, createRef, Fragment, render } from 'preact';
4import 'preact/compat';
5
6const globalLocales = [];
7
8const MINIMAL_RAW_EN_LOCALE = {
9 code: 'en',
10 week: {
11 dow: 0,
12 doy: 4, // 4 days need to be within the year to be considered the first week
13 },
14 direction: 'ltr',
15 buttonText: {
16 prev: 'prev',
17 next: 'next',
18 prevYear: 'prev year',
19 nextYear: 'next year',
20 year: 'year',
21 today: 'today',
22 month: 'month',
23 week: 'week',
24 day: 'day',
25 list: 'list',
26 },
27 weekText: 'W',
28 weekTextLong: 'Week',
29 closeHint: 'Close',
30 timeHint: 'Time',
31 eventHint: 'Event',
32 allDayText: 'all-day',
33 moreLinkText: 'more',
34 noEventsText: 'No events to display',
35};
36const RAW_EN_LOCALE = Object.assign(Object.assign({}, MINIMAL_RAW_EN_LOCALE), {
37 // Includes things we don't want other locales to inherit,
38 // things that derive from other translatable strings.
39 buttonHints: {
40 prev: 'Previous $0',
41 next: 'Next $0',
42 today(buttonText, unit) {
43 return (unit === 'day')
44 ? 'Today'
45 : `This ${buttonText}`;
46 },
47 }, viewHint: '$0 view', navLinkHint: 'Go to $0', moreLinkHint(eventCnt) {
48 return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`;
49 } });
50function organizeRawLocales(explicitRawLocales) {
51 let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en';
52 let allRawLocales = globalLocales.concat(explicitRawLocales);
53 let rawLocaleMap = {
54 en: RAW_EN_LOCALE,
55 };
56 for (let rawLocale of allRawLocales) {
57 rawLocaleMap[rawLocale.code] = rawLocale;
58 }
59 return {
60 map: rawLocaleMap,
61 defaultCode,
62 };
63}
64function buildLocale(inputSingular, available) {
65 if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
66 return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
67 }
68 return queryLocale(inputSingular, available);
69}
70function queryLocale(codeArg, available) {
71 let codes = [].concat(codeArg || []); // will convert to array
72 let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
73 return parseLocale(codeArg, codes, raw);
74}
75function queryRawLocale(codes, available) {
76 for (let i = 0; i < codes.length; i += 1) {
77 let parts = codes[i].toLocaleLowerCase().split('-');
78 for (let j = parts.length; j > 0; j -= 1) {
79 let simpleId = parts.slice(0, j).join('-');
80 if (available[simpleId]) {
81 return available[simpleId];
82 }
83 }
84 }
85 return null;
86}
87function parseLocale(codeArg, codes, raw) {
88 let merged = mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']);
89 delete merged.code; // don't want this part of the options
90 let { week } = merged;
91 delete merged.week;
92 return {
93 codeArg,
94 codes,
95 week,
96 simpleNumberFormat: new Intl.NumberFormat(codeArg),
97 options: merged,
98 };
99}
100
101// TODO: easier way to add new hooks? need to update a million things
102function createPlugin(input) {
103 return {
104 id: guid(),
105 name: input.name,
106 premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
107 deps: input.deps || [],
108 reducers: input.reducers || [],
109 isLoadingFuncs: input.isLoadingFuncs || [],
110 contextInit: [].concat(input.contextInit || []),
111 eventRefiners: input.eventRefiners || {},
112 eventDefMemberAdders: input.eventDefMemberAdders || [],
113 eventSourceRefiners: input.eventSourceRefiners || {},
114 isDraggableTransformers: input.isDraggableTransformers || [],
115 eventDragMutationMassagers: input.eventDragMutationMassagers || [],
116 eventDefMutationAppliers: input.eventDefMutationAppliers || [],
117 dateSelectionTransformers: input.dateSelectionTransformers || [],
118 datePointTransforms: input.datePointTransforms || [],
119 dateSpanTransforms: input.dateSpanTransforms || [],
120 views: input.views || {},
121 viewPropsTransformers: input.viewPropsTransformers || [],
122 isPropsValid: input.isPropsValid || null,
123 externalDefTransforms: input.externalDefTransforms || [],
124 viewContainerAppends: input.viewContainerAppends || [],
125 eventDropTransformers: input.eventDropTransformers || [],
126 componentInteractions: input.componentInteractions || [],
127 calendarInteractions: input.calendarInteractions || [],
128 themeClasses: input.themeClasses || {},
129 eventSourceDefs: input.eventSourceDefs || [],
130 cmdFormatter: input.cmdFormatter,
131 recurringTypes: input.recurringTypes || [],
132 namedTimeZonedImpl: input.namedTimeZonedImpl,
133 initialView: input.initialView || '',
134 elementDraggingImpl: input.elementDraggingImpl,
135 optionChangeHandlers: input.optionChangeHandlers || {},
136 scrollGridImpl: input.scrollGridImpl || null,
137 listenerRefiners: input.listenerRefiners || {},
138 optionRefiners: input.optionRefiners || {},
139 propSetHandlers: input.propSetHandlers || {},
140 };
141}
142function buildPluginHooks(pluginDefs, globalDefs) {
143 let currentPluginIds = {};
144 let hooks = {
145 premiumReleaseDate: undefined,
146 reducers: [],
147 isLoadingFuncs: [],
148 contextInit: [],
149 eventRefiners: {},
150 eventDefMemberAdders: [],
151 eventSourceRefiners: {},
152 isDraggableTransformers: [],
153 eventDragMutationMassagers: [],
154 eventDefMutationAppliers: [],
155 dateSelectionTransformers: [],
156 datePointTransforms: [],
157 dateSpanTransforms: [],
158 views: {},
159 viewPropsTransformers: [],
160 isPropsValid: null,
161 externalDefTransforms: [],
162 viewContainerAppends: [],
163 eventDropTransformers: [],
164 componentInteractions: [],
165 calendarInteractions: [],
166 themeClasses: {},
167 eventSourceDefs: [],
168 cmdFormatter: null,
169 recurringTypes: [],
170 namedTimeZonedImpl: null,
171 initialView: '',
172 elementDraggingImpl: null,
173 optionChangeHandlers: {},
174 scrollGridImpl: null,
175 listenerRefiners: {},
176 optionRefiners: {},
177 propSetHandlers: {},
178 };
179 function addDefs(defs) {
180 for (let def of defs) {
181 const pluginName = def.name;
182 const currentId = currentPluginIds[pluginName];
183 if (currentId === undefined) {
184 currentPluginIds[pluginName] = def.id;
185 addDefs(def.deps);
186 hooks = combineHooks(hooks, def);
187 }
188 else if (currentId !== def.id) {
189 // different ID than the one already added
190 console.warn(`Duplicate plugin '${pluginName}'`);
191 }
192 }
193 }
194 if (pluginDefs) {
195 addDefs(pluginDefs);
196 }
197 addDefs(globalDefs);
198 return hooks;
199}
200function buildBuildPluginHooks() {
201 let currentOverrideDefs = [];
202 let currentGlobalDefs = [];
203 let currentHooks;
204 return (overrideDefs, globalDefs) => {
205 if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) {
206 currentHooks = buildPluginHooks(overrideDefs, globalDefs);
207 }
208 currentOverrideDefs = overrideDefs;
209 currentGlobalDefs = globalDefs;
210 return currentHooks;
211 };
212}
213function combineHooks(hooks0, hooks1) {
214 return {
215 premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate),
216 reducers: hooks0.reducers.concat(hooks1.reducers),
217 isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs),
218 contextInit: hooks0.contextInit.concat(hooks1.contextInit),
219 eventRefiners: Object.assign(Object.assign({}, hooks0.eventRefiners), hooks1.eventRefiners),
220 eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
221 eventSourceRefiners: Object.assign(Object.assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners),
222 isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
223 eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
224 eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
225 dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
226 datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
227 dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
228 views: Object.assign(Object.assign({}, hooks0.views), hooks1.views),
229 viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
230 isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
231 externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
232 viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
233 eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
234 calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
235 componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
236 themeClasses: Object.assign(Object.assign({}, hooks0.themeClasses), hooks1.themeClasses),
237 eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
238 cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
239 recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
240 namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
241 initialView: hooks0.initialView || hooks1.initialView,
242 elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
243 optionChangeHandlers: Object.assign(Object.assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers),
244 scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
245 listenerRefiners: Object.assign(Object.assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners),
246 optionRefiners: Object.assign(Object.assign({}, hooks0.optionRefiners), hooks1.optionRefiners),
247 propSetHandlers: Object.assign(Object.assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers),
248 };
249}
250function compareOptionalDates(date0, date1) {
251 if (date0 === undefined) {
252 return date1;
253 }
254 if (date1 === undefined) {
255 return date0;
256 }
257 return new Date(Math.max(date0.valueOf(), date1.valueOf()));
258}
259
260class StandardTheme extends Theme {
261}
262StandardTheme.prototype.classes = {
263 root: 'fc-theme-standard',
264 tableCellShaded: 'fc-cell-shaded',
265 buttonGroup: 'fc-button-group',
266 button: 'fc-button fc-button-primary',
267 buttonActive: 'fc-button-active',
268};
269StandardTheme.prototype.baseIconClass = 'fc-icon';
270StandardTheme.prototype.iconClasses = {
271 close: 'fc-icon-x',
272 prev: 'fc-icon-chevron-left',
273 next: 'fc-icon-chevron-right',
274 prevYear: 'fc-icon-chevrons-left',
275 nextYear: 'fc-icon-chevrons-right',
276};
277StandardTheme.prototype.rtlIconClasses = {
278 prev: 'fc-icon-chevron-right',
279 next: 'fc-icon-chevron-left',
280 prevYear: 'fc-icon-chevrons-right',
281 nextYear: 'fc-icon-chevrons-left',
282};
283StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly
284StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
285StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
286
287function compileViewDefs(defaultConfigs, overrideConfigs) {
288 let hash = {};
289 let viewType;
290 for (viewType in defaultConfigs) {
291 ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
292 }
293 for (viewType in overrideConfigs) {
294 ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
295 }
296 return hash;
297}
298function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
299 if (hash[viewType]) {
300 return hash[viewType];
301 }
302 let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
303 if (viewDef) {
304 hash[viewType] = viewDef;
305 }
306 return viewDef;
307}
308function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
309 let defaultConfig = defaultConfigs[viewType];
310 let overrideConfig = overrideConfigs[viewType];
311 let queryProp = (name) => ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
312 ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null));
313 let theComponent = queryProp('component');
314 let superType = queryProp('superType');
315 let superDef = null;
316 if (superType) {
317 if (superType === viewType) {
318 throw new Error('Can\'t have a custom view type that references itself');
319 }
320 superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
321 }
322 if (!theComponent && superDef) {
323 theComponent = superDef.component;
324 }
325 if (!theComponent) {
326 return null; // don't throw a warning, might be settings for a single-unit view
327 }
328 return {
329 type: viewType,
330 component: theComponent,
331 defaults: Object.assign(Object.assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})),
332 overrides: Object.assign(Object.assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})),
333 };
334}
335
336function parseViewConfigs(inputs) {
337 return mapHash(inputs, parseViewConfig);
338}
339function parseViewConfig(input) {
340 let rawOptions = typeof input === 'function' ?
341 { component: input } :
342 input;
343 let { component } = rawOptions;
344 if (rawOptions.content) {
345 // TODO: remove content/classNames/didMount/etc from options?
346 component = createViewHookComponent(rawOptions);
347 }
348 else if (component && !(component.prototype instanceof BaseComponent)) {
349 // WHY?: people were using `component` property for `content`
350 // TODO: converge on one setting name
351 component = createViewHookComponent(Object.assign(Object.assign({}, rawOptions), { content: component }));
352 }
353 return {
354 superType: rawOptions.type,
355 component: component,
356 rawOptions, // includes type and component too :(
357 };
358}
359function createViewHookComponent(options) {
360 return (viewProps) => (createElement(ViewContextType.Consumer, null, (context) => (createElement(ContentContainer, { elTag: "div", elClasses: buildViewClassNames(context.viewSpec), renderProps: Object.assign(Object.assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }), generatorName: undefined, customGenerator: options.content, classNameGenerator: options.classNames, didMount: options.didMount, willUnmount: options.willUnmount }))));
361}
362
363function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
364 let defaultConfigs = parseViewConfigs(defaultInputs);
365 let overrideConfigs = parseViewConfigs(optionOverrides.views);
366 let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
367 return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults));
368}
369function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
370 let durationInput = viewDef.overrides.duration ||
371 viewDef.defaults.duration ||
372 dynamicOptionOverrides.duration ||
373 optionOverrides.duration;
374 let duration = null;
375 let durationUnit = '';
376 let singleUnit = '';
377 let singleUnitOverrides = {};
378 if (durationInput) {
379 duration = createDurationCached(durationInput);
380 if (duration) { // valid?
381 let denom = greatestDurationDenominator(duration);
382 durationUnit = denom.unit;
383 if (denom.value === 1) {
384 singleUnit = durationUnit;
385 singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {};
386 }
387 }
388 }
389 let queryButtonText = (optionsSubset) => {
390 let buttonTextMap = optionsSubset.buttonText || {};
391 let buttonTextKey = viewDef.defaults.buttonTextKey;
392 if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
393 return buttonTextMap[buttonTextKey];
394 }
395 if (buttonTextMap[viewDef.type] != null) {
396 return buttonTextMap[viewDef.type];
397 }
398 if (buttonTextMap[singleUnit] != null) {
399 return buttonTextMap[singleUnit];
400 }
401 return null;
402 };
403 let queryButtonTitle = (optionsSubset) => {
404 let buttonHints = optionsSubset.buttonHints || {};
405 let buttonKey = viewDef.defaults.buttonTextKey; // use same key as text
406 if (buttonKey != null && buttonHints[buttonKey] != null) {
407 return buttonHints[buttonKey];
408 }
409 if (buttonHints[viewDef.type] != null) {
410 return buttonHints[viewDef.type];
411 }
412 if (buttonHints[singleUnit] != null) {
413 return buttonHints[singleUnit];
414 }
415 return null;
416 };
417 return {
418 type: viewDef.type,
419 component: viewDef.component,
420 duration,
421 durationUnit,
422 singleUnit,
423 optionDefaults: viewDef.defaults,
424 optionOverrides: Object.assign(Object.assign({}, singleUnitOverrides), viewDef.overrides),
425 buttonTextOverride: queryButtonText(dynamicOptionOverrides) ||
426 queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence
427 viewDef.overrides.buttonText,
428 buttonTextDefault: queryButtonText(localeDefaults) ||
429 viewDef.defaults.buttonText ||
430 queryButtonText(BASE_OPTION_DEFAULTS) ||
431 viewDef.type,
432 // not DRY
433 buttonTitleOverride: queryButtonTitle(dynamicOptionOverrides) ||
434 queryButtonTitle(optionOverrides) ||
435 viewDef.overrides.buttonHint,
436 buttonTitleDefault: queryButtonTitle(localeDefaults) ||
437 viewDef.defaults.buttonHint ||
438 queryButtonTitle(BASE_OPTION_DEFAULTS),
439 // will eventually fall back to buttonText
440 };
441}
442// hack to get memoization working
443let durationInputMap = {};
444function createDurationCached(durationInput) {
445 let json = JSON.stringify(durationInput);
446 let res = durationInputMap[json];
447 if (res === undefined) {
448 res = createDuration(durationInput);
449 durationInputMap[json] = res;
450 }
451 return res;
452}
453
454function reduceViewType(viewType, action) {
455 switch (action.type) {
456 case 'CHANGE_VIEW_TYPE':
457 viewType = action.viewType;
458 }
459 return viewType;
460}
461
462function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) {
463 switch (action.type) {
464 case 'SET_OPTION':
465 return Object.assign(Object.assign({}, dynamicOptionOverrides), { [action.optionName]: action.rawOptionValue });
466 default:
467 return dynamicOptionOverrides;
468 }
469}
470
471function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) {
472 let dp;
473 switch (action.type) {
474 case 'CHANGE_VIEW_TYPE':
475 return dateProfileGenerator.build(action.dateMarker || currentDate);
476 case 'CHANGE_DATE':
477 return dateProfileGenerator.build(action.dateMarker);
478 case 'PREV':
479 dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate);
480 if (dp.isValid) {
481 return dp;
482 }
483 break;
484 case 'NEXT':
485 dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate);
486 if (dp.isValid) {
487 return dp;
488 }
489 break;
490 }
491 return currentDateProfile;
492}
493
494function initEventSources(calendarOptions, dateProfile, context) {
495 let activeRange = dateProfile ? dateProfile.activeRange : null;
496 return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context);
497}
498function reduceEventSources(eventSources, action, dateProfile, context) {
499 let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
500 switch (action.type) {
501 case 'ADD_EVENT_SOURCES': // already parsed
502 return addSources(eventSources, action.sources, activeRange, context);
503 case 'REMOVE_EVENT_SOURCE':
504 return removeSource(eventSources, action.sourceId);
505 case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
506 case 'NEXT':
507 case 'CHANGE_DATE':
508 case 'CHANGE_VIEW_TYPE':
509 if (dateProfile) {
510 return fetchDirtySources(eventSources, activeRange, context);
511 }
512 return eventSources;
513 case 'FETCH_EVENT_SOURCES':
514 return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type?
515 arrayToHash(action.sourceIds) :
516 excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context);
517 case 'RECEIVE_EVENTS':
518 case 'RECEIVE_EVENT_ERROR':
519 return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
520 case 'REMOVE_ALL_EVENT_SOURCES':
521 return {};
522 default:
523 return eventSources;
524 }
525}
526function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) {
527 let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
528 return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context);
529}
530function computeEventSourcesLoading(eventSources) {
531 for (let sourceId in eventSources) {
532 if (eventSources[sourceId].isFetching) {
533 return true;
534 }
535 }
536 return false;
537}
538function addSources(eventSourceHash, sources, fetchRange, context) {
539 let hash = {};
540 for (let source of sources) {
541 hash[source.sourceId] = source;
542 }
543 if (fetchRange) {
544 hash = fetchDirtySources(hash, fetchRange, context);
545 }
546 return Object.assign(Object.assign({}, eventSourceHash), hash);
547}
548function removeSource(eventSourceHash, sourceId) {
549 return filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId);
550}
551function fetchDirtySources(sourceHash, fetchRange, context) {
552 return fetchSourcesByIds(sourceHash, filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), fetchRange, false, context);
553}
554function isSourceDirty(eventSource, fetchRange, context) {
555 if (!doesSourceNeedRange(eventSource, context)) {
556 return !eventSource.latestFetchId;
557 }
558 return !context.options.lazyFetching ||
559 !eventSource.fetchRange ||
560 eventSource.isFetching || // always cancel outdated in-progress fetches
561 fetchRange.start < eventSource.fetchRange.start ||
562 fetchRange.end > eventSource.fetchRange.end;
563}
564function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) {
565 let nextSources = {};
566 for (let sourceId in prevSources) {
567 let source = prevSources[sourceId];
568 if (sourceIdHash[sourceId]) {
569 nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context);
570 }
571 else {
572 nextSources[sourceId] = source;
573 }
574 }
575 return nextSources;
576}
577function fetchSource(eventSource, fetchRange, isRefetch, context) {
578 let { options, calendarApi } = context;
579 let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId];
580 let fetchId = guid();
581 sourceDef.fetch({
582 eventSource,
583 range: fetchRange,
584 isRefetch,
585 context,
586 }, (res) => {
587 let { rawEvents } = res;
588 if (options.eventSourceSuccess) {
589 rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents;
590 }
591 if (eventSource.success) {
592 rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents;
593 }
594 context.dispatch({
595 type: 'RECEIVE_EVENTS',
596 sourceId: eventSource.sourceId,
597 fetchId,
598 fetchRange,
599 rawEvents,
600 });
601 }, (error) => {
602 let errorHandled = false;
603 if (options.eventSourceFailure) {
604 options.eventSourceFailure.call(calendarApi, error);
605 errorHandled = true;
606 }
607 if (eventSource.failure) {
608 eventSource.failure(error);
609 errorHandled = true;
610 }
611 if (!errorHandled) {
612 console.warn(error.message, error);
613 }
614 context.dispatch({
615 type: 'RECEIVE_EVENT_ERROR',
616 sourceId: eventSource.sourceId,
617 fetchId,
618 fetchRange,
619 error,
620 });
621 });
622 return Object.assign(Object.assign({}, eventSource), { isFetching: true, latestFetchId: fetchId });
623}
624function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
625 let eventSource = sourceHash[sourceId];
626 if (eventSource && // not already removed
627 fetchId === eventSource.latestFetchId) {
628 return Object.assign(Object.assign({}, sourceHash), { [sourceId]: Object.assign(Object.assign({}, eventSource), { isFetching: false, fetchRange }) });
629 }
630 return sourceHash;
631}
632function excludeStaticSources(eventSources, context) {
633 return filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context));
634}
635function parseInitialSources(rawOptions, context) {
636 let refiners = buildEventSourceRefiners(context);
637 let rawSources = [].concat(rawOptions.eventSources || []);
638 let sources = []; // parsed
639 if (rawOptions.initialEvents) {
640 rawSources.unshift(rawOptions.initialEvents);
641 }
642 if (rawOptions.events) {
643 rawSources.unshift(rawOptions.events);
644 }
645 for (let rawSource of rawSources) {
646 let source = parseEventSource(rawSource, context, refiners);
647 if (source) {
648 sources.push(source);
649 }
650 }
651 return sources;
652}
653function doesSourceNeedRange(eventSource, context) {
654 let defs = context.pluginHooks.eventSourceDefs;
655 return !defs[eventSource.sourceDefId].ignoreRange;
656}
657
658function reduceDateSelection(currentSelection, action) {
659 switch (action.type) {
660 case 'UNSELECT_DATES':
661 return null;
662 case 'SELECT_DATES':
663 return action.selection;
664 default:
665 return currentSelection;
666 }
667}
668
669function reduceSelectedEvent(currentInstanceId, action) {
670 switch (action.type) {
671 case 'UNSELECT_EVENT':
672 return '';
673 case 'SELECT_EVENT':
674 return action.eventInstanceId;
675 default:
676 return currentInstanceId;
677 }
678}
679
680function reduceEventDrag(currentDrag, action) {
681 let newDrag;
682 switch (action.type) {
683 case 'UNSET_EVENT_DRAG':
684 return null;
685 case 'SET_EVENT_DRAG':
686 newDrag = action.state;
687 return {
688 affectedEvents: newDrag.affectedEvents,
689 mutatedEvents: newDrag.mutatedEvents,
690 isEvent: newDrag.isEvent,
691 };
692 default:
693 return currentDrag;
694 }
695}
696
697function reduceEventResize(currentResize, action) {
698 let newResize;
699 switch (action.type) {
700 case 'UNSET_EVENT_RESIZE':
701 return null;
702 case 'SET_EVENT_RESIZE':
703 newResize = action.state;
704 return {
705 affectedEvents: newResize.affectedEvents,
706 mutatedEvents: newResize.mutatedEvents,
707 isEvent: newResize.isEvent,
708 };
709 default:
710 return currentResize;
711 }
712}
713
714function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
715 let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
716 let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
717 return { header, footer };
718}
719function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
720 let sectionWidgets = {};
721 let viewsWithButtons = [];
722 let hasTitle = false;
723 for (let sectionName in sectionStrHash) {
724 let sectionStr = sectionStrHash[sectionName];
725 let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi);
726 sectionWidgets[sectionName] = sectionRes.widgets;
727 viewsWithButtons.push(...sectionRes.viewsWithButtons);
728 hasTitle = hasTitle || sectionRes.hasTitle;
729 }
730 return { sectionWidgets, viewsWithButtons, hasTitle };
731}
732/*
733BAD: querying icons and text here. should be done at render time
734*/
735function parseSection(sectionStr, calendarOptions, // defaults+overrides, then refined
736calendarOptionOverrides, // overrides only!, unrefined :(
737theme, viewSpecs, calendarApi) {
738 let isRtl = calendarOptions.direction === 'rtl';
739 let calendarCustomButtons = calendarOptions.customButtons || {};
740 let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {};
741 let calendarButtonText = calendarOptions.buttonText || {};
742 let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {};
743 let calendarButtonHints = calendarOptions.buttonHints || {};
744 let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
745 let viewsWithButtons = [];
746 let hasTitle = false;
747 let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((buttonName) => {
748 if (buttonName === 'title') {
749 hasTitle = true;
750 return { buttonName };
751 }
752 let customButtonProps;
753 let viewSpec;
754 let buttonClick;
755 let buttonIcon; // only one of these will be set
756 let buttonText; // "
757 let buttonHint;
758 // ^ for the title="" attribute, for accessibility
759 if ((customButtonProps = calendarCustomButtons[buttonName])) {
760 buttonClick = (ev) => {
761 if (customButtonProps.click) {
762 customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context?
763 }
764 };
765 (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
766 (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
767 (buttonText = customButtonProps.text);
768 buttonHint = customButtonProps.hint || customButtonProps.text;
769 }
770 else if ((viewSpec = viewSpecs[buttonName])) {
771 viewsWithButtons.push(buttonName);
772 buttonClick = () => {
773 calendarApi.changeView(buttonName);
774 };
775 (buttonText = viewSpec.buttonTextOverride) ||
776 (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
777 (buttonText = viewSpec.buttonTextDefault);
778 let textFallback = viewSpec.buttonTextOverride ||
779 viewSpec.buttonTextDefault;
780 buttonHint = formatWithOrdinals(viewSpec.buttonTitleOverride ||
781 viewSpec.buttonTitleDefault ||
782 calendarOptions.viewHint, [textFallback, buttonName], // view-name = buttonName
783 textFallback);
784 }
785 else if (calendarApi[buttonName]) { // a calendarApi method
786 buttonClick = () => {
787 calendarApi[buttonName]();
788 };
789 (buttonText = calendarButtonTextOverrides[buttonName]) ||
790 (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
791 (buttonText = calendarButtonText[buttonName]); // everything else is considered default
792 if (buttonName === 'prevYear' || buttonName === 'nextYear') {
793 let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next';
794 buttonHint = formatWithOrdinals(calendarButtonHintOverrides[prevOrNext] ||
795 calendarButtonHints[prevOrNext], [
796 calendarButtonText.year || 'year',
797 'year',
798 ], calendarButtonText[buttonName]);
799 }
800 else {
801 buttonHint = (navUnit) => formatWithOrdinals(calendarButtonHintOverrides[buttonName] ||
802 calendarButtonHints[buttonName], [
803 calendarButtonText[navUnit] || navUnit,
804 navUnit,
805 ], calendarButtonText[buttonName]);
806 }
807 }
808 return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint };
809 })));
810 return { widgets, viewsWithButtons, hasTitle };
811}
812
813// always represents the current view. otherwise, it'd need to change value every time date changes
814class ViewImpl {
815 constructor(type, getCurrentData, dateEnv) {
816 this.type = type;
817 this.getCurrentData = getCurrentData;
818 this.dateEnv = dateEnv;
819 }
820 get calendar() {
821 return this.getCurrentData().calendarApi;
822 }
823 get title() {
824 return this.getCurrentData().viewTitle;
825 }
826 get activeStart() {
827 return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start);
828 }
829 get activeEnd() {
830 return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end);
831 }
832 get currentStart() {
833 return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start);
834 }
835 get currentEnd() {
836 return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end);
837 }
838 getOption(name) {
839 return this.getCurrentData().options[name]; // are the view-specific options
840 }
841}
842
843let eventSourceDef$2 = {
844 ignoreRange: true,
845 parseMeta(refined) {
846 if (Array.isArray(refined.events)) {
847 return refined.events;
848 }
849 return null;
850 },
851 fetch(arg, successCallback) {
852 successCallback({
853 rawEvents: arg.eventSource.meta,
854 });
855 },
856};
857const arrayEventSourcePlugin = createPlugin({
858 name: 'array-event-source',
859 eventSourceDefs: [eventSourceDef$2],
860});
861
862let eventSourceDef$1 = {
863 parseMeta(refined) {
864 if (typeof refined.events === 'function') {
865 return refined.events;
866 }
867 return null;
868 },
869 fetch(arg, successCallback, errorCallback) {
870 const { dateEnv } = arg.context;
871 const func = arg.eventSource.meta;
872 unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback);
873 },
874};
875const funcEventSourcePlugin = createPlugin({
876 name: 'func-event-source',
877 eventSourceDefs: [eventSourceDef$1],
878});
879
880const JSON_FEED_EVENT_SOURCE_REFINERS = {
881 method: String,
882 extraParams: identity,
883 startParam: String,
884 endParam: String,
885 timeZoneParam: String,
886};
887
888let eventSourceDef = {
889 parseMeta(refined) {
890 if (refined.url && (refined.format === 'json' || !refined.format)) {
891 return {
892 url: refined.url,
893 format: 'json',
894 method: (refined.method || 'GET').toUpperCase(),
895 extraParams: refined.extraParams,
896 startParam: refined.startParam,
897 endParam: refined.endParam,
898 timeZoneParam: refined.timeZoneParam,
899 };
900 }
901 return null;
902 },
903 fetch(arg, successCallback, errorCallback) {
904 const { meta } = arg.eventSource;
905 const requestParams = buildRequestParams(meta, arg.range, arg.context);
906 requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => {
907 successCallback({ rawEvents, response });
908 }, errorCallback);
909 },
910};
911const jsonFeedEventSourcePlugin = createPlugin({
912 name: 'json-event-source',
913 eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS,
914 eventSourceDefs: [eventSourceDef],
915});
916function buildRequestParams(meta, range, context) {
917 let { dateEnv, options } = context;
918 let startParam;
919 let endParam;
920 let timeZoneParam;
921 let customRequestParams;
922 let params = {};
923 startParam = meta.startParam;
924 if (startParam == null) {
925 startParam = options.startParam;
926 }
927 endParam = meta.endParam;
928 if (endParam == null) {
929 endParam = options.endParam;
930 }
931 timeZoneParam = meta.timeZoneParam;
932 if (timeZoneParam == null) {
933 timeZoneParam = options.timeZoneParam;
934 }
935 // retrieve any outbound GET/POST data from the options
936 if (typeof meta.extraParams === 'function') {
937 // supplied as a function that returns a key/value object
938 customRequestParams = meta.extraParams();
939 }
940 else {
941 // probably supplied as a straight key/value object
942 customRequestParams = meta.extraParams || {};
943 }
944 Object.assign(params, customRequestParams);
945 params[startParam] = dateEnv.formatIso(range.start);
946 params[endParam] = dateEnv.formatIso(range.end);
947 if (dateEnv.timeZone !== 'local') {
948 params[timeZoneParam] = dateEnv.timeZone;
949 }
950 return params;
951}
952
953const SIMPLE_RECURRING_REFINERS = {
954 daysOfWeek: identity,
955 startTime: createDuration,
956 endTime: createDuration,
957 duration: createDuration,
958 startRecur: identity,
959 endRecur: identity,
960};
961
962let recurring = {
963 parse(refined, dateEnv) {
964 if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) {
965 let recurringData = {
966 daysOfWeek: refined.daysOfWeek || null,
967 startTime: refined.startTime || null,
968 endTime: refined.endTime || null,
969 startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null,
970 endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null,
971 };
972 let duration;
973 if (refined.duration) {
974 duration = refined.duration;
975 }
976 if (!duration && refined.startTime && refined.endTime) {
977 duration = subtractDurations(refined.endTime, refined.startTime);
978 }
979 return {
980 allDayGuess: Boolean(!refined.startTime && !refined.endTime),
981 duration,
982 typeData: recurringData, // doesn't need endTime anymore but oh well
983 };
984 }
985 return null;
986 },
987 expand(typeData, framingRange, dateEnv) {
988 let clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
989 if (clippedFramingRange) {
990 return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv);
991 }
992 return [];
993 },
994};
995const simpleRecurringEventsPlugin = createPlugin({
996 name: 'simple-recurring-event',
997 recurringTypes: [recurring],
998 eventRefiners: SIMPLE_RECURRING_REFINERS,
999});
1000function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) {
1001 let dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null;
1002 let dayMarker = startOfDay(framingRange.start);
1003 let endMarker = framingRange.end;
1004 let instanceStarts = [];
1005 while (dayMarker < endMarker) {
1006 let instanceStart;
1007 // if everyday, or this particular day-of-week
1008 if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
1009 if (startTime) {
1010 instanceStart = dateEnv.add(dayMarker, startTime);
1011 }
1012 else {
1013 instanceStart = dayMarker;
1014 }
1015 instanceStarts.push(instanceStart);
1016 }
1017 dayMarker = addDays(dayMarker, 1);
1018 }
1019 return instanceStarts;
1020}
1021
1022const changeHandlerPlugin = createPlugin({
1023 name: 'change-handler',
1024 optionChangeHandlers: {
1025 events(events, context) {
1026 handleEventSources([events], context);
1027 },
1028 eventSources: handleEventSources,
1029 },
1030});
1031/*
1032BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out
1033*/
1034function handleEventSources(inputs, context) {
1035 let unfoundSources = hashValuesToArray(context.getCurrentData().eventSources);
1036 if (unfoundSources.length === 1 &&
1037 inputs.length === 1 &&
1038 Array.isArray(unfoundSources[0]._raw) &&
1039 Array.isArray(inputs[0])) {
1040 context.dispatch({
1041 type: 'RESET_RAW_EVENTS',
1042 sourceId: unfoundSources[0].sourceId,
1043 rawEvents: inputs[0],
1044 });
1045 return;
1046 }
1047 let newInputs = [];
1048 for (let input of inputs) {
1049 let inputFound = false;
1050 for (let i = 0; i < unfoundSources.length; i += 1) {
1051 if (unfoundSources[i]._raw === input) {
1052 unfoundSources.splice(i, 1); // delete
1053 inputFound = true;
1054 break;
1055 }
1056 }
1057 if (!inputFound) {
1058 newInputs.push(input);
1059 }
1060 }
1061 for (let unfoundSource of unfoundSources) {
1062 context.dispatch({
1063 type: 'REMOVE_EVENT_SOURCE',
1064 sourceId: unfoundSource.sourceId,
1065 });
1066 }
1067 for (let newInput of newInputs) {
1068 context.calendarApi.addEventSource(newInput);
1069 }
1070}
1071
1072function handleDateProfile(dateProfile, context) {
1073 context.emitter.trigger('datesSet', Object.assign(Object.assign({}, buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi }));
1074}
1075
1076function handleEventStore(eventStore, context) {
1077 let { emitter } = context;
1078 if (emitter.hasHandlers('eventsSet')) {
1079 emitter.trigger('eventsSet', buildEventApis(eventStore, context));
1080 }
1081}
1082
1083/*
1084this array is exposed on the root namespace so that UMD plugins can add to it.
1085see the rollup-bundles script.
1086*/
1087const globalPlugins = [
1088 arrayEventSourcePlugin,
1089 funcEventSourcePlugin,
1090 jsonFeedEventSourcePlugin,
1091 simpleRecurringEventsPlugin,
1092 changeHandlerPlugin,
1093 createPlugin({
1094 name: 'misc',
1095 isLoadingFuncs: [
1096 (state) => computeEventSourcesLoading(state.eventSources),
1097 ],
1098 propSetHandlers: {
1099 dateProfile: handleDateProfile,
1100 eventStore: handleEventStore,
1101 },
1102 }),
1103];
1104
1105class TaskRunner {
1106 constructor(runTaskOption, drainedOption) {
1107 this.runTaskOption = runTaskOption;
1108 this.drainedOption = drainedOption;
1109 this.queue = [];
1110 this.delayedRunner = new DelayedRunner(this.drain.bind(this));
1111 }
1112 request(task, delay) {
1113 this.queue.push(task);
1114 this.delayedRunner.request(delay);
1115 }
1116 pause(scope) {
1117 this.delayedRunner.pause(scope);
1118 }
1119 resume(scope, force) {
1120 this.delayedRunner.resume(scope, force);
1121 }
1122 drain() {
1123 let { queue } = this;
1124 while (queue.length) {
1125 let completedTasks = [];
1126 let task;
1127 while ((task = queue.shift())) {
1128 this.runTask(task);
1129 completedTasks.push(task);
1130 }
1131 this.drained(completedTasks);
1132 } // keep going, in case new tasks were added in the drained handler
1133 }
1134 runTask(task) {
1135 if (this.runTaskOption) {
1136 this.runTaskOption(task);
1137 }
1138 }
1139 drained(completedTasks) {
1140 if (this.drainedOption) {
1141 this.drainedOption(completedTasks);
1142 }
1143 }
1144}
1145
1146// Computes what the title at the top of the calendarApi should be for this view
1147function buildTitle(dateProfile, viewOptions, dateEnv) {
1148 let range;
1149 // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
1150 if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
1151 range = dateProfile.currentRange;
1152 }
1153 else { // for day units or smaller, use the actual day range
1154 range = dateProfile.activeRange;
1155 }
1156 return dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), {
1157 isEndExclusive: dateProfile.isRangeAllDay,
1158 defaultSeparator: viewOptions.titleRangeSeparator,
1159 });
1160}
1161// Generates the format string that should be used to generate the title for the current date range.
1162// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
1163function buildTitleFormat(dateProfile) {
1164 let { currentRangeUnit } = dateProfile;
1165 if (currentRangeUnit === 'year') {
1166 return { year: 'numeric' };
1167 }
1168 if (currentRangeUnit === 'month') {
1169 return { year: 'numeric', month: 'long' }; // like "September 2014"
1170 }
1171 let days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
1172 if (days !== null && days > 1) {
1173 // multi-day range. shorter, like "Sep 9 - 10 2014"
1174 return { year: 'numeric', month: 'short', day: 'numeric' };
1175 }
1176 // one day. longer, like "September 9 2014"
1177 return { year: 'numeric', month: 'long', day: 'numeric' };
1178}
1179
1180// in future refactor, do the redux-style function(state=initial) for initial-state
1181// also, whatever is happening in constructor, have it happen in action queue too
1182class CalendarDataManager {
1183 constructor(props) {
1184 this.computeCurrentViewData = memoize(this._computeCurrentViewData);
1185 this.organizeRawLocales = memoize(organizeRawLocales);
1186 this.buildLocale = memoize(buildLocale);
1187 this.buildPluginHooks = buildBuildPluginHooks();
1188 this.buildDateEnv = memoize(buildDateEnv$1);
1189 this.buildTheme = memoize(buildTheme);
1190 this.parseToolbars = memoize(parseToolbars);
1191 this.buildViewSpecs = memoize(buildViewSpecs);
1192 this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator);
1193 this.buildViewApi = memoize(buildViewApi);
1194 this.buildViewUiProps = memoizeObjArg(buildViewUiProps);
1195 this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual);
1196 this.buildEventUiBases = memoize(buildEventUiBases);
1197 this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours);
1198 this.buildTitle = memoize(buildTitle);
1199 this.emitter = new Emitter();
1200 this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this));
1201 this.currentCalendarOptionsInput = {};
1202 this.currentCalendarOptionsRefined = {};
1203 this.currentViewOptionsInput = {};
1204 this.currentViewOptionsRefined = {};
1205 this.currentCalendarOptionsRefiners = {};
1206 this.optionsForRefining = [];
1207 this.optionsForHandling = [];
1208 this.getCurrentData = () => this.data;
1209 this.dispatch = (action) => {
1210 this.actionRunner.request(action); // protects against recursive calls to _handleAction
1211 };
1212 this.props = props;
1213 this.actionRunner.pause();
1214 let dynamicOptionOverrides = {};
1215 let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
1216 let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView;
1217 let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
1218 // wire things up
1219 // TODO: not DRY
1220 props.calendarApi.currentDataManager = this;
1221 this.emitter.setThisContext(props.calendarApi);
1222 this.emitter.setOptions(currentViewData.options);
1223 let currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv);
1224 let dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
1225 if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) {
1226 currentDate = dateProfile.currentRange.start;
1227 }
1228 let calendarContext = {
1229 dateEnv: optionsData.dateEnv,
1230 options: optionsData.calendarOptions,
1231 pluginHooks: optionsData.pluginHooks,
1232 calendarApi: props.calendarApi,
1233 dispatch: this.dispatch,
1234 emitter: this.emitter,
1235 getCurrentData: this.getCurrentData,
1236 };
1237 // needs to be after setThisContext
1238 for (let callback of optionsData.pluginHooks.contextInit) {
1239 callback(calendarContext);
1240 }
1241 // NOT DRY
1242 let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext);
1243 let initialState = {
1244 dynamicOptionOverrides,
1245 currentViewType,
1246 currentDate,
1247 dateProfile,
1248 businessHours: this.parseContextBusinessHours(calendarContext),
1249 eventSources,
1250 eventUiBases: {},
1251 eventStore: createEmptyEventStore(),
1252 renderableEventStore: createEmptyEventStore(),
1253 dateSelection: null,
1254 eventSelection: '',
1255 eventDrag: null,
1256 eventResize: null,
1257 selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig,
1258 };
1259 let contextAndState = Object.assign(Object.assign({}, calendarContext), initialState);
1260 for (let reducer of optionsData.pluginHooks.reducers) {
1261 Object.assign(initialState, reducer(null, null, contextAndState));
1262 }
1263 if (computeIsLoading(initialState, calendarContext)) {
1264 this.emitter.trigger('loading', true); // NOT DRY
1265 }
1266 this.state = initialState;
1267 this.updateData();
1268 this.actionRunner.resume();
1269 }
1270 resetOptions(optionOverrides, changedOptionNames) {
1271 let { props } = this;
1272 if (changedOptionNames === undefined) {
1273 props.optionOverrides = optionOverrides;
1274 }
1275 else {
1276 props.optionOverrides = Object.assign(Object.assign({}, (props.optionOverrides || {})), optionOverrides);
1277 this.optionsForRefining.push(...changedOptionNames);
1278 }
1279 if (changedOptionNames === undefined || changedOptionNames.length) {
1280 this.actionRunner.request({
1281 type: 'NOTHING',
1282 });
1283 }
1284 }
1285 _handleAction(action) {
1286 let { props, state, emitter } = this;
1287 let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action);
1288 let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
1289 let currentViewType = reduceViewType(state.currentViewType, action);
1290 let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
1291 // wire things up
1292 // TODO: not DRY
1293 props.calendarApi.currentDataManager = this;
1294 emitter.setThisContext(props.calendarApi);
1295 emitter.setOptions(currentViewData.options);
1296 let calendarContext = {
1297 dateEnv: optionsData.dateEnv,
1298 options: optionsData.calendarOptions,
1299 pluginHooks: optionsData.pluginHooks,
1300 calendarApi: props.calendarApi,
1301 dispatch: this.dispatch,
1302 emitter,
1303 getCurrentData: this.getCurrentData,
1304 };
1305 let { currentDate, dateProfile } = state;
1306 if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack
1307 dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
1308 }
1309 currentDate = reduceCurrentDate(currentDate, action);
1310 dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator);
1311 if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator
1312 action.type === 'NEXT' || // "
1313 !rangeContainsMarker(dateProfile.currentRange, currentDate)) {
1314 currentDate = dateProfile.currentRange.start;
1315 }
1316 let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext);
1317 let eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext);
1318 let isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading
1319 let renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ?
1320 (state.renderableEventStore || eventStore) : // try from previous state
1321 eventStore;
1322 let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext); // will memoize obj
1323 let eventUiBySource = this.buildEventUiBySource(eventSources);
1324 let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
1325 let newState = {
1326 dynamicOptionOverrides,
1327 currentViewType,
1328 currentDate,
1329 dateProfile,
1330 eventSources,
1331 eventStore,
1332 renderableEventStore,
1333 selectionConfig,
1334 eventUiBases,
1335 businessHours: this.parseContextBusinessHours(calendarContext),
1336 dateSelection: reduceDateSelection(state.dateSelection, action),
1337 eventSelection: reduceSelectedEvent(state.eventSelection, action),
1338 eventDrag: reduceEventDrag(state.eventDrag, action),
1339 eventResize: reduceEventResize(state.eventResize, action),
1340 };
1341 let contextAndState = Object.assign(Object.assign({}, calendarContext), newState);
1342 for (let reducer of optionsData.pluginHooks.reducers) {
1343 Object.assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value
1344 }
1345 let wasLoading = computeIsLoading(state, calendarContext);
1346 let isLoading = computeIsLoading(newState, calendarContext);
1347 // TODO: use propSetHandlers in plugin system
1348 if (!wasLoading && isLoading) {
1349 emitter.trigger('loading', true);
1350 }
1351 else if (wasLoading && !isLoading) {
1352 emitter.trigger('loading', false);
1353 }
1354 this.state = newState;
1355 if (props.onAction) {
1356 props.onAction(action);
1357 }
1358 }
1359 updateData() {
1360 let { props, state } = this;
1361 let oldData = this.data;
1362 let optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi);
1363 let currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides);
1364 let data = this.data = Object.assign(Object.assign(Object.assign({ viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state);
1365 let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
1366 let oldCalendarOptions = oldData && oldData.calendarOptions;
1367 let newCalendarOptions = optionsData.calendarOptions;
1368 if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) {
1369 if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
1370 // hack
1371 state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data);
1372 state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv);
1373 state.renderableEventStore = data.renderableEventStore = rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv);
1374 }
1375 for (let optionName in changeHandlers) {
1376 if (this.optionsForHandling.indexOf(optionName) !== -1 ||
1377 oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
1378 changeHandlers[optionName](newCalendarOptions[optionName], data);
1379 }
1380 }
1381 }
1382 this.optionsForHandling = [];
1383 if (props.onData) {
1384 props.onData(data);
1385 }
1386 }
1387 computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi) {
1388 // TODO: blacklist options that are handled by optionChangeHandlers
1389 if (!this.optionsForRefining.length &&
1390 optionOverrides === this.stableOptionOverrides &&
1391 dynamicOptionOverrides === this.stableDynamicOptionOverrides) {
1392 return this.stableCalendarOptionsData;
1393 }
1394 let { refinedOptions, pluginHooks, localeDefaults, availableLocaleData, extra, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
1395 warnUnknownOptions(extra);
1396 let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator);
1397 let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults);
1398 let theme = this.buildTheme(refinedOptions, pluginHooks);
1399 let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi);
1400 return this.stableCalendarOptionsData = {
1401 calendarOptions: refinedOptions,
1402 pluginHooks,
1403 dateEnv,
1404 viewSpecs,
1405 theme,
1406 toolbarConfig,
1407 localeDefaults,
1408 availableRawLocales: availableLocaleData.map,
1409 };
1410 }
1411 // always called from behind a memoizer
1412 processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
1413 let { locales, locale } = mergeRawOptions([
1414 BASE_OPTION_DEFAULTS,
1415 optionOverrides,
1416 dynamicOptionOverrides,
1417 ]);
1418 let availableLocaleData = this.organizeRawLocales(locales);
1419 let availableRawLocales = availableLocaleData.map;
1420 let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options;
1421 let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins);
1422 let refiners = this.currentCalendarOptionsRefiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
1423 let extra = {};
1424 let raw = mergeRawOptions([
1425 BASE_OPTION_DEFAULTS,
1426 localeDefaults,
1427 optionOverrides,
1428 dynamicOptionOverrides,
1429 ]);
1430 let refined = {};
1431 let currentRaw = this.currentCalendarOptionsInput;
1432 let currentRefined = this.currentCalendarOptionsRefined;
1433 let anyChanges = false;
1434 for (let optionName in raw) {
1435 if (this.optionsForRefining.indexOf(optionName) === -1 && (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
1436 (optionName in currentRaw) &&
1437 COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])))) {
1438 refined[optionName] = currentRefined[optionName];
1439 }
1440 else if (refiners[optionName]) {
1441 refined[optionName] = refiners[optionName](raw[optionName]);
1442 anyChanges = true;
1443 }
1444 else {
1445 extra[optionName] = currentRaw[optionName];
1446 }
1447 }
1448 if (anyChanges) {
1449 this.currentCalendarOptionsInput = raw;
1450 this.currentCalendarOptionsRefined = refined;
1451 this.stableOptionOverrides = optionOverrides;
1452 this.stableDynamicOptionOverrides = dynamicOptionOverrides;
1453 }
1454 this.optionsForHandling.push(...this.optionsForRefining);
1455 this.optionsForRefining = [];
1456 return {
1457 rawOptions: this.currentCalendarOptionsInput,
1458 refinedOptions: this.currentCalendarOptionsRefined,
1459 pluginHooks,
1460 availableLocaleData,
1461 localeDefaults,
1462 extra,
1463 };
1464 }
1465 _computeCurrentViewData(viewType, optionsData, optionOverrides, dynamicOptionOverrides) {
1466 let viewSpec = optionsData.viewSpecs[viewType];
1467 if (!viewSpec) {
1468 throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`);
1469 }
1470 let { refinedOptions, extra } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
1471 warnUnknownOptions(extra);
1472 let dateProfileGenerator = this.buildDateProfileGenerator({
1473 dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
1474 duration: viewSpec.duration,
1475 durationUnit: viewSpec.durationUnit,
1476 usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
1477 dateEnv: optionsData.dateEnv,
1478 calendarApi: this.props.calendarApi,
1479 slotMinTime: refinedOptions.slotMinTime,
1480 slotMaxTime: refinedOptions.slotMaxTime,
1481 showNonCurrentDates: refinedOptions.showNonCurrentDates,
1482 dayCount: refinedOptions.dayCount,
1483 dateAlignment: refinedOptions.dateAlignment,
1484 dateIncrement: refinedOptions.dateIncrement,
1485 hiddenDays: refinedOptions.hiddenDays,
1486 weekends: refinedOptions.weekends,
1487 nowInput: refinedOptions.now,
1488 validRangeInput: refinedOptions.validRange,
1489 visibleRangeInput: refinedOptions.visibleRange,
1490 fixedWeekCount: refinedOptions.fixedWeekCount,
1491 });
1492 let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv);
1493 return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi };
1494 }
1495 processRawViewOptions(viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) {
1496 let raw = mergeRawOptions([
1497 BASE_OPTION_DEFAULTS,
1498 viewSpec.optionDefaults,
1499 localeDefaults,
1500 optionOverrides,
1501 viewSpec.optionOverrides,
1502 dynamicOptionOverrides,
1503 ]);
1504 let refiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
1505 let refined = {};
1506 let currentRaw = this.currentViewOptionsInput;
1507 let currentRefined = this.currentViewOptionsRefined;
1508 let anyChanges = false;
1509 let extra = {};
1510 for (let optionName in raw) {
1511 if (raw[optionName] === currentRaw[optionName] ||
1512 (COMPLEX_OPTION_COMPARATORS[optionName] &&
1513 COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName]))) {
1514 refined[optionName] = currentRefined[optionName];
1515 }
1516 else {
1517 if (raw[optionName] === this.currentCalendarOptionsInput[optionName] ||
1518 (COMPLEX_OPTION_COMPARATORS[optionName] &&
1519 COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName]))) {
1520 if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop
1521 refined[optionName] = this.currentCalendarOptionsRefined[optionName];
1522 }
1523 }
1524 else if (refiners[optionName]) {
1525 refined[optionName] = refiners[optionName](raw[optionName]);
1526 }
1527 else {
1528 extra[optionName] = raw[optionName];
1529 }
1530 anyChanges = true;
1531 }
1532 }
1533 if (anyChanges) {
1534 this.currentViewOptionsInput = raw;
1535 this.currentViewOptionsRefined = refined;
1536 }
1537 return {
1538 rawOptions: this.currentViewOptionsInput,
1539 refinedOptions: this.currentViewOptionsRefined,
1540 extra,
1541 };
1542 }
1543}
1544function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) {
1545 let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
1546 return new DateEnv({
1547 calendarSystem: 'gregory',
1548 timeZone,
1549 namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
1550 locale,
1551 weekNumberCalculation,
1552 firstDay,
1553 weekText,
1554 cmdFormatter: pluginHooks.cmdFormatter,
1555 defaultSeparator,
1556 });
1557}
1558function buildTheme(options, pluginHooks) {
1559 let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme;
1560 return new ThemeClass(options);
1561}
1562function buildDateProfileGenerator(props) {
1563 let DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator;
1564 return new DateProfileGeneratorClass(props);
1565}
1566function buildViewApi(type, getCurrentData, dateEnv) {
1567 return new ViewImpl(type, getCurrentData, dateEnv);
1568}
1569function buildEventUiBySource(eventSources) {
1570 return mapHash(eventSources, (eventSource) => eventSource.ui);
1571}
1572function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
1573 let eventUiBases = { '': eventUiSingleBase };
1574 for (let defId in eventDefs) {
1575 let def = eventDefs[defId];
1576 if (def.sourceId && eventUiBySource[def.sourceId]) {
1577 eventUiBases[defId] = eventUiBySource[def.sourceId];
1578 }
1579 }
1580 return eventUiBases;
1581}
1582function buildViewUiProps(calendarContext) {
1583 let { options } = calendarContext;
1584 return {
1585 eventUiSingleBase: createEventUi({
1586 display: options.eventDisplay,
1587 editable: options.editable,
1588 startEditable: options.eventStartEditable,
1589 durationEditable: options.eventDurationEditable,
1590 constraint: options.eventConstraint,
1591 overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
1592 allow: options.eventAllow,
1593 backgroundColor: options.eventBackgroundColor,
1594 borderColor: options.eventBorderColor,
1595 textColor: options.eventTextColor,
1596 color: options.eventColor,
1597 // classNames: options.eventClassNames // render hook will handle this
1598 }, calendarContext),
1599 selectionConfig: createEventUi({
1600 constraint: options.selectConstraint,
1601 overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined,
1602 allow: options.selectAllow,
1603 }, calendarContext),
1604 };
1605}
1606function computeIsLoading(state, context) {
1607 for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) {
1608 if (isLoadingFunc(state)) {
1609 return true;
1610 }
1611 }
1612 return false;
1613}
1614function parseContextBusinessHours(calendarContext) {
1615 return parseBusinessHours(calendarContext.options.businessHours, calendarContext);
1616}
1617function warnUnknownOptions(options, viewName) {
1618 for (let optionName in options) {
1619 console.warn(`Unknown option '${optionName}'` +
1620 (viewName ? ` for view '${viewName}'` : ''));
1621 }
1622}
1623
1624class ToolbarSection extends BaseComponent {
1625 render() {
1626 let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
1627 return createElement('div', { className: 'fc-toolbar-chunk' }, ...children);
1628 }
1629 renderWidgetGroup(widgetGroup) {
1630 let { props } = this;
1631 let { theme } = this.context;
1632 let children = [];
1633 let isOnlyButtons = true;
1634 for (let widget of widgetGroup) {
1635 let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget;
1636 if (buttonName === 'title') {
1637 isOnlyButtons = false;
1638 children.push(createElement("h2", { className: "fc-toolbar-title", id: props.titleId }, props.title));
1639 }
1640 else {
1641 let isPressed = buttonName === props.activeButton;
1642 let isDisabled = (!props.isTodayEnabled && buttonName === 'today') ||
1643 (!props.isPrevEnabled && buttonName === 'prev') ||
1644 (!props.isNextEnabled && buttonName === 'next');
1645 let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')];
1646 if (isPressed) {
1647 buttonClasses.push(theme.getClass('buttonActive'));
1648 }
1649 children.push(createElement("button", { type: "button", title: typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint, disabled: isDisabled, "aria-pressed": isPressed, className: buttonClasses.join(' '), onClick: buttonClick }, buttonText || (buttonIcon ? createElement("span", { className: buttonIcon, role: "img" }) : '')));
1650 }
1651 }
1652 if (children.length > 1) {
1653 let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || '';
1654 return createElement('div', { className: groupClassName }, ...children);
1655 }
1656 return children[0];
1657 }
1658}
1659
1660class Toolbar extends BaseComponent {
1661 render() {
1662 let { model, extraClassName } = this.props;
1663 let forceLtr = false;
1664 let startContent;
1665 let endContent;
1666 let sectionWidgets = model.sectionWidgets;
1667 let centerContent = sectionWidgets.center;
1668 if (sectionWidgets.left) {
1669 forceLtr = true;
1670 startContent = sectionWidgets.left;
1671 }
1672 else {
1673 startContent = sectionWidgets.start;
1674 }
1675 if (sectionWidgets.right) {
1676 forceLtr = true;
1677 endContent = sectionWidgets.right;
1678 }
1679 else {
1680 endContent = sectionWidgets.end;
1681 }
1682 let classNames = [
1683 extraClassName || '',
1684 'fc-toolbar',
1685 forceLtr ? 'fc-toolbar-ltr' : '',
1686 ];
1687 return (createElement("div", { className: classNames.join(' ') },
1688 this.renderSection('start', startContent || []),
1689 this.renderSection('center', centerContent || []),
1690 this.renderSection('end', endContent || [])));
1691 }
1692 renderSection(key, widgetGroups) {
1693 let { props } = this;
1694 return (createElement(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, navUnit: props.navUnit, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled, titleId: props.titleId }));
1695 }
1696}
1697
1698class ViewHarness extends BaseComponent {
1699 constructor() {
1700 super(...arguments);
1701 this.state = {
1702 availableWidth: null,
1703 };
1704 this.handleEl = (el) => {
1705 this.el = el;
1706 setRef(this.props.elRef, el);
1707 this.updateAvailableWidth();
1708 };
1709 this.handleResize = () => {
1710 this.updateAvailableWidth();
1711 };
1712 }
1713 render() {
1714 let { props, state } = this;
1715 let { aspectRatio } = props;
1716 let classNames = [
1717 'fc-view-harness',
1718 (aspectRatio || props.liquid || props.height)
1719 ? 'fc-view-harness-active' // harness controls the height
1720 : 'fc-view-harness-passive', // let the view do the height
1721 ];
1722 let height = '';
1723 let paddingBottom = '';
1724 if (aspectRatio) {
1725 if (state.availableWidth !== null) {
1726 height = state.availableWidth / aspectRatio;
1727 }
1728 else {
1729 // while waiting to know availableWidth, we can't set height to *zero*
1730 // because will cause lots of unnecessary scrollbars within scrollgrid.
1731 // BETTER: don't start rendering ANYTHING yet until we know container width
1732 // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606)
1733 paddingBottom = `${(1 / aspectRatio) * 100}%`;
1734 }
1735 }
1736 else {
1737 height = props.height || '';
1738 }
1739 return (createElement("div", { "aria-labelledby": props.labeledById, ref: this.handleEl, className: classNames.join(' '), style: { height, paddingBottom } }, props.children));
1740 }
1741 componentDidMount() {
1742 this.context.addResizeHandler(this.handleResize);
1743 }
1744 componentWillUnmount() {
1745 this.context.removeResizeHandler(this.handleResize);
1746 }
1747 updateAvailableWidth() {
1748 if (this.el && // needed. but why?
1749 this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth
1750 ) {
1751 this.setState({ availableWidth: this.el.offsetWidth });
1752 }
1753 }
1754}
1755
1756/*
1757Detects when the user clicks on an event within a DateComponent
1758*/
1759class EventClicking extends Interaction {
1760 constructor(settings) {
1761 super(settings);
1762 this.handleSegClick = (ev, segEl) => {
1763 let { component } = this;
1764 let { context } = component;
1765 let seg = getElSeg(segEl);
1766 if (seg && // might be the <div> surrounding the more link
1767 component.isValidSegDownEl(ev.target)) {
1768 // our way to simulate a link click for elements that can't be <a> tags
1769 // grab before trigger fired in case trigger trashes DOM thru rerendering
1770 let hasUrlContainer = elementClosest(ev.target, '.fc-event-forced-url');
1771 let url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : '';
1772 context.emitter.trigger('eventClick', {
1773 el: segEl,
1774 event: new EventImpl(component.context, seg.eventRange.def, seg.eventRange.instance),
1775 jsEvent: ev,
1776 view: context.viewApi,
1777 });
1778 if (url && !ev.defaultPrevented) {
1779 window.location.href = url;
1780 }
1781 }
1782 };
1783 this.destroy = listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events
1784 this.handleSegClick);
1785 }
1786}
1787
1788/*
1789Triggers events and adds/removes core classNames when the user's pointer
1790enters/leaves event-elements of a component.
1791*/
1792class EventHovering extends Interaction {
1793 constructor(settings) {
1794 super(settings);
1795 // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
1796 this.handleEventElRemove = (el) => {
1797 if (el === this.currentSegEl) {
1798 this.handleSegLeave(null, this.currentSegEl);
1799 }
1800 };
1801 this.handleSegEnter = (ev, segEl) => {
1802 if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
1803 this.currentSegEl = segEl;
1804 this.triggerEvent('eventMouseEnter', ev, segEl);
1805 }
1806 };
1807 this.handleSegLeave = (ev, segEl) => {
1808 if (this.currentSegEl) {
1809 this.currentSegEl = null;
1810 this.triggerEvent('eventMouseLeave', ev, segEl);
1811 }
1812 };
1813 this.removeHoverListeners = listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events
1814 this.handleSegEnter, this.handleSegLeave);
1815 }
1816 destroy() {
1817 this.removeHoverListeners();
1818 }
1819 triggerEvent(publicEvName, ev, segEl) {
1820 let { component } = this;
1821 let { context } = component;
1822 let seg = getElSeg(segEl);
1823 if (!ev || component.isValidSegDownEl(ev.target)) {
1824 context.emitter.trigger(publicEvName, {
1825 el: segEl,
1826 event: new EventImpl(context, seg.eventRange.def, seg.eventRange.instance),
1827 jsEvent: ev,
1828 view: context.viewApi,
1829 });
1830 }
1831 }
1832}
1833
1834class CalendarContent extends PureComponent {
1835 constructor() {
1836 super(...arguments);
1837 this.buildViewContext = memoize(buildViewContext);
1838 this.buildViewPropTransformers = memoize(buildViewPropTransformers);
1839 this.buildToolbarProps = memoize(buildToolbarProps);
1840 this.headerRef = createRef();
1841 this.footerRef = createRef();
1842 this.interactionsStore = {};
1843 // eslint-disable-next-line
1844 this.state = {
1845 viewLabelId: getUniqueDomId(),
1846 };
1847 // Component Registration
1848 // -----------------------------------------------------------------------------------------------------------------
1849 this.registerInteractiveComponent = (component, settingsInput) => {
1850 let settings = parseInteractionSettings(component, settingsInput);
1851 let DEFAULT_INTERACTIONS = [
1852 EventClicking,
1853 EventHovering,
1854 ];
1855 let interactionClasses = DEFAULT_INTERACTIONS.concat(this.props.pluginHooks.componentInteractions);
1856 let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings));
1857 this.interactionsStore[component.uid] = interactions;
1858 interactionSettingsStore[component.uid] = settings;
1859 };
1860 this.unregisterInteractiveComponent = (component) => {
1861 let listeners = this.interactionsStore[component.uid];
1862 if (listeners) {
1863 for (let listener of listeners) {
1864 listener.destroy();
1865 }
1866 delete this.interactionsStore[component.uid];
1867 }
1868 delete interactionSettingsStore[component.uid];
1869 };
1870 // Resizing
1871 // -----------------------------------------------------------------------------------------------------------------
1872 this.resizeRunner = new DelayedRunner(() => {
1873 this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ?
1874 this.props.emitter.trigger('windowResize', { view: this.props.viewApi });
1875 });
1876 this.handleWindowResize = (ev) => {
1877 let { options } = this.props;
1878 if (options.handleWindowResize &&
1879 ev.target === window // avoid jqui events
1880 ) {
1881 this.resizeRunner.request(options.windowResizeDelay);
1882 }
1883 };
1884 }
1885 /*
1886 renders INSIDE of an outer div
1887 */
1888 render() {
1889 let { props } = this;
1890 let { toolbarConfig, options } = props;
1891 let toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, getNow(props.options.now, props.dateEnv), // TODO: use NowTimer????
1892 props.viewTitle);
1893 let viewVGrow = false;
1894 let viewHeight = '';
1895 let viewAspectRatio;
1896 if (props.isHeightAuto || props.forPrint) {
1897 viewHeight = '';
1898 }
1899 else if (options.height != null) {
1900 viewVGrow = true;
1901 }
1902 else if (options.contentHeight != null) {
1903 viewHeight = options.contentHeight;
1904 }
1905 else {
1906 viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall
1907 }
1908 let viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
1909 let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle)
1910 ? this.state.viewLabelId
1911 : undefined;
1912 return (createElement(ViewContextType.Provider, { value: viewContext },
1913 toolbarConfig.header && (createElement(Toolbar, Object.assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.header, titleId: viewLabelId }, toolbarProps))),
1914 createElement(ViewHarness, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, labeledById: viewLabelId },
1915 this.renderView(props),
1916 this.buildAppendContent()),
1917 toolbarConfig.footer && (createElement(Toolbar, Object.assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footer, titleId: "" }, toolbarProps)))));
1918 }
1919 componentDidMount() {
1920 let { props } = this;
1921 this.calendarInteractions = props.pluginHooks.calendarInteractions
1922 .map((CalendarInteractionClass) => new CalendarInteractionClass(props));
1923 window.addEventListener('resize', this.handleWindowResize);
1924 let { propSetHandlers } = props.pluginHooks;
1925 for (let propName in propSetHandlers) {
1926 propSetHandlers[propName](props[propName], props);
1927 }
1928 }
1929 componentDidUpdate(prevProps) {
1930 let { props } = this;
1931 let { propSetHandlers } = props.pluginHooks;
1932 for (let propName in propSetHandlers) {
1933 if (props[propName] !== prevProps[propName]) {
1934 propSetHandlers[propName](props[propName], props);
1935 }
1936 }
1937 }
1938 componentWillUnmount() {
1939 window.removeEventListener('resize', this.handleWindowResize);
1940 this.resizeRunner.clear();
1941 for (let interaction of this.calendarInteractions) {
1942 interaction.destroy();
1943 }
1944 this.props.emitter.trigger('_unmount');
1945 }
1946 buildAppendContent() {
1947 let { props } = this;
1948 let children = props.pluginHooks.viewContainerAppends.map((buildAppendContent) => buildAppendContent(props));
1949 return createElement(Fragment, {}, ...children);
1950 }
1951 renderView(props) {
1952 let { pluginHooks } = props;
1953 let { viewSpec } = props;
1954 let viewProps = {
1955 dateProfile: props.dateProfile,
1956 businessHours: props.businessHours,
1957 eventStore: props.renderableEventStore,
1958 eventUiBases: props.eventUiBases,
1959 dateSelection: props.dateSelection,
1960 eventSelection: props.eventSelection,
1961 eventDrag: props.eventDrag,
1962 eventResize: props.eventResize,
1963 isHeightAuto: props.isHeightAuto,
1964 forPrint: props.forPrint,
1965 };
1966 let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
1967 for (let transformer of transformers) {
1968 Object.assign(viewProps, transformer.transform(viewProps, props));
1969 }
1970 let ViewComponent = viewSpec.component;
1971 return (createElement(ViewComponent, Object.assign({}, viewProps)));
1972 }
1973}
1974function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) {
1975 // don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
1976 let todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason
1977 let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false);
1978 let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false);
1979 return {
1980 title,
1981 activeButton: viewSpec.type,
1982 navUnit: viewSpec.singleUnit,
1983 isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
1984 isPrevEnabled: prevInfo.isValid,
1985 isNextEnabled: nextInfo.isValid,
1986 };
1987}
1988// Plugin
1989// -----------------------------------------------------------------------------------------------------------------
1990function buildViewPropTransformers(theClasses) {
1991 return theClasses.map((TheClass) => new TheClass());
1992}
1993
1994class Calendar extends CalendarImpl {
1995 constructor(el, optionOverrides = {}) {
1996 super();
1997 this.isRendering = false;
1998 this.isRendered = false;
1999 this.currentClassNames = [];
2000 this.customContentRenderId = 0;
2001 this.handleAction = (action) => {
2002 // actions we know we want to render immediately
2003 switch (action.type) {
2004 case 'SET_EVENT_DRAG':
2005 case 'SET_EVENT_RESIZE':
2006 this.renderRunner.tryDrain();
2007 }
2008 };
2009 this.handleData = (data) => {
2010 this.currentData = data;
2011 this.renderRunner.request(data.calendarOptions.rerenderDelay);
2012 };
2013 this.handleRenderRequest = () => {
2014 if (this.isRendering) {
2015 this.isRendered = true;
2016 let { currentData } = this;
2017 flushSync(() => {
2018 render(createElement(CalendarRoot, { options: currentData.calendarOptions, theme: currentData.theme, emitter: currentData.emitter }, (classNames, height, isHeightAuto, forPrint) => {
2019 this.setClassNames(classNames);
2020 this.setHeight(height);
2021 return (createElement(RenderId.Provider, { value: this.customContentRenderId },
2022 createElement(CalendarContent, Object.assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData))));
2023 }), this.el);
2024 });
2025 }
2026 else if (this.isRendered) {
2027 this.isRendered = false;
2028 render(null, this.el);
2029 this.setClassNames([]);
2030 this.setHeight('');
2031 }
2032 };
2033 ensureElHasStyles(el);
2034 this.el = el;
2035 this.renderRunner = new DelayedRunner(this.handleRenderRequest);
2036 new CalendarDataManager({
2037 optionOverrides,
2038 calendarApi: this,
2039 onAction: this.handleAction,
2040 onData: this.handleData,
2041 });
2042 }
2043 render() {
2044 let wasRendering = this.isRendering;
2045 if (!wasRendering) {
2046 this.isRendering = true;
2047 }
2048 else {
2049 this.customContentRenderId += 1;
2050 }
2051 this.renderRunner.request();
2052 if (wasRendering) {
2053 this.updateSize();
2054 }
2055 }
2056 destroy() {
2057 if (this.isRendering) {
2058 this.isRendering = false;
2059 this.renderRunner.request();
2060 }
2061 }
2062 updateSize() {
2063 flushSync(() => {
2064 super.updateSize();
2065 });
2066 }
2067 batchRendering(func) {
2068 this.renderRunner.pause('batchRendering');
2069 func();
2070 this.renderRunner.resume('batchRendering');
2071 }
2072 pauseRendering() {
2073 this.renderRunner.pause('pauseRendering');
2074 }
2075 resumeRendering() {
2076 this.renderRunner.resume('pauseRendering', true);
2077 }
2078 resetOptions(optionOverrides, changedOptionNames) {
2079 this.currentDataManager.resetOptions(optionOverrides, changedOptionNames);
2080 }
2081 setClassNames(classNames) {
2082 if (!isArraysEqual(classNames, this.currentClassNames)) {
2083 let { classList } = this.el;
2084 for (let className of this.currentClassNames) {
2085 classList.remove(className);
2086 }
2087 for (let className of classNames) {
2088 classList.add(className);
2089 }
2090 this.currentClassNames = classNames;
2091 }
2092 }
2093 setHeight(height) {
2094 applyStyleProp(this.el, 'height', height);
2095 }
2096}
2097
2098function formatDate(dateInput, options = {}) {
2099 let dateEnv = buildDateEnv(options);
2100 let formatter = createFormatter(options);
2101 let dateMeta = dateEnv.createMarkerMeta(dateInput);
2102 if (!dateMeta) { // TODO: warning?
2103 return '';
2104 }
2105 return dateEnv.format(dateMeta.marker, formatter, {
2106 forcedTzo: dateMeta.forcedTzo,
2107 });
2108}
2109function formatRange(startInput, endInput, options) {
2110 let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object
2111 let formatter = createFormatter(options);
2112 let startMeta = dateEnv.createMarkerMeta(startInput);
2113 let endMeta = dateEnv.createMarkerMeta(endInput);
2114 if (!startMeta || !endMeta) { // TODO: warning?
2115 return '';
2116 }
2117 return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
2118 forcedStartTzo: startMeta.forcedTzo,
2119 forcedEndTzo: endMeta.forcedTzo,
2120 isEndExclusive: options.isEndExclusive,
2121 defaultSeparator: BASE_OPTION_DEFAULTS.defaultRangeSeparator,
2122 });
2123}
2124// TODO: more DRY and optimized
2125function buildDateEnv(settings) {
2126 let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
2127 return new DateEnv(Object.assign(Object.assign({ timeZone: BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale }));
2128}
2129
2130// HELPERS
2131/*
2132if nextDayThreshold is specified, slicing is done in an all-day fashion.
2133you can get nextDayThreshold from context.nextDayThreshold
2134*/
2135function sliceEvents(props, allDay) {
2136 return sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg;
2137}
2138
2139const version = '6.1.15';
2140
2141export { Calendar, createPlugin, formatDate, formatRange, globalLocales, globalPlugins, sliceEvents, version };