2212 lines
92 KiB
JavaScript
2212 lines
92 KiB
JavaScript
import { 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 rangeContainsMarker, H as createEmptyEventStore, I as reduceEventStore, J as rezoneEventStoreDates, K as mergeRawOptions, L as BASE_OPTION_REFINERS, M as CALENDAR_LISTENER_REFINERS, N as CALENDAR_OPTION_REFINERS, O as COMPLEX_OPTION_COMPARATORS, P as VIEW_OPTION_REFINERS, Q as DateEnv, R as DateProfileGenerator, S as createEventUi, U as parseBusinessHours, W as setRef, X as Interaction, Y as getElSeg, Z as elementClosest, _ as EventImpl, $ as listenBySelector, a0 as listenToHoverBySelector, a1 as PureComponent, a2 as buildViewContext, a3 as getUniqueDomId, a4 as parseInteractionSettings, a5 as interactionSettingsStore, a6 as NowTimer, a7 as CalendarImpl, a8 as flushSync, a9 as CalendarRoot, aa as RenderId, ab as ensureElHasStyles, ac as applyStyleProp, ad as sliceEventStore } from './internal-common.js';
|
|
export { ae as JsonRequestError } from './internal-common.js';
|
|
import { createElement, createRef, Fragment, render } from 'preact';
|
|
import 'preact/compat';
|
|
|
|
const globalLocales = [];
|
|
|
|
const MINIMAL_RAW_EN_LOCALE = {
|
|
code: 'en',
|
|
week: {
|
|
dow: 0,
|
|
doy: 4, // 4 days need to be within the year to be considered the first week
|
|
},
|
|
direction: 'ltr',
|
|
buttonText: {
|
|
prev: 'prev',
|
|
next: 'next',
|
|
prevYear: 'prev year',
|
|
nextYear: 'next year',
|
|
year: 'year',
|
|
today: 'today',
|
|
month: 'month',
|
|
week: 'week',
|
|
day: 'day',
|
|
list: 'list',
|
|
},
|
|
weekText: 'W',
|
|
weekTextLong: 'Week',
|
|
closeHint: 'Close',
|
|
timeHint: 'Time',
|
|
eventHint: 'Event',
|
|
allDayText: 'all-day',
|
|
moreLinkText: 'more',
|
|
noEventsText: 'No events to display',
|
|
};
|
|
const RAW_EN_LOCALE = Object.assign(Object.assign({}, MINIMAL_RAW_EN_LOCALE), {
|
|
// Includes things we don't want other locales to inherit,
|
|
// things that derive from other translatable strings.
|
|
buttonHints: {
|
|
prev: 'Previous $0',
|
|
next: 'Next $0',
|
|
today(buttonText, unit) {
|
|
return (unit === 'day')
|
|
? 'Today'
|
|
: `This ${buttonText}`;
|
|
},
|
|
}, viewHint: '$0 view', navLinkHint: 'Go to $0', moreLinkHint(eventCnt) {
|
|
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`;
|
|
} });
|
|
function organizeRawLocales(explicitRawLocales) {
|
|
let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en';
|
|
let allRawLocales = globalLocales.concat(explicitRawLocales);
|
|
let rawLocaleMap = {
|
|
en: RAW_EN_LOCALE,
|
|
};
|
|
for (let rawLocale of allRawLocales) {
|
|
rawLocaleMap[rawLocale.code] = rawLocale;
|
|
}
|
|
return {
|
|
map: rawLocaleMap,
|
|
defaultCode,
|
|
};
|
|
}
|
|
function buildLocale(inputSingular, available) {
|
|
if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
|
|
return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
|
|
}
|
|
return queryLocale(inputSingular, available);
|
|
}
|
|
function queryLocale(codeArg, available) {
|
|
let codes = [].concat(codeArg || []); // will convert to array
|
|
let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
|
|
return parseLocale(codeArg, codes, raw);
|
|
}
|
|
function queryRawLocale(codes, available) {
|
|
for (let i = 0; i < codes.length; i += 1) {
|
|
let parts = codes[i].toLocaleLowerCase().split('-');
|
|
for (let j = parts.length; j > 0; j -= 1) {
|
|
let simpleId = parts.slice(0, j).join('-');
|
|
if (available[simpleId]) {
|
|
return available[simpleId];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function parseLocale(codeArg, codes, raw) {
|
|
let merged = mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']);
|
|
delete merged.code; // don't want this part of the options
|
|
let { week } = merged;
|
|
delete merged.week;
|
|
return {
|
|
codeArg,
|
|
codes,
|
|
week,
|
|
simpleNumberFormat: new Intl.NumberFormat(codeArg),
|
|
options: merged,
|
|
};
|
|
}
|
|
|
|
// TODO: easier way to add new hooks? need to update a million things
|
|
function createPlugin(input) {
|
|
return {
|
|
id: guid(),
|
|
name: input.name,
|
|
premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
|
|
deps: input.deps || [],
|
|
reducers: input.reducers || [],
|
|
isLoadingFuncs: input.isLoadingFuncs || [],
|
|
contextInit: [].concat(input.contextInit || []),
|
|
eventRefiners: input.eventRefiners || {},
|
|
eventDefMemberAdders: input.eventDefMemberAdders || [],
|
|
eventSourceRefiners: input.eventSourceRefiners || {},
|
|
isDraggableTransformers: input.isDraggableTransformers || [],
|
|
eventDragMutationMassagers: input.eventDragMutationMassagers || [],
|
|
eventDefMutationAppliers: input.eventDefMutationAppliers || [],
|
|
dateSelectionTransformers: input.dateSelectionTransformers || [],
|
|
datePointTransforms: input.datePointTransforms || [],
|
|
dateSpanTransforms: input.dateSpanTransforms || [],
|
|
views: input.views || {},
|
|
viewPropsTransformers: input.viewPropsTransformers || [],
|
|
isPropsValid: input.isPropsValid || null,
|
|
externalDefTransforms: input.externalDefTransforms || [],
|
|
viewContainerAppends: input.viewContainerAppends || [],
|
|
eventDropTransformers: input.eventDropTransformers || [],
|
|
componentInteractions: input.componentInteractions || [],
|
|
calendarInteractions: input.calendarInteractions || [],
|
|
themeClasses: input.themeClasses || {},
|
|
eventSourceDefs: input.eventSourceDefs || [],
|
|
cmdFormatter: input.cmdFormatter,
|
|
recurringTypes: input.recurringTypes || [],
|
|
namedTimeZonedImpl: input.namedTimeZonedImpl,
|
|
initialView: input.initialView || '',
|
|
elementDraggingImpl: input.elementDraggingImpl,
|
|
optionChangeHandlers: input.optionChangeHandlers || {},
|
|
scrollGridImpl: input.scrollGridImpl || null,
|
|
listenerRefiners: input.listenerRefiners || {},
|
|
optionRefiners: input.optionRefiners || {},
|
|
propSetHandlers: input.propSetHandlers || {},
|
|
};
|
|
}
|
|
function buildPluginHooks(pluginDefs, globalDefs) {
|
|
let currentPluginIds = {};
|
|
let hooks = {
|
|
premiumReleaseDate: undefined,
|
|
reducers: [],
|
|
isLoadingFuncs: [],
|
|
contextInit: [],
|
|
eventRefiners: {},
|
|
eventDefMemberAdders: [],
|
|
eventSourceRefiners: {},
|
|
isDraggableTransformers: [],
|
|
eventDragMutationMassagers: [],
|
|
eventDefMutationAppliers: [],
|
|
dateSelectionTransformers: [],
|
|
datePointTransforms: [],
|
|
dateSpanTransforms: [],
|
|
views: {},
|
|
viewPropsTransformers: [],
|
|
isPropsValid: null,
|
|
externalDefTransforms: [],
|
|
viewContainerAppends: [],
|
|
eventDropTransformers: [],
|
|
componentInteractions: [],
|
|
calendarInteractions: [],
|
|
themeClasses: {},
|
|
eventSourceDefs: [],
|
|
cmdFormatter: null,
|
|
recurringTypes: [],
|
|
namedTimeZonedImpl: null,
|
|
initialView: '',
|
|
elementDraggingImpl: null,
|
|
optionChangeHandlers: {},
|
|
scrollGridImpl: null,
|
|
listenerRefiners: {},
|
|
optionRefiners: {},
|
|
propSetHandlers: {},
|
|
};
|
|
function addDefs(defs) {
|
|
for (let def of defs) {
|
|
const pluginName = def.name;
|
|
const currentId = currentPluginIds[pluginName];
|
|
if (currentId === undefined) {
|
|
currentPluginIds[pluginName] = def.id;
|
|
addDefs(def.deps);
|
|
hooks = combineHooks(hooks, def);
|
|
}
|
|
else if (currentId !== def.id) {
|
|
// different ID than the one already added
|
|
console.warn(`Duplicate plugin '${pluginName}'`);
|
|
}
|
|
}
|
|
}
|
|
if (pluginDefs) {
|
|
addDefs(pluginDefs);
|
|
}
|
|
addDefs(globalDefs);
|
|
return hooks;
|
|
}
|
|
function buildBuildPluginHooks() {
|
|
let currentOverrideDefs = [];
|
|
let currentGlobalDefs = [];
|
|
let currentHooks;
|
|
return (overrideDefs, globalDefs) => {
|
|
if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) {
|
|
currentHooks = buildPluginHooks(overrideDefs, globalDefs);
|
|
}
|
|
currentOverrideDefs = overrideDefs;
|
|
currentGlobalDefs = globalDefs;
|
|
return currentHooks;
|
|
};
|
|
}
|
|
function combineHooks(hooks0, hooks1) {
|
|
return {
|
|
premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate),
|
|
reducers: hooks0.reducers.concat(hooks1.reducers),
|
|
isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs),
|
|
contextInit: hooks0.contextInit.concat(hooks1.contextInit),
|
|
eventRefiners: Object.assign(Object.assign({}, hooks0.eventRefiners), hooks1.eventRefiners),
|
|
eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
|
|
eventSourceRefiners: Object.assign(Object.assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners),
|
|
isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
|
|
eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
|
|
eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
|
|
dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
|
|
datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
|
|
dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
|
|
views: Object.assign(Object.assign({}, hooks0.views), hooks1.views),
|
|
viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
|
|
isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
|
|
externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
|
|
viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
|
|
eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
|
|
calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
|
|
componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
|
|
themeClasses: Object.assign(Object.assign({}, hooks0.themeClasses), hooks1.themeClasses),
|
|
eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
|
|
cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
|
|
recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
|
|
namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
|
|
initialView: hooks0.initialView || hooks1.initialView,
|
|
elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
|
|
optionChangeHandlers: Object.assign(Object.assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers),
|
|
scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
|
|
listenerRefiners: Object.assign(Object.assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners),
|
|
optionRefiners: Object.assign(Object.assign({}, hooks0.optionRefiners), hooks1.optionRefiners),
|
|
propSetHandlers: Object.assign(Object.assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers),
|
|
};
|
|
}
|
|
function compareOptionalDates(date0, date1) {
|
|
if (date0 === undefined) {
|
|
return date1;
|
|
}
|
|
if (date1 === undefined) {
|
|
return date0;
|
|
}
|
|
return new Date(Math.max(date0.valueOf(), date1.valueOf()));
|
|
}
|
|
|
|
class StandardTheme extends Theme {
|
|
}
|
|
StandardTheme.prototype.classes = {
|
|
root: 'fc-theme-standard',
|
|
tableCellShaded: 'fc-cell-shaded',
|
|
buttonGroup: 'fc-button-group',
|
|
button: 'fc-button fc-button-primary',
|
|
buttonActive: 'fc-button-active',
|
|
};
|
|
StandardTheme.prototype.baseIconClass = 'fc-icon';
|
|
StandardTheme.prototype.iconClasses = {
|
|
close: 'fc-icon-x',
|
|
prev: 'fc-icon-chevron-left',
|
|
next: 'fc-icon-chevron-right',
|
|
prevYear: 'fc-icon-chevrons-left',
|
|
nextYear: 'fc-icon-chevrons-right',
|
|
};
|
|
StandardTheme.prototype.rtlIconClasses = {
|
|
prev: 'fc-icon-chevron-right',
|
|
next: 'fc-icon-chevron-left',
|
|
prevYear: 'fc-icon-chevrons-right',
|
|
nextYear: 'fc-icon-chevrons-left',
|
|
};
|
|
StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly
|
|
StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
|
|
StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
|
|
|
|
function compileViewDefs(defaultConfigs, overrideConfigs) {
|
|
let hash = {};
|
|
let viewType;
|
|
for (viewType in defaultConfigs) {
|
|
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
for (viewType in overrideConfigs) {
|
|
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
return hash;
|
|
}
|
|
function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
|
|
if (hash[viewType]) {
|
|
return hash[viewType];
|
|
}
|
|
let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
if (viewDef) {
|
|
hash[viewType] = viewDef;
|
|
}
|
|
return viewDef;
|
|
}
|
|
function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
|
|
let defaultConfig = defaultConfigs[viewType];
|
|
let overrideConfig = overrideConfigs[viewType];
|
|
let queryProp = (name) => ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
|
|
((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null));
|
|
let theComponent = queryProp('component');
|
|
let superType = queryProp('superType');
|
|
let superDef = null;
|
|
if (superType) {
|
|
if (superType === viewType) {
|
|
throw new Error('Can\'t have a custom view type that references itself');
|
|
}
|
|
superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
if (!theComponent && superDef) {
|
|
theComponent = superDef.component;
|
|
}
|
|
if (!theComponent) {
|
|
return null; // don't throw a warning, might be settings for a single-unit view
|
|
}
|
|
return {
|
|
type: viewType,
|
|
component: theComponent,
|
|
defaults: Object.assign(Object.assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})),
|
|
overrides: Object.assign(Object.assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})),
|
|
};
|
|
}
|
|
|
|
function parseViewConfigs(inputs) {
|
|
return mapHash(inputs, parseViewConfig);
|
|
}
|
|
function parseViewConfig(input) {
|
|
let rawOptions = typeof input === 'function' ?
|
|
{ component: input } :
|
|
input;
|
|
let { component } = rawOptions;
|
|
if (rawOptions.content) {
|
|
// TODO: remove content/classNames/didMount/etc from options?
|
|
component = createViewHookComponent(rawOptions);
|
|
}
|
|
else if (component && !(component.prototype instanceof BaseComponent)) {
|
|
// WHY?: people were using `component` property for `content`
|
|
// TODO: converge on one setting name
|
|
component = createViewHookComponent(Object.assign(Object.assign({}, rawOptions), { content: component }));
|
|
}
|
|
return {
|
|
superType: rawOptions.type,
|
|
component: component,
|
|
rawOptions, // includes type and component too :(
|
|
};
|
|
}
|
|
function createViewHookComponent(options) {
|
|
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 }))));
|
|
}
|
|
|
|
function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
|
|
let defaultConfigs = parseViewConfigs(defaultInputs);
|
|
let overrideConfigs = parseViewConfigs(optionOverrides.views);
|
|
let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
|
|
return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults));
|
|
}
|
|
function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
|
|
let durationInput = viewDef.overrides.duration ||
|
|
viewDef.defaults.duration ||
|
|
dynamicOptionOverrides.duration ||
|
|
optionOverrides.duration;
|
|
let duration = null;
|
|
let durationUnit = '';
|
|
let singleUnit = '';
|
|
let singleUnitOverrides = {};
|
|
if (durationInput) {
|
|
duration = createDurationCached(durationInput);
|
|
if (duration) { // valid?
|
|
let denom = greatestDurationDenominator(duration);
|
|
durationUnit = denom.unit;
|
|
if (denom.value === 1) {
|
|
singleUnit = durationUnit;
|
|
singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {};
|
|
}
|
|
}
|
|
}
|
|
let queryButtonText = (optionsSubset) => {
|
|
let buttonTextMap = optionsSubset.buttonText || {};
|
|
let buttonTextKey = viewDef.defaults.buttonTextKey;
|
|
if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
|
|
return buttonTextMap[buttonTextKey];
|
|
}
|
|
if (buttonTextMap[viewDef.type] != null) {
|
|
return buttonTextMap[viewDef.type];
|
|
}
|
|
if (buttonTextMap[singleUnit] != null) {
|
|
return buttonTextMap[singleUnit];
|
|
}
|
|
return null;
|
|
};
|
|
let queryButtonTitle = (optionsSubset) => {
|
|
let buttonHints = optionsSubset.buttonHints || {};
|
|
let buttonKey = viewDef.defaults.buttonTextKey; // use same key as text
|
|
if (buttonKey != null && buttonHints[buttonKey] != null) {
|
|
return buttonHints[buttonKey];
|
|
}
|
|
if (buttonHints[viewDef.type] != null) {
|
|
return buttonHints[viewDef.type];
|
|
}
|
|
if (buttonHints[singleUnit] != null) {
|
|
return buttonHints[singleUnit];
|
|
}
|
|
return null;
|
|
};
|
|
return {
|
|
type: viewDef.type,
|
|
component: viewDef.component,
|
|
duration,
|
|
durationUnit,
|
|
singleUnit,
|
|
optionDefaults: viewDef.defaults,
|
|
optionOverrides: Object.assign(Object.assign({}, singleUnitOverrides), viewDef.overrides),
|
|
buttonTextOverride: queryButtonText(dynamicOptionOverrides) ||
|
|
queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence
|
|
viewDef.overrides.buttonText,
|
|
buttonTextDefault: queryButtonText(localeDefaults) ||
|
|
viewDef.defaults.buttonText ||
|
|
queryButtonText(BASE_OPTION_DEFAULTS) ||
|
|
viewDef.type,
|
|
// not DRY
|
|
buttonTitleOverride: queryButtonTitle(dynamicOptionOverrides) ||
|
|
queryButtonTitle(optionOverrides) ||
|
|
viewDef.overrides.buttonHint,
|
|
buttonTitleDefault: queryButtonTitle(localeDefaults) ||
|
|
viewDef.defaults.buttonHint ||
|
|
queryButtonTitle(BASE_OPTION_DEFAULTS),
|
|
// will eventually fall back to buttonText
|
|
};
|
|
}
|
|
// hack to get memoization working
|
|
let durationInputMap = {};
|
|
function createDurationCached(durationInput) {
|
|
let json = JSON.stringify(durationInput);
|
|
let res = durationInputMap[json];
|
|
if (res === undefined) {
|
|
res = createDuration(durationInput);
|
|
durationInputMap[json] = res;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function reduceViewType(viewType, action) {
|
|
switch (action.type) {
|
|
case 'CHANGE_VIEW_TYPE':
|
|
viewType = action.viewType;
|
|
}
|
|
return viewType;
|
|
}
|
|
|
|
function reduceCurrentDate(currentDate, action) {
|
|
switch (action.type) {
|
|
case 'CHANGE_DATE':
|
|
return action.dateMarker;
|
|
default:
|
|
return currentDate;
|
|
}
|
|
}
|
|
// should be initialized once and stay constant
|
|
// this will change too
|
|
function getInitialDate(options, dateEnv, nowManager) {
|
|
let initialDateInput = options.initialDate;
|
|
// compute the initial ambig-timezone date
|
|
if (initialDateInput != null) {
|
|
return dateEnv.createMarker(initialDateInput);
|
|
}
|
|
return nowManager.getDateMarker();
|
|
}
|
|
|
|
function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) {
|
|
switch (action.type) {
|
|
case 'SET_OPTION':
|
|
return Object.assign(Object.assign({}, dynamicOptionOverrides), { [action.optionName]: action.rawOptionValue });
|
|
default:
|
|
return dynamicOptionOverrides;
|
|
}
|
|
}
|
|
|
|
function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) {
|
|
let dp;
|
|
switch (action.type) {
|
|
case 'CHANGE_VIEW_TYPE':
|
|
return dateProfileGenerator.build(action.dateMarker || currentDate);
|
|
case 'CHANGE_DATE':
|
|
return dateProfileGenerator.build(action.dateMarker);
|
|
case 'PREV':
|
|
dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate);
|
|
if (dp.isValid) {
|
|
return dp;
|
|
}
|
|
break;
|
|
case 'NEXT':
|
|
dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate);
|
|
if (dp.isValid) {
|
|
return dp;
|
|
}
|
|
break;
|
|
}
|
|
return currentDateProfile;
|
|
}
|
|
|
|
function initEventSources(calendarOptions, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null;
|
|
return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context);
|
|
}
|
|
function reduceEventSources(eventSources, action, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
|
|
switch (action.type) {
|
|
case 'ADD_EVENT_SOURCES': // already parsed
|
|
return addSources(eventSources, action.sources, activeRange, context);
|
|
case 'REMOVE_EVENT_SOURCE':
|
|
return removeSource(eventSources, action.sourceId);
|
|
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
|
|
case 'NEXT':
|
|
case 'CHANGE_DATE':
|
|
case 'CHANGE_VIEW_TYPE':
|
|
if (dateProfile) {
|
|
return fetchDirtySources(eventSources, activeRange, context);
|
|
}
|
|
return eventSources;
|
|
case 'FETCH_EVENT_SOURCES':
|
|
return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type?
|
|
arrayToHash(action.sourceIds) :
|
|
excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context);
|
|
case 'RECEIVE_EVENTS':
|
|
case 'RECEIVE_EVENT_ERROR':
|
|
return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
|
|
case 'REMOVE_ALL_EVENT_SOURCES':
|
|
return {};
|
|
default:
|
|
return eventSources;
|
|
}
|
|
}
|
|
function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
|
|
return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context);
|
|
}
|
|
function computeEventSourcesLoading(eventSources) {
|
|
for (let sourceId in eventSources) {
|
|
if (eventSources[sourceId].isFetching) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function addSources(eventSourceHash, sources, fetchRange, context) {
|
|
let hash = {};
|
|
for (let source of sources) {
|
|
hash[source.sourceId] = source;
|
|
}
|
|
if (fetchRange) {
|
|
hash = fetchDirtySources(hash, fetchRange, context);
|
|
}
|
|
return Object.assign(Object.assign({}, eventSourceHash), hash);
|
|
}
|
|
function removeSource(eventSourceHash, sourceId) {
|
|
return filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId);
|
|
}
|
|
function fetchDirtySources(sourceHash, fetchRange, context) {
|
|
return fetchSourcesByIds(sourceHash, filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), fetchRange, false, context);
|
|
}
|
|
function isSourceDirty(eventSource, fetchRange, context) {
|
|
if (!doesSourceNeedRange(eventSource, context)) {
|
|
return !eventSource.latestFetchId;
|
|
}
|
|
return !context.options.lazyFetching ||
|
|
!eventSource.fetchRange ||
|
|
eventSource.isFetching || // always cancel outdated in-progress fetches
|
|
fetchRange.start < eventSource.fetchRange.start ||
|
|
fetchRange.end > eventSource.fetchRange.end;
|
|
}
|
|
function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) {
|
|
let nextSources = {};
|
|
for (let sourceId in prevSources) {
|
|
let source = prevSources[sourceId];
|
|
if (sourceIdHash[sourceId]) {
|
|
nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context);
|
|
}
|
|
else {
|
|
nextSources[sourceId] = source;
|
|
}
|
|
}
|
|
return nextSources;
|
|
}
|
|
function fetchSource(eventSource, fetchRange, isRefetch, context) {
|
|
let { options, calendarApi } = context;
|
|
let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId];
|
|
let fetchId = guid();
|
|
sourceDef.fetch({
|
|
eventSource,
|
|
range: fetchRange,
|
|
isRefetch,
|
|
context,
|
|
}, (res) => {
|
|
let { rawEvents } = res;
|
|
if (options.eventSourceSuccess) {
|
|
rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents;
|
|
}
|
|
if (eventSource.success) {
|
|
rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents;
|
|
}
|
|
context.dispatch({
|
|
type: 'RECEIVE_EVENTS',
|
|
sourceId: eventSource.sourceId,
|
|
fetchId,
|
|
fetchRange,
|
|
rawEvents,
|
|
});
|
|
}, (error) => {
|
|
let errorHandled = false;
|
|
if (options.eventSourceFailure) {
|
|
options.eventSourceFailure.call(calendarApi, error);
|
|
errorHandled = true;
|
|
}
|
|
if (eventSource.failure) {
|
|
eventSource.failure(error);
|
|
errorHandled = true;
|
|
}
|
|
if (!errorHandled) {
|
|
console.warn(error.message, error);
|
|
}
|
|
context.dispatch({
|
|
type: 'RECEIVE_EVENT_ERROR',
|
|
sourceId: eventSource.sourceId,
|
|
fetchId,
|
|
fetchRange,
|
|
error,
|
|
});
|
|
});
|
|
return Object.assign(Object.assign({}, eventSource), { isFetching: true, latestFetchId: fetchId });
|
|
}
|
|
function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
|
|
let eventSource = sourceHash[sourceId];
|
|
if (eventSource && // not already removed
|
|
fetchId === eventSource.latestFetchId) {
|
|
return Object.assign(Object.assign({}, sourceHash), { [sourceId]: Object.assign(Object.assign({}, eventSource), { isFetching: false, fetchRange }) });
|
|
}
|
|
return sourceHash;
|
|
}
|
|
function excludeStaticSources(eventSources, context) {
|
|
return filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context));
|
|
}
|
|
function parseInitialSources(rawOptions, context) {
|
|
let refiners = buildEventSourceRefiners(context);
|
|
let rawSources = [].concat(rawOptions.eventSources || []);
|
|
let sources = []; // parsed
|
|
if (rawOptions.initialEvents) {
|
|
rawSources.unshift(rawOptions.initialEvents);
|
|
}
|
|
if (rawOptions.events) {
|
|
rawSources.unshift(rawOptions.events);
|
|
}
|
|
for (let rawSource of rawSources) {
|
|
let source = parseEventSource(rawSource, context, refiners);
|
|
if (source) {
|
|
sources.push(source);
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
function doesSourceNeedRange(eventSource, context) {
|
|
let defs = context.pluginHooks.eventSourceDefs;
|
|
return !defs[eventSource.sourceDefId].ignoreRange;
|
|
}
|
|
|
|
function reduceDateSelection(currentSelection, action) {
|
|
switch (action.type) {
|
|
case 'UNSELECT_DATES':
|
|
return null;
|
|
case 'SELECT_DATES':
|
|
return action.selection;
|
|
default:
|
|
return currentSelection;
|
|
}
|
|
}
|
|
|
|
function reduceSelectedEvent(currentInstanceId, action) {
|
|
switch (action.type) {
|
|
case 'UNSELECT_EVENT':
|
|
return '';
|
|
case 'SELECT_EVENT':
|
|
return action.eventInstanceId;
|
|
default:
|
|
return currentInstanceId;
|
|
}
|
|
}
|
|
|
|
function reduceEventDrag(currentDrag, action) {
|
|
let newDrag;
|
|
switch (action.type) {
|
|
case 'UNSET_EVENT_DRAG':
|
|
return null;
|
|
case 'SET_EVENT_DRAG':
|
|
newDrag = action.state;
|
|
return {
|
|
affectedEvents: newDrag.affectedEvents,
|
|
mutatedEvents: newDrag.mutatedEvents,
|
|
isEvent: newDrag.isEvent,
|
|
};
|
|
default:
|
|
return currentDrag;
|
|
}
|
|
}
|
|
|
|
function reduceEventResize(currentResize, action) {
|
|
let newResize;
|
|
switch (action.type) {
|
|
case 'UNSET_EVENT_RESIZE':
|
|
return null;
|
|
case 'SET_EVENT_RESIZE':
|
|
newResize = action.state;
|
|
return {
|
|
affectedEvents: newResize.affectedEvents,
|
|
mutatedEvents: newResize.mutatedEvents,
|
|
isEvent: newResize.isEvent,
|
|
};
|
|
default:
|
|
return currentResize;
|
|
}
|
|
}
|
|
|
|
function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
|
|
let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
|
|
let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
|
|
return { header, footer };
|
|
}
|
|
function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
|
|
let sectionWidgets = {};
|
|
let viewsWithButtons = [];
|
|
let hasTitle = false;
|
|
for (let sectionName in sectionStrHash) {
|
|
let sectionStr = sectionStrHash[sectionName];
|
|
let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi);
|
|
sectionWidgets[sectionName] = sectionRes.widgets;
|
|
viewsWithButtons.push(...sectionRes.viewsWithButtons);
|
|
hasTitle = hasTitle || sectionRes.hasTitle;
|
|
}
|
|
return { sectionWidgets, viewsWithButtons, hasTitle };
|
|
}
|
|
/*
|
|
BAD: querying icons and text here. should be done at render time
|
|
*/
|
|
function parseSection(sectionStr, calendarOptions, // defaults+overrides, then refined
|
|
calendarOptionOverrides, // overrides only!, unrefined :(
|
|
theme, viewSpecs, calendarApi) {
|
|
let isRtl = calendarOptions.direction === 'rtl';
|
|
let calendarCustomButtons = calendarOptions.customButtons || {};
|
|
let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {};
|
|
let calendarButtonText = calendarOptions.buttonText || {};
|
|
let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {};
|
|
let calendarButtonHints = calendarOptions.buttonHints || {};
|
|
let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
|
|
let viewsWithButtons = [];
|
|
let hasTitle = false;
|
|
let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((buttonName) => {
|
|
if (buttonName === 'title') {
|
|
hasTitle = true;
|
|
return { buttonName };
|
|
}
|
|
let customButtonProps;
|
|
let viewSpec;
|
|
let buttonClick;
|
|
let buttonIcon; // only one of these will be set
|
|
let buttonText; // "
|
|
let buttonHint;
|
|
// ^ for the title="" attribute, for accessibility
|
|
if ((customButtonProps = calendarCustomButtons[buttonName])) {
|
|
buttonClick = (ev) => {
|
|
if (customButtonProps.click) {
|
|
customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context?
|
|
}
|
|
};
|
|
(buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = customButtonProps.text);
|
|
buttonHint = customButtonProps.hint || customButtonProps.text;
|
|
}
|
|
else if ((viewSpec = viewSpecs[buttonName])) {
|
|
viewsWithButtons.push(buttonName);
|
|
buttonClick = () => {
|
|
calendarApi.changeView(buttonName);
|
|
};
|
|
(buttonText = viewSpec.buttonTextOverride) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = viewSpec.buttonTextDefault);
|
|
let textFallback = viewSpec.buttonTextOverride ||
|
|
viewSpec.buttonTextDefault;
|
|
buttonHint = formatWithOrdinals(viewSpec.buttonTitleOverride ||
|
|
viewSpec.buttonTitleDefault ||
|
|
calendarOptions.viewHint, [textFallback, buttonName], // view-name = buttonName
|
|
textFallback);
|
|
}
|
|
else if (calendarApi[buttonName]) { // a calendarApi method
|
|
buttonClick = () => {
|
|
calendarApi[buttonName]();
|
|
};
|
|
(buttonText = calendarButtonTextOverrides[buttonName]) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = calendarButtonText[buttonName]); // everything else is considered default
|
|
if (buttonName === 'prevYear' || buttonName === 'nextYear') {
|
|
let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next';
|
|
buttonHint = formatWithOrdinals(calendarButtonHintOverrides[prevOrNext] ||
|
|
calendarButtonHints[prevOrNext], [
|
|
calendarButtonText.year || 'year',
|
|
'year',
|
|
], calendarButtonText[buttonName]);
|
|
}
|
|
else {
|
|
buttonHint = (navUnit) => formatWithOrdinals(calendarButtonHintOverrides[buttonName] ||
|
|
calendarButtonHints[buttonName], [
|
|
calendarButtonText[navUnit] || navUnit,
|
|
navUnit,
|
|
], calendarButtonText[buttonName]);
|
|
}
|
|
}
|
|
return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint };
|
|
})));
|
|
return { widgets, viewsWithButtons, hasTitle };
|
|
}
|
|
|
|
// always represents the current view. otherwise, it'd need to change value every time date changes
|
|
class ViewImpl {
|
|
constructor(type, getCurrentData, dateEnv) {
|
|
this.type = type;
|
|
this.getCurrentData = getCurrentData;
|
|
this.dateEnv = dateEnv;
|
|
}
|
|
get calendar() {
|
|
return this.getCurrentData().calendarApi;
|
|
}
|
|
get title() {
|
|
return this.getCurrentData().viewTitle;
|
|
}
|
|
get activeStart() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start);
|
|
}
|
|
get activeEnd() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end);
|
|
}
|
|
get currentStart() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start);
|
|
}
|
|
get currentEnd() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end);
|
|
}
|
|
getOption(name) {
|
|
return this.getCurrentData().options[name]; // are the view-specific options
|
|
}
|
|
}
|
|
|
|
let eventSourceDef$2 = {
|
|
ignoreRange: true,
|
|
parseMeta(refined) {
|
|
if (Array.isArray(refined.events)) {
|
|
return refined.events;
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback) {
|
|
successCallback({
|
|
rawEvents: arg.eventSource.meta,
|
|
});
|
|
},
|
|
};
|
|
const arrayEventSourcePlugin = createPlugin({
|
|
name: 'array-event-source',
|
|
eventSourceDefs: [eventSourceDef$2],
|
|
});
|
|
|
|
let eventSourceDef$1 = {
|
|
parseMeta(refined) {
|
|
if (typeof refined.events === 'function') {
|
|
return refined.events;
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback, errorCallback) {
|
|
const { dateEnv } = arg.context;
|
|
const func = arg.eventSource.meta;
|
|
unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback);
|
|
},
|
|
};
|
|
const funcEventSourcePlugin = createPlugin({
|
|
name: 'func-event-source',
|
|
eventSourceDefs: [eventSourceDef$1],
|
|
});
|
|
|
|
const JSON_FEED_EVENT_SOURCE_REFINERS = {
|
|
method: String,
|
|
extraParams: identity,
|
|
startParam: String,
|
|
endParam: String,
|
|
timeZoneParam: String,
|
|
};
|
|
|
|
let eventSourceDef = {
|
|
parseMeta(refined) {
|
|
if (refined.url && (refined.format === 'json' || !refined.format)) {
|
|
return {
|
|
url: refined.url,
|
|
format: 'json',
|
|
method: (refined.method || 'GET').toUpperCase(),
|
|
extraParams: refined.extraParams,
|
|
startParam: refined.startParam,
|
|
endParam: refined.endParam,
|
|
timeZoneParam: refined.timeZoneParam,
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback, errorCallback) {
|
|
const { meta } = arg.eventSource;
|
|
const requestParams = buildRequestParams(meta, arg.range, arg.context);
|
|
requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => {
|
|
successCallback({ rawEvents, response });
|
|
}, errorCallback);
|
|
},
|
|
};
|
|
const jsonFeedEventSourcePlugin = createPlugin({
|
|
name: 'json-event-source',
|
|
eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS,
|
|
eventSourceDefs: [eventSourceDef],
|
|
});
|
|
function buildRequestParams(meta, range, context) {
|
|
let { dateEnv, options } = context;
|
|
let startParam;
|
|
let endParam;
|
|
let timeZoneParam;
|
|
let customRequestParams;
|
|
let params = {};
|
|
startParam = meta.startParam;
|
|
if (startParam == null) {
|
|
startParam = options.startParam;
|
|
}
|
|
endParam = meta.endParam;
|
|
if (endParam == null) {
|
|
endParam = options.endParam;
|
|
}
|
|
timeZoneParam = meta.timeZoneParam;
|
|
if (timeZoneParam == null) {
|
|
timeZoneParam = options.timeZoneParam;
|
|
}
|
|
// retrieve any outbound GET/POST data from the options
|
|
if (typeof meta.extraParams === 'function') {
|
|
// supplied as a function that returns a key/value object
|
|
customRequestParams = meta.extraParams();
|
|
}
|
|
else {
|
|
// probably supplied as a straight key/value object
|
|
customRequestParams = meta.extraParams || {};
|
|
}
|
|
Object.assign(params, customRequestParams);
|
|
params[startParam] = dateEnv.formatIso(range.start);
|
|
params[endParam] = dateEnv.formatIso(range.end);
|
|
if (dateEnv.timeZone !== 'local') {
|
|
params[timeZoneParam] = dateEnv.timeZone;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
const SIMPLE_RECURRING_REFINERS = {
|
|
daysOfWeek: identity,
|
|
startTime: createDuration,
|
|
endTime: createDuration,
|
|
duration: createDuration,
|
|
startRecur: identity,
|
|
endRecur: identity,
|
|
};
|
|
|
|
let recurring = {
|
|
parse(refined, dateEnv) {
|
|
if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) {
|
|
let recurringData = {
|
|
daysOfWeek: refined.daysOfWeek || null,
|
|
startTime: refined.startTime || null,
|
|
endTime: refined.endTime || null,
|
|
startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null,
|
|
endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null,
|
|
dateEnv,
|
|
};
|
|
let duration;
|
|
if (refined.duration) {
|
|
duration = refined.duration;
|
|
}
|
|
if (!duration && refined.startTime && refined.endTime) {
|
|
duration = subtractDurations(refined.endTime, refined.startTime);
|
|
}
|
|
return {
|
|
allDayGuess: Boolean(!refined.startTime && !refined.endTime),
|
|
duration,
|
|
typeData: recurringData, // doesn't need endTime anymore but oh well
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
expand(typeData, framingRange, dateEnv) {
|
|
let clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
|
|
if (clippedFramingRange) {
|
|
return expandRanges(typeData.daysOfWeek, typeData.startTime, typeData.dateEnv, dateEnv, clippedFramingRange);
|
|
}
|
|
return [];
|
|
},
|
|
};
|
|
const simpleRecurringEventsPlugin = createPlugin({
|
|
name: 'simple-recurring-event',
|
|
recurringTypes: [recurring],
|
|
eventRefiners: SIMPLE_RECURRING_REFINERS,
|
|
});
|
|
function expandRanges(daysOfWeek, startTime, eventDateEnv, calendarDateEnv, framingRange) {
|
|
let dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null;
|
|
let dayMarker = startOfDay(framingRange.start);
|
|
let endMarker = framingRange.end;
|
|
let instanceStarts = [];
|
|
while (dayMarker < endMarker) {
|
|
let instanceStart;
|
|
// if everyday, or this particular day-of-week
|
|
if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
|
|
if (startTime) {
|
|
instanceStart = calendarDateEnv.add(dayMarker, startTime);
|
|
}
|
|
else {
|
|
instanceStart = dayMarker;
|
|
}
|
|
instanceStarts.push(calendarDateEnv.createMarker(eventDateEnv.toDate(instanceStart)));
|
|
}
|
|
dayMarker = addDays(dayMarker, 1);
|
|
}
|
|
return instanceStarts;
|
|
}
|
|
|
|
const changeHandlerPlugin = createPlugin({
|
|
name: 'change-handler',
|
|
optionChangeHandlers: {
|
|
events(events, context) {
|
|
handleEventSources([events], context);
|
|
},
|
|
eventSources: handleEventSources,
|
|
},
|
|
});
|
|
/*
|
|
BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out
|
|
*/
|
|
function handleEventSources(inputs, context) {
|
|
let unfoundSources = hashValuesToArray(context.getCurrentData().eventSources);
|
|
if (unfoundSources.length === 1 &&
|
|
inputs.length === 1 &&
|
|
Array.isArray(unfoundSources[0]._raw) &&
|
|
Array.isArray(inputs[0])) {
|
|
context.dispatch({
|
|
type: 'RESET_RAW_EVENTS',
|
|
sourceId: unfoundSources[0].sourceId,
|
|
rawEvents: inputs[0],
|
|
});
|
|
return;
|
|
}
|
|
let newInputs = [];
|
|
for (let input of inputs) {
|
|
let inputFound = false;
|
|
for (let i = 0; i < unfoundSources.length; i += 1) {
|
|
if (unfoundSources[i]._raw === input) {
|
|
unfoundSources.splice(i, 1); // delete
|
|
inputFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inputFound) {
|
|
newInputs.push(input);
|
|
}
|
|
}
|
|
for (let unfoundSource of unfoundSources) {
|
|
context.dispatch({
|
|
type: 'REMOVE_EVENT_SOURCE',
|
|
sourceId: unfoundSource.sourceId,
|
|
});
|
|
}
|
|
for (let newInput of newInputs) {
|
|
context.calendarApi.addEventSource(newInput);
|
|
}
|
|
}
|
|
|
|
function handleDateProfile(dateProfile, context) {
|
|
context.emitter.trigger('datesSet', Object.assign(Object.assign({}, buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi }));
|
|
}
|
|
|
|
function handleEventStore(eventStore, context) {
|
|
let { emitter } = context;
|
|
if (emitter.hasHandlers('eventsSet')) {
|
|
emitter.trigger('eventsSet', buildEventApis(eventStore, context));
|
|
}
|
|
}
|
|
|
|
/*
|
|
this array is exposed on the root namespace so that UMD plugins can add to it.
|
|
see the rollup-bundles script.
|
|
*/
|
|
const globalPlugins = [
|
|
arrayEventSourcePlugin,
|
|
funcEventSourcePlugin,
|
|
jsonFeedEventSourcePlugin,
|
|
simpleRecurringEventsPlugin,
|
|
changeHandlerPlugin,
|
|
createPlugin({
|
|
name: 'misc',
|
|
isLoadingFuncs: [
|
|
(state) => computeEventSourcesLoading(state.eventSources),
|
|
],
|
|
propSetHandlers: {
|
|
dateProfile: handleDateProfile,
|
|
eventStore: handleEventStore,
|
|
},
|
|
}),
|
|
];
|
|
|
|
class TaskRunner {
|
|
constructor(runTaskOption, drainedOption) {
|
|
this.runTaskOption = runTaskOption;
|
|
this.drainedOption = drainedOption;
|
|
this.queue = [];
|
|
this.delayedRunner = new DelayedRunner(this.drain.bind(this));
|
|
}
|
|
request(task, delay) {
|
|
this.queue.push(task);
|
|
this.delayedRunner.request(delay);
|
|
}
|
|
pause(scope) {
|
|
this.delayedRunner.pause(scope);
|
|
}
|
|
resume(scope, force) {
|
|
this.delayedRunner.resume(scope, force);
|
|
}
|
|
drain() {
|
|
let { queue } = this;
|
|
while (queue.length) {
|
|
let completedTasks = [];
|
|
let task;
|
|
while ((task = queue.shift())) {
|
|
this.runTask(task);
|
|
completedTasks.push(task);
|
|
}
|
|
this.drained(completedTasks);
|
|
} // keep going, in case new tasks were added in the drained handler
|
|
}
|
|
runTask(task) {
|
|
if (this.runTaskOption) {
|
|
this.runTaskOption(task);
|
|
}
|
|
}
|
|
drained(completedTasks) {
|
|
if (this.drainedOption) {
|
|
this.drainedOption(completedTasks);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computes what the title at the top of the calendarApi should be for this view
|
|
function buildTitle(dateProfile, viewOptions, dateEnv) {
|
|
let range;
|
|
// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
|
|
if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
|
|
range = dateProfile.currentRange;
|
|
}
|
|
else { // for day units or smaller, use the actual day range
|
|
range = dateProfile.activeRange;
|
|
}
|
|
return dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), {
|
|
isEndExclusive: dateProfile.isRangeAllDay,
|
|
defaultSeparator: viewOptions.titleRangeSeparator,
|
|
});
|
|
}
|
|
// Generates the format string that should be used to generate the title for the current date range.
|
|
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
|
|
function buildTitleFormat(dateProfile) {
|
|
let { currentRangeUnit } = dateProfile;
|
|
if (currentRangeUnit === 'year') {
|
|
return { year: 'numeric' };
|
|
}
|
|
if (currentRangeUnit === 'month') {
|
|
return { year: 'numeric', month: 'long' }; // like "September 2014"
|
|
}
|
|
let days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
|
|
if (days !== null && days > 1) {
|
|
// multi-day range. shorter, like "Sep 9 - 10 2014"
|
|
return { year: 'numeric', month: 'short', day: 'numeric' };
|
|
}
|
|
// one day. longer, like "September 9 2014"
|
|
return { year: 'numeric', month: 'long', day: 'numeric' };
|
|
}
|
|
|
|
/*
|
|
TODO: test switching timezones when NO timezone plugin
|
|
*/
|
|
class CalendarNowManager {
|
|
constructor() {
|
|
this.resetListeners = new Set();
|
|
}
|
|
handleInput(dateEnv, // will change if timezone setup changed
|
|
nowInput) {
|
|
const oldDateEnv = this.dateEnv;
|
|
if (dateEnv !== oldDateEnv) {
|
|
if (typeof nowInput === 'function') {
|
|
this.nowFn = nowInput;
|
|
}
|
|
else if (!oldDateEnv) { // first time?
|
|
this.nowAnchorDate = dateEnv.toDate(nowInput
|
|
? dateEnv.createMarker(nowInput)
|
|
: dateEnv.createNowMarker());
|
|
this.nowAnchorQueried = Date.now();
|
|
}
|
|
this.dateEnv = dateEnv;
|
|
// not first time? fire reset handlers
|
|
if (oldDateEnv) {
|
|
for (const resetListener of this.resetListeners.values()) {
|
|
resetListener();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
getDateMarker() {
|
|
return this.nowAnchorDate
|
|
? this.dateEnv.timestampToMarker(this.nowAnchorDate.valueOf() +
|
|
(Date.now() - this.nowAnchorQueried))
|
|
: this.dateEnv.createMarker(this.nowFn());
|
|
}
|
|
addResetListener(handler) {
|
|
this.resetListeners.add(handler);
|
|
}
|
|
removeResetListener(handler) {
|
|
this.resetListeners.delete(handler);
|
|
}
|
|
}
|
|
|
|
// in future refactor, do the redux-style function(state=initial) for initial-state
|
|
// also, whatever is happening in constructor, have it happen in action queue too
|
|
class CalendarDataManager {
|
|
constructor(props) {
|
|
this.computeCurrentViewData = memoize(this._computeCurrentViewData);
|
|
this.organizeRawLocales = memoize(organizeRawLocales);
|
|
this.buildLocale = memoize(buildLocale);
|
|
this.buildPluginHooks = buildBuildPluginHooks();
|
|
this.buildDateEnv = memoize(buildDateEnv$1);
|
|
this.buildTheme = memoize(buildTheme);
|
|
this.parseToolbars = memoize(parseToolbars);
|
|
this.buildViewSpecs = memoize(buildViewSpecs);
|
|
this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator);
|
|
this.buildViewApi = memoize(buildViewApi);
|
|
this.buildViewUiProps = memoizeObjArg(buildViewUiProps);
|
|
this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual);
|
|
this.buildEventUiBases = memoize(buildEventUiBases);
|
|
this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours);
|
|
this.buildTitle = memoize(buildTitle);
|
|
this.nowManager = new CalendarNowManager();
|
|
this.emitter = new Emitter();
|
|
this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this));
|
|
this.currentCalendarOptionsInput = {};
|
|
this.currentCalendarOptionsRefined = {};
|
|
this.currentViewOptionsInput = {};
|
|
this.currentViewOptionsRefined = {};
|
|
this.currentCalendarOptionsRefiners = {};
|
|
this.optionsForRefining = [];
|
|
this.optionsForHandling = [];
|
|
this.getCurrentData = () => this.data;
|
|
this.dispatch = (action) => {
|
|
this.actionRunner.request(action); // protects against recursive calls to _handleAction
|
|
};
|
|
this.props = props;
|
|
this.actionRunner.pause();
|
|
this.nowManager = new CalendarNowManager();
|
|
let dynamicOptionOverrides = {};
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView;
|
|
let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
|
|
// wire things up
|
|
// TODO: not DRY
|
|
props.calendarApi.currentDataManager = this;
|
|
this.emitter.setThisContext(props.calendarApi);
|
|
this.emitter.setOptions(currentViewData.options);
|
|
let calendarContext = {
|
|
nowManager: this.nowManager,
|
|
dateEnv: optionsData.dateEnv,
|
|
options: optionsData.calendarOptions,
|
|
pluginHooks: optionsData.pluginHooks,
|
|
calendarApi: props.calendarApi,
|
|
dispatch: this.dispatch,
|
|
emitter: this.emitter,
|
|
getCurrentData: this.getCurrentData,
|
|
};
|
|
let currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv, this.nowManager);
|
|
let dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
|
|
if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) {
|
|
currentDate = dateProfile.currentRange.start;
|
|
}
|
|
// needs to be after setThisContext
|
|
for (let callback of optionsData.pluginHooks.contextInit) {
|
|
callback(calendarContext);
|
|
}
|
|
// NOT DRY
|
|
let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext);
|
|
let initialState = {
|
|
dynamicOptionOverrides,
|
|
currentViewType,
|
|
currentDate,
|
|
dateProfile,
|
|
businessHours: this.parseContextBusinessHours(calendarContext),
|
|
eventSources,
|
|
eventUiBases: {},
|
|
eventStore: createEmptyEventStore(),
|
|
renderableEventStore: createEmptyEventStore(),
|
|
dateSelection: null,
|
|
eventSelection: '',
|
|
eventDrag: null,
|
|
eventResize: null,
|
|
selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig,
|
|
};
|
|
let contextAndState = Object.assign(Object.assign({}, calendarContext), initialState);
|
|
for (let reducer of optionsData.pluginHooks.reducers) {
|
|
Object.assign(initialState, reducer(null, null, contextAndState));
|
|
}
|
|
if (computeIsLoading(initialState, calendarContext)) {
|
|
this.emitter.trigger('loading', true); // NOT DRY
|
|
}
|
|
this.state = initialState;
|
|
this.updateData();
|
|
this.actionRunner.resume();
|
|
}
|
|
resetOptions(optionOverrides, changedOptionNames) {
|
|
let { props } = this;
|
|
if (changedOptionNames === undefined) {
|
|
props.optionOverrides = optionOverrides;
|
|
}
|
|
else {
|
|
props.optionOverrides = Object.assign(Object.assign({}, (props.optionOverrides || {})), optionOverrides);
|
|
this.optionsForRefining.push(...changedOptionNames);
|
|
}
|
|
if (changedOptionNames === undefined || changedOptionNames.length) {
|
|
this.actionRunner.request({
|
|
type: 'NOTHING',
|
|
});
|
|
}
|
|
}
|
|
_handleAction(action) {
|
|
let { props, state, emitter } = this;
|
|
let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action);
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewType = reduceViewType(state.currentViewType, action);
|
|
let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
|
|
// wire things up
|
|
// TODO: not DRY
|
|
props.calendarApi.currentDataManager = this;
|
|
emitter.setThisContext(props.calendarApi);
|
|
emitter.setOptions(currentViewData.options);
|
|
let calendarContext = {
|
|
nowManager: this.nowManager,
|
|
dateEnv: optionsData.dateEnv,
|
|
options: optionsData.calendarOptions,
|
|
pluginHooks: optionsData.pluginHooks,
|
|
calendarApi: props.calendarApi,
|
|
dispatch: this.dispatch,
|
|
emitter,
|
|
getCurrentData: this.getCurrentData,
|
|
};
|
|
let { currentDate, dateProfile } = state;
|
|
if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack
|
|
dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
|
|
}
|
|
currentDate = reduceCurrentDate(currentDate, action);
|
|
dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator);
|
|
if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator
|
|
action.type === 'NEXT' || // "
|
|
!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
|
|
currentDate = dateProfile.currentRange.start;
|
|
}
|
|
let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext);
|
|
let eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext);
|
|
let isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading
|
|
let renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ?
|
|
(state.renderableEventStore || eventStore) : // try from previous state
|
|
eventStore;
|
|
let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext); // will memoize obj
|
|
let eventUiBySource = this.buildEventUiBySource(eventSources);
|
|
let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
|
|
let newState = {
|
|
dynamicOptionOverrides,
|
|
currentViewType,
|
|
currentDate,
|
|
dateProfile,
|
|
eventSources,
|
|
eventStore,
|
|
renderableEventStore,
|
|
selectionConfig,
|
|
eventUiBases,
|
|
businessHours: this.parseContextBusinessHours(calendarContext),
|
|
dateSelection: reduceDateSelection(state.dateSelection, action),
|
|
eventSelection: reduceSelectedEvent(state.eventSelection, action),
|
|
eventDrag: reduceEventDrag(state.eventDrag, action),
|
|
eventResize: reduceEventResize(state.eventResize, action),
|
|
};
|
|
let contextAndState = Object.assign(Object.assign({}, calendarContext), newState);
|
|
for (let reducer of optionsData.pluginHooks.reducers) {
|
|
Object.assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value
|
|
}
|
|
let wasLoading = computeIsLoading(state, calendarContext);
|
|
let isLoading = computeIsLoading(newState, calendarContext);
|
|
// TODO: use propSetHandlers in plugin system
|
|
if (!wasLoading && isLoading) {
|
|
emitter.trigger('loading', true);
|
|
}
|
|
else if (wasLoading && !isLoading) {
|
|
emitter.trigger('loading', false);
|
|
}
|
|
this.state = newState;
|
|
if (props.onAction) {
|
|
props.onAction(action);
|
|
}
|
|
}
|
|
updateData() {
|
|
let { props, state } = this;
|
|
let oldData = this.data;
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides);
|
|
let data = this.data = Object.assign(Object.assign(Object.assign({ nowManager: this.nowManager, viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state);
|
|
let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
|
|
let oldCalendarOptions = oldData && oldData.calendarOptions;
|
|
let newCalendarOptions = optionsData.calendarOptions;
|
|
if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) {
|
|
if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
|
|
// hack
|
|
state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data);
|
|
state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv);
|
|
state.renderableEventStore = data.renderableEventStore = rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv);
|
|
}
|
|
for (let optionName in changeHandlers) {
|
|
if (this.optionsForHandling.indexOf(optionName) !== -1 ||
|
|
oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
|
|
changeHandlers[optionName](newCalendarOptions[optionName], data);
|
|
}
|
|
}
|
|
}
|
|
this.optionsForHandling = [];
|
|
if (props.onData) {
|
|
props.onData(data);
|
|
}
|
|
}
|
|
computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi) {
|
|
// TODO: blacklist options that are handled by optionChangeHandlers
|
|
if (!this.optionsForRefining.length &&
|
|
optionOverrides === this.stableOptionOverrides &&
|
|
dynamicOptionOverrides === this.stableDynamicOptionOverrides) {
|
|
return this.stableCalendarOptionsData;
|
|
}
|
|
let { refinedOptions, pluginHooks, localeDefaults, availableLocaleData, extra, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
|
|
warnUnknownOptions(extra);
|
|
let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator);
|
|
let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults);
|
|
let theme = this.buildTheme(refinedOptions, pluginHooks);
|
|
let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi);
|
|
return this.stableCalendarOptionsData = {
|
|
calendarOptions: refinedOptions,
|
|
pluginHooks,
|
|
dateEnv,
|
|
viewSpecs,
|
|
theme,
|
|
toolbarConfig,
|
|
localeDefaults,
|
|
availableRawLocales: availableLocaleData.map,
|
|
};
|
|
}
|
|
// always called from behind a memoizer
|
|
processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
|
|
let { locales, locale } = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
let availableLocaleData = this.organizeRawLocales(locales);
|
|
let availableRawLocales = availableLocaleData.map;
|
|
let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options;
|
|
let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins);
|
|
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);
|
|
let extra = {};
|
|
let raw = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
localeDefaults,
|
|
optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
let refined = {};
|
|
let currentRaw = this.currentCalendarOptionsInput;
|
|
let currentRefined = this.currentCalendarOptionsRefined;
|
|
let anyChanges = false;
|
|
for (let optionName in raw) {
|
|
if (this.optionsForRefining.indexOf(optionName) === -1 && (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
(optionName in currentRaw) &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])))) {
|
|
refined[optionName] = currentRefined[optionName];
|
|
}
|
|
else if (refiners[optionName]) {
|
|
refined[optionName] = refiners[optionName](raw[optionName]);
|
|
anyChanges = true;
|
|
}
|
|
else {
|
|
extra[optionName] = currentRaw[optionName];
|
|
}
|
|
}
|
|
if (anyChanges) {
|
|
this.currentCalendarOptionsInput = raw;
|
|
this.currentCalendarOptionsRefined = refined;
|
|
this.stableOptionOverrides = optionOverrides;
|
|
this.stableDynamicOptionOverrides = dynamicOptionOverrides;
|
|
}
|
|
this.optionsForHandling.push(...this.optionsForRefining);
|
|
this.optionsForRefining = [];
|
|
return {
|
|
rawOptions: this.currentCalendarOptionsInput,
|
|
refinedOptions: this.currentCalendarOptionsRefined,
|
|
pluginHooks,
|
|
availableLocaleData,
|
|
localeDefaults,
|
|
extra,
|
|
};
|
|
}
|
|
_computeCurrentViewData(viewType, optionsData, optionOverrides, dynamicOptionOverrides) {
|
|
let viewSpec = optionsData.viewSpecs[viewType];
|
|
if (!viewSpec) {
|
|
throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`);
|
|
}
|
|
let { refinedOptions, extra } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
|
|
warnUnknownOptions(extra);
|
|
this.nowManager.handleInput(optionsData.dateEnv, refinedOptions.now);
|
|
let dateProfileGenerator = this.buildDateProfileGenerator({
|
|
dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
|
|
nowManager: this.nowManager,
|
|
duration: viewSpec.duration,
|
|
durationUnit: viewSpec.durationUnit,
|
|
usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
|
|
dateEnv: optionsData.dateEnv,
|
|
calendarApi: this.props.calendarApi,
|
|
slotMinTime: refinedOptions.slotMinTime,
|
|
slotMaxTime: refinedOptions.slotMaxTime,
|
|
showNonCurrentDates: refinedOptions.showNonCurrentDates,
|
|
dayCount: refinedOptions.dayCount,
|
|
dateAlignment: refinedOptions.dateAlignment,
|
|
dateIncrement: refinedOptions.dateIncrement,
|
|
hiddenDays: refinedOptions.hiddenDays,
|
|
weekends: refinedOptions.weekends,
|
|
validRangeInput: refinedOptions.validRange,
|
|
visibleRangeInput: refinedOptions.visibleRange,
|
|
fixedWeekCount: refinedOptions.fixedWeekCount,
|
|
});
|
|
let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv);
|
|
return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi };
|
|
}
|
|
processRawViewOptions(viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) {
|
|
let raw = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
viewSpec.optionDefaults,
|
|
localeDefaults,
|
|
optionOverrides,
|
|
viewSpec.optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
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);
|
|
let refined = {};
|
|
let currentRaw = this.currentViewOptionsInput;
|
|
let currentRefined = this.currentViewOptionsRefined;
|
|
let anyChanges = false;
|
|
let extra = {};
|
|
for (let optionName in raw) {
|
|
if (raw[optionName] === currentRaw[optionName] ||
|
|
(COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName]))) {
|
|
refined[optionName] = currentRefined[optionName];
|
|
}
|
|
else {
|
|
if (raw[optionName] === this.currentCalendarOptionsInput[optionName] ||
|
|
(COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName]))) {
|
|
if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop
|
|
refined[optionName] = this.currentCalendarOptionsRefined[optionName];
|
|
}
|
|
}
|
|
else if (refiners[optionName]) {
|
|
refined[optionName] = refiners[optionName](raw[optionName]);
|
|
}
|
|
else {
|
|
extra[optionName] = raw[optionName];
|
|
}
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
if (anyChanges) {
|
|
this.currentViewOptionsInput = raw;
|
|
this.currentViewOptionsRefined = refined;
|
|
}
|
|
return {
|
|
rawOptions: this.currentViewOptionsInput,
|
|
refinedOptions: this.currentViewOptionsRefined,
|
|
extra,
|
|
};
|
|
}
|
|
}
|
|
function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) {
|
|
let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
|
|
return new DateEnv({
|
|
calendarSystem: 'gregory',
|
|
timeZone,
|
|
namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
|
|
locale,
|
|
weekNumberCalculation,
|
|
firstDay,
|
|
weekText,
|
|
cmdFormatter: pluginHooks.cmdFormatter,
|
|
defaultSeparator,
|
|
});
|
|
}
|
|
function buildTheme(options, pluginHooks) {
|
|
let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme;
|
|
return new ThemeClass(options);
|
|
}
|
|
function buildDateProfileGenerator(props) {
|
|
let DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator;
|
|
return new DateProfileGeneratorClass(props);
|
|
}
|
|
function buildViewApi(type, getCurrentData, dateEnv) {
|
|
return new ViewImpl(type, getCurrentData, dateEnv);
|
|
}
|
|
function buildEventUiBySource(eventSources) {
|
|
return mapHash(eventSources, (eventSource) => eventSource.ui);
|
|
}
|
|
function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
|
|
let eventUiBases = { '': eventUiSingleBase };
|
|
for (let defId in eventDefs) {
|
|
let def = eventDefs[defId];
|
|
if (def.sourceId && eventUiBySource[def.sourceId]) {
|
|
eventUiBases[defId] = eventUiBySource[def.sourceId];
|
|
}
|
|
}
|
|
return eventUiBases;
|
|
}
|
|
function buildViewUiProps(calendarContext) {
|
|
let { options } = calendarContext;
|
|
return {
|
|
eventUiSingleBase: createEventUi({
|
|
display: options.eventDisplay,
|
|
editable: options.editable,
|
|
startEditable: options.eventStartEditable,
|
|
durationEditable: options.eventDurationEditable,
|
|
constraint: options.eventConstraint,
|
|
overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
|
|
allow: options.eventAllow,
|
|
backgroundColor: options.eventBackgroundColor,
|
|
borderColor: options.eventBorderColor,
|
|
textColor: options.eventTextColor,
|
|
color: options.eventColor,
|
|
// classNames: options.eventClassNames // render hook will handle this
|
|
}, calendarContext),
|
|
selectionConfig: createEventUi({
|
|
constraint: options.selectConstraint,
|
|
overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined,
|
|
allow: options.selectAllow,
|
|
}, calendarContext),
|
|
};
|
|
}
|
|
function computeIsLoading(state, context) {
|
|
for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) {
|
|
if (isLoadingFunc(state)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function parseContextBusinessHours(calendarContext) {
|
|
return parseBusinessHours(calendarContext.options.businessHours, calendarContext);
|
|
}
|
|
function warnUnknownOptions(options, viewName) {
|
|
for (let optionName in options) {
|
|
console.warn(`Unknown option '${optionName}'` +
|
|
(viewName ? ` for view '${viewName}'` : ''));
|
|
}
|
|
}
|
|
|
|
class ToolbarSection extends BaseComponent {
|
|
render() {
|
|
let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
|
|
return createElement('div', { className: 'fc-toolbar-chunk' }, ...children);
|
|
}
|
|
renderWidgetGroup(widgetGroup) {
|
|
let { props } = this;
|
|
let { theme } = this.context;
|
|
let children = [];
|
|
let isOnlyButtons = true;
|
|
for (let widget of widgetGroup) {
|
|
let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget;
|
|
if (buttonName === 'title') {
|
|
isOnlyButtons = false;
|
|
children.push(createElement("h2", { className: "fc-toolbar-title", id: props.titleId }, props.title));
|
|
}
|
|
else {
|
|
let isPressed = buttonName === props.activeButton;
|
|
let isDisabled = (!props.isTodayEnabled && buttonName === 'today') ||
|
|
(!props.isPrevEnabled && buttonName === 'prev') ||
|
|
(!props.isNextEnabled && buttonName === 'next');
|
|
let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')];
|
|
if (isPressed) {
|
|
buttonClasses.push(theme.getClass('buttonActive'));
|
|
}
|
|
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" }) : '')));
|
|
}
|
|
}
|
|
if (children.length > 1) {
|
|
let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || '';
|
|
return createElement('div', { className: groupClassName }, ...children);
|
|
}
|
|
return children[0];
|
|
}
|
|
}
|
|
|
|
class Toolbar extends BaseComponent {
|
|
render() {
|
|
let { model, extraClassName } = this.props;
|
|
let forceLtr = false;
|
|
let startContent;
|
|
let endContent;
|
|
let sectionWidgets = model.sectionWidgets;
|
|
let centerContent = sectionWidgets.center;
|
|
if (sectionWidgets.left) {
|
|
forceLtr = true;
|
|
startContent = sectionWidgets.left;
|
|
}
|
|
else {
|
|
startContent = sectionWidgets.start;
|
|
}
|
|
if (sectionWidgets.right) {
|
|
forceLtr = true;
|
|
endContent = sectionWidgets.right;
|
|
}
|
|
else {
|
|
endContent = sectionWidgets.end;
|
|
}
|
|
let classNames = [
|
|
extraClassName || '',
|
|
'fc-toolbar',
|
|
forceLtr ? 'fc-toolbar-ltr' : '',
|
|
];
|
|
return (createElement("div", { className: classNames.join(' ') },
|
|
this.renderSection('start', startContent || []),
|
|
this.renderSection('center', centerContent || []),
|
|
this.renderSection('end', endContent || [])));
|
|
}
|
|
renderSection(key, widgetGroups) {
|
|
let { props } = this;
|
|
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 }));
|
|
}
|
|
}
|
|
|
|
class ViewHarness extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
availableWidth: null,
|
|
};
|
|
this.handleEl = (el) => {
|
|
this.el = el;
|
|
setRef(this.props.elRef, el);
|
|
this.updateAvailableWidth();
|
|
};
|
|
this.handleResize = () => {
|
|
this.updateAvailableWidth();
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state } = this;
|
|
let { aspectRatio } = props;
|
|
let classNames = [
|
|
'fc-view-harness',
|
|
(aspectRatio || props.liquid || props.height)
|
|
? 'fc-view-harness-active' // harness controls the height
|
|
: 'fc-view-harness-passive', // let the view do the height
|
|
];
|
|
let height = '';
|
|
let paddingBottom = '';
|
|
if (aspectRatio) {
|
|
if (state.availableWidth !== null) {
|
|
height = state.availableWidth / aspectRatio;
|
|
}
|
|
else {
|
|
// while waiting to know availableWidth, we can't set height to *zero*
|
|
// because will cause lots of unnecessary scrollbars within scrollgrid.
|
|
// BETTER: don't start rendering ANYTHING yet until we know container width
|
|
// NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606)
|
|
paddingBottom = `${(1 / aspectRatio) * 100}%`;
|
|
}
|
|
}
|
|
else {
|
|
height = props.height || '';
|
|
}
|
|
return (createElement("div", { "aria-labelledby": props.labeledById, ref: this.handleEl, className: classNames.join(' '), style: { height, paddingBottom } }, props.children));
|
|
}
|
|
componentDidMount() {
|
|
this.context.addResizeHandler(this.handleResize);
|
|
}
|
|
componentWillUnmount() {
|
|
this.context.removeResizeHandler(this.handleResize);
|
|
}
|
|
updateAvailableWidth() {
|
|
if (this.el && // needed. but why?
|
|
this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth
|
|
) {
|
|
this.setState({ availableWidth: this.el.offsetWidth });
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Detects when the user clicks on an event within a DateComponent
|
|
*/
|
|
class EventClicking extends Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
this.handleSegClick = (ev, segEl) => {
|
|
let { component } = this;
|
|
let { context } = component;
|
|
let seg = getElSeg(segEl);
|
|
if (seg && // might be the <div> surrounding the more link
|
|
component.isValidSegDownEl(ev.target)) {
|
|
// our way to simulate a link click for elements that can't be <a> tags
|
|
// grab before trigger fired in case trigger trashes DOM thru rerendering
|
|
let hasUrlContainer = elementClosest(ev.target, '.fc-event-forced-url');
|
|
let url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : '';
|
|
context.emitter.trigger('eventClick', {
|
|
el: segEl,
|
|
event: new EventImpl(component.context, seg.eventRange.def, seg.eventRange.instance),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
if (url && !ev.defaultPrevented) {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
};
|
|
this.destroy = listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events
|
|
this.handleSegClick);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Triggers events and adds/removes core classNames when the user's pointer
|
|
enters/leaves event-elements of a component.
|
|
*/
|
|
class EventHovering extends Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
// for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
|
|
this.handleEventElRemove = (el) => {
|
|
if (el === this.currentSegEl) {
|
|
this.handleSegLeave(null, this.currentSegEl);
|
|
}
|
|
};
|
|
this.handleSegEnter = (ev, segEl) => {
|
|
if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
|
|
this.currentSegEl = segEl;
|
|
this.triggerEvent('eventMouseEnter', ev, segEl);
|
|
}
|
|
};
|
|
this.handleSegLeave = (ev, segEl) => {
|
|
if (this.currentSegEl) {
|
|
this.currentSegEl = null;
|
|
this.triggerEvent('eventMouseLeave', ev, segEl);
|
|
}
|
|
};
|
|
this.removeHoverListeners = listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events
|
|
this.handleSegEnter, this.handleSegLeave);
|
|
}
|
|
destroy() {
|
|
this.removeHoverListeners();
|
|
}
|
|
triggerEvent(publicEvName, ev, segEl) {
|
|
let { component } = this;
|
|
let { context } = component;
|
|
let seg = getElSeg(segEl);
|
|
if (!ev || component.isValidSegDownEl(ev.target)) {
|
|
context.emitter.trigger(publicEvName, {
|
|
el: segEl,
|
|
event: new EventImpl(context, seg.eventRange.def, seg.eventRange.instance),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class CalendarContent extends PureComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buildViewContext = memoize(buildViewContext);
|
|
this.buildViewPropTransformers = memoize(buildViewPropTransformers);
|
|
this.buildToolbarProps = memoize(buildToolbarProps);
|
|
this.headerRef = createRef();
|
|
this.footerRef = createRef();
|
|
this.interactionsStore = {};
|
|
// eslint-disable-next-line
|
|
this.state = {
|
|
viewLabelId: getUniqueDomId(),
|
|
};
|
|
// Component Registration
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
this.registerInteractiveComponent = (component, settingsInput) => {
|
|
let settings = parseInteractionSettings(component, settingsInput);
|
|
let DEFAULT_INTERACTIONS = [
|
|
EventClicking,
|
|
EventHovering,
|
|
];
|
|
let interactionClasses = DEFAULT_INTERACTIONS.concat(this.props.pluginHooks.componentInteractions);
|
|
let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings));
|
|
this.interactionsStore[component.uid] = interactions;
|
|
interactionSettingsStore[component.uid] = settings;
|
|
};
|
|
this.unregisterInteractiveComponent = (component) => {
|
|
let listeners = this.interactionsStore[component.uid];
|
|
if (listeners) {
|
|
for (let listener of listeners) {
|
|
listener.destroy();
|
|
}
|
|
delete this.interactionsStore[component.uid];
|
|
}
|
|
delete interactionSettingsStore[component.uid];
|
|
};
|
|
// Resizing
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
this.resizeRunner = new DelayedRunner(() => {
|
|
this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ?
|
|
this.props.emitter.trigger('windowResize', { view: this.props.viewApi });
|
|
});
|
|
this.handleWindowResize = (ev) => {
|
|
let { options } = this.props;
|
|
if (options.handleWindowResize &&
|
|
ev.target === window // avoid jqui events
|
|
) {
|
|
this.resizeRunner.request(options.windowResizeDelay);
|
|
}
|
|
};
|
|
}
|
|
/*
|
|
renders INSIDE of an outer div
|
|
*/
|
|
render() {
|
|
let { props } = this;
|
|
let { toolbarConfig, options } = props;
|
|
let viewVGrow = false;
|
|
let viewHeight = '';
|
|
let viewAspectRatio;
|
|
if (props.isHeightAuto || props.forPrint) {
|
|
viewHeight = '';
|
|
}
|
|
else if (options.height != null) {
|
|
viewVGrow = true;
|
|
}
|
|
else if (options.contentHeight != null) {
|
|
viewHeight = options.contentHeight;
|
|
}
|
|
else {
|
|
viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall
|
|
}
|
|
let viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.nowManager, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
|
|
let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle)
|
|
? this.state.viewLabelId
|
|
: undefined;
|
|
return (createElement(ViewContextType.Provider, { value: viewContext },
|
|
createElement(NowTimer, { unit: "day" }, (nowDate) => {
|
|
let toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, nowDate, props.viewTitle);
|
|
return (createElement(Fragment, null,
|
|
toolbarConfig.header && (createElement(Toolbar, Object.assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.header, titleId: viewLabelId }, toolbarProps))),
|
|
createElement(ViewHarness, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, labeledById: viewLabelId },
|
|
this.renderView(props),
|
|
this.buildAppendContent()),
|
|
toolbarConfig.footer && (createElement(Toolbar, Object.assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footer, titleId: "" }, toolbarProps)))));
|
|
})));
|
|
}
|
|
componentDidMount() {
|
|
let { props } = this;
|
|
this.calendarInteractions = props.pluginHooks.calendarInteractions
|
|
.map((CalendarInteractionClass) => new CalendarInteractionClass(props));
|
|
window.addEventListener('resize', this.handleWindowResize);
|
|
let { propSetHandlers } = props.pluginHooks;
|
|
for (let propName in propSetHandlers) {
|
|
propSetHandlers[propName](props[propName], props);
|
|
}
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
let { props } = this;
|
|
let { propSetHandlers } = props.pluginHooks;
|
|
for (let propName in propSetHandlers) {
|
|
if (props[propName] !== prevProps[propName]) {
|
|
propSetHandlers[propName](props[propName], props);
|
|
}
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.handleWindowResize);
|
|
this.resizeRunner.clear();
|
|
for (let interaction of this.calendarInteractions) {
|
|
interaction.destroy();
|
|
}
|
|
this.props.emitter.trigger('_unmount');
|
|
}
|
|
buildAppendContent() {
|
|
let { props } = this;
|
|
let children = props.pluginHooks.viewContainerAppends.map((buildAppendContent) => buildAppendContent(props));
|
|
return createElement(Fragment, {}, ...children);
|
|
}
|
|
renderView(props) {
|
|
let { pluginHooks } = props;
|
|
let { viewSpec } = props;
|
|
let viewProps = {
|
|
dateProfile: props.dateProfile,
|
|
businessHours: props.businessHours,
|
|
eventStore: props.renderableEventStore,
|
|
eventUiBases: props.eventUiBases,
|
|
dateSelection: props.dateSelection,
|
|
eventSelection: props.eventSelection,
|
|
eventDrag: props.eventDrag,
|
|
eventResize: props.eventResize,
|
|
isHeightAuto: props.isHeightAuto,
|
|
forPrint: props.forPrint,
|
|
};
|
|
let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
|
|
for (let transformer of transformers) {
|
|
Object.assign(viewProps, transformer.transform(viewProps, props));
|
|
}
|
|
let ViewComponent = viewSpec.component;
|
|
return (createElement(ViewComponent, Object.assign({}, viewProps)));
|
|
}
|
|
}
|
|
function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) {
|
|
// don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
|
|
let todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason
|
|
let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false);
|
|
let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false);
|
|
return {
|
|
title,
|
|
activeButton: viewSpec.type,
|
|
navUnit: viewSpec.singleUnit,
|
|
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
|
|
isPrevEnabled: prevInfo.isValid,
|
|
isNextEnabled: nextInfo.isValid,
|
|
};
|
|
}
|
|
// Plugin
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
function buildViewPropTransformers(theClasses) {
|
|
return theClasses.map((TheClass) => new TheClass());
|
|
}
|
|
|
|
class Calendar extends CalendarImpl {
|
|
constructor(el, optionOverrides = {}) {
|
|
super();
|
|
this.isRendering = false;
|
|
this.isRendered = false;
|
|
this.currentClassNames = [];
|
|
this.customContentRenderId = 0;
|
|
this.handleAction = (action) => {
|
|
// actions we know we want to render immediately
|
|
switch (action.type) {
|
|
case 'SET_EVENT_DRAG':
|
|
case 'SET_EVENT_RESIZE':
|
|
this.renderRunner.tryDrain();
|
|
}
|
|
};
|
|
this.handleData = (data) => {
|
|
this.currentData = data;
|
|
this.renderRunner.request(data.calendarOptions.rerenderDelay);
|
|
};
|
|
this.handleRenderRequest = () => {
|
|
if (this.isRendering) {
|
|
this.isRendered = true;
|
|
let { currentData } = this;
|
|
flushSync(() => {
|
|
render(createElement(CalendarRoot, { options: currentData.calendarOptions, theme: currentData.theme, emitter: currentData.emitter }, (classNames, height, isHeightAuto, forPrint) => {
|
|
this.setClassNames(classNames);
|
|
this.setHeight(height);
|
|
return (createElement(RenderId.Provider, { value: this.customContentRenderId },
|
|
createElement(CalendarContent, Object.assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData))));
|
|
}), this.el);
|
|
});
|
|
}
|
|
else if (this.isRendered) {
|
|
this.isRendered = false;
|
|
render(null, this.el);
|
|
this.setClassNames([]);
|
|
this.setHeight('');
|
|
}
|
|
};
|
|
ensureElHasStyles(el);
|
|
this.el = el;
|
|
this.renderRunner = new DelayedRunner(this.handleRenderRequest);
|
|
new CalendarDataManager({
|
|
optionOverrides,
|
|
calendarApi: this,
|
|
onAction: this.handleAction,
|
|
onData: this.handleData,
|
|
});
|
|
}
|
|
render() {
|
|
let wasRendering = this.isRendering;
|
|
if (!wasRendering) {
|
|
this.isRendering = true;
|
|
}
|
|
else {
|
|
this.customContentRenderId += 1;
|
|
}
|
|
this.renderRunner.request();
|
|
if (wasRendering) {
|
|
this.updateSize();
|
|
}
|
|
}
|
|
destroy() {
|
|
if (this.isRendering) {
|
|
this.isRendering = false;
|
|
this.renderRunner.request();
|
|
}
|
|
}
|
|
updateSize() {
|
|
flushSync(() => {
|
|
super.updateSize();
|
|
});
|
|
}
|
|
batchRendering(func) {
|
|
this.renderRunner.pause('batchRendering');
|
|
func();
|
|
this.renderRunner.resume('batchRendering');
|
|
}
|
|
pauseRendering() {
|
|
this.renderRunner.pause('pauseRendering');
|
|
}
|
|
resumeRendering() {
|
|
this.renderRunner.resume('pauseRendering', true);
|
|
}
|
|
resetOptions(optionOverrides, changedOptionNames) {
|
|
this.currentDataManager.resetOptions(optionOverrides, changedOptionNames);
|
|
}
|
|
setClassNames(classNames) {
|
|
if (!isArraysEqual(classNames, this.currentClassNames)) {
|
|
let { classList } = this.el;
|
|
for (let className of this.currentClassNames) {
|
|
classList.remove(className);
|
|
}
|
|
for (let className of classNames) {
|
|
classList.add(className);
|
|
}
|
|
this.currentClassNames = classNames;
|
|
}
|
|
}
|
|
setHeight(height) {
|
|
applyStyleProp(this.el, 'height', height);
|
|
}
|
|
}
|
|
|
|
function formatDate(dateInput, options = {}) {
|
|
let dateEnv = buildDateEnv(options);
|
|
let formatter = createFormatter(options);
|
|
let dateMeta = dateEnv.createMarkerMeta(dateInput);
|
|
if (!dateMeta) { // TODO: warning?
|
|
return '';
|
|
}
|
|
return dateEnv.format(dateMeta.marker, formatter, {
|
|
forcedTzo: dateMeta.forcedTzo,
|
|
});
|
|
}
|
|
function formatRange(startInput, endInput, options) {
|
|
let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object
|
|
let formatter = createFormatter(options);
|
|
let startMeta = dateEnv.createMarkerMeta(startInput);
|
|
let endMeta = dateEnv.createMarkerMeta(endInput);
|
|
if (!startMeta || !endMeta) { // TODO: warning?
|
|
return '';
|
|
}
|
|
return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
|
|
forcedStartTzo: startMeta.forcedTzo,
|
|
forcedEndTzo: endMeta.forcedTzo,
|
|
isEndExclusive: options.isEndExclusive,
|
|
defaultSeparator: BASE_OPTION_DEFAULTS.defaultRangeSeparator,
|
|
});
|
|
}
|
|
// TODO: more DRY and optimized
|
|
function buildDateEnv(settings) {
|
|
let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
|
|
return new DateEnv(Object.assign(Object.assign({ timeZone: BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale }));
|
|
}
|
|
|
|
// HELPERS
|
|
/*
|
|
if nextDayThreshold is specified, slicing is done in an all-day fashion.
|
|
you can get nextDayThreshold from context.nextDayThreshold
|
|
*/
|
|
function sliceEvents(props, allDay) {
|
|
return sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg;
|
|
}
|
|
|
|
const version = '6.1.17';
|
|
|
|
export { Calendar, createPlugin, formatDate, formatRange, globalLocales, globalPlugins, sliceEvents, version };
|