>>1,1),e.h.removeChild(n);}};}R(k$1(Z,{context:e.context},n.__v),e.v);}function $(n,e){var r=k$1(Y,{__v:n,h:e});return r.containerInfo=e,r}(B.prototype=new C).__a=function(n){var t=this,e=j(t.__v),r=t.l.get(n);return r[0]++,function(u){var o=function(){t.props.revealOrder?(r.push(u),H(t,n,r)):u();};e?e(o):o();}},B.prototype.render=function(n){this.i=null,this.l=new Map;var t=F(n.children);n.revealOrder&&"b"===n.revealOrder[0]&&t.reverse();for(var e=t.length;e--;)this.l.set(t[e],this.i=[1,0,this.i]);return n.children},B.prototype.componentDidUpdate=B.prototype.componentDidMount=function(){var n=this;this.l.forEach(function(t,e){H(n,e,t);});};var q="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,G=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,J=/^on(Ani|Tra|Tou|BeforeInp|Compo)/,K=/[A-Z0-9]/g,Q="undefined"!=typeof document,X=function(n){return ("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/:/fil|che|ra/).test(n)};function nn(n,t,e){return null==t.__k&&(t.textContent=""),R(n,t),"function"==typeof e&&e(),n?n.__c:null}C.prototype.isReactComponent=!0,["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(t){Object.defineProperty(C.prototype,t,{configurable:!0,get:function(){return this["UNSAFE_"+t]},set:function(n){Object.defineProperty(this,t,{configurable:!0,writable:!0,value:n});}});});var en=l$2.event;l$2.event=function(n){return en&&(n=en(n)),n.persist=function(){},n.isPropagationStopped=function(){return this.cancelBubble},n.isDefaultPrevented=function(){return this.defaultPrevented},n.nativeEvent=n};var un={configurable:!0,get:function(){return this.class}},on=l$2.vnode;l$2.vnode=function(n){"string"==typeof n.type&&function(n){var t=n.props,e=n.type,u={},o=-1==e.indexOf("-");for(var i in t){var l=t[i];if(!("value"===i&&"defaultValue"in t&&null==l||Q&&"children"===i&&"noscript"===e||"class"===i||"className"===i)){var c=i.toLowerCase();"defaultValue"===i&&"value"in t&&null==t.value?i="value":"download"===i&&!0===l?l="":"translate"===c&&"no"===l?l=!1:"o"===c[0]&&"n"===c[1]?"ondoubleclick"===c?i="ondblclick":"onchange"!==c||"input"!==e&&"textarea"!==e||X(t.type)?"onfocus"===c?i="onfocusin":"onblur"===c?i="onfocusout":J.test(i)&&(i=c):c=i="oninput":o&&G.test(i)?i=i.replace(K,"-$&").toLowerCase():null===l&&(l=void 0),"oninput"===c&&u[i=c]&&(i="oninputCapture"),u[i]=l;}}"select"==e&&(u.multiple&&Array.isArray(u.value)&&(u.value=F(t.children).forEach(function(n){n.props.selected=-1!=u.value.indexOf(n.props.value);})),null!=u.defaultValue&&(u.value=F(t.children).forEach(function(n){n.props.selected=u.multiple?-1!=u.defaultValue.indexOf(n.props.value):u.defaultValue==n.props.value;}))),t.class&&!t.className?(u.class=t.class,Object.defineProperty(u,"className",un)):t.className&&(u.class=u.className=t.className),n.props=u;}(n),n.$$typeof=q,on&&on(n);};var ln=l$2.__r;l$2.__r=function(n){ln&&ln(n),n.__c;};var cn=l$2.diffed;l$2.diffed=function(n){cn&&cn(n);var t=n.props,e=n.__e;null!=e&&"textarea"===n.type&&"value"in t&&t.value!==e.value&&(e.value=null==t.value?"":t.value);};function hn(n){return !!n&&n.$$typeof===q}function pn(n){return !!n.__k&&(R(null,n),!0)}var bn=function(n,t){var r=l$2.debounceRendering;l$2.debounceRendering=function(n){return n()};var u=n(t);return l$2.debounceRendering=r,u};
+
+ function memoize(workerFunc, resEquality, teardownFunc) {
+ let currentArgs;
+ let currentRes;
+ return function (...newArgs) {
+ if (!currentArgs) {
+ currentRes = workerFunc.apply(this, newArgs);
+ }
+ else if (!isArraysEqual(currentArgs, newArgs)) {
+ if (teardownFunc) {
+ teardownFunc(currentRes);
+ }
+ let res = workerFunc.apply(this, newArgs);
+ if (!resEquality || !resEquality(res, currentRes)) {
+ currentRes = res;
+ }
+ }
+ currentArgs = newArgs;
+ return currentRes;
+ };
+ }
+ function memoizeObjArg(workerFunc, resEquality, teardownFunc) {
+ let currentArg;
+ let currentRes;
+ return (newArg) => {
+ if (!currentArg) {
+ currentRes = workerFunc.call(this, newArg);
+ }
+ else if (!isPropsEqualShallow(currentArg, newArg)) {
+ if (teardownFunc) {
+ teardownFunc(currentRes);
+ }
+ let res = workerFunc.call(this, newArg);
+ if (!resEquality || !resEquality(res, currentRes)) {
+ currentRes = res;
+ }
+ }
+ currentArg = newArg;
+ return currentRes;
+ };
+ }
+
+ const ViewContextType = X$1({}); // for Components
+ function buildViewContext(viewSpec, viewApi, viewOptions, dateProfileGenerator, dateEnv, nowManager, pluginHooks, dispatch, getCurrentData, emitter, calendarApi, baseId, registerInteractiveComponent, unregisterInteractiveComponent) {
+ return {
+ dateEnv,
+ nowManager,
+ options: viewOptions,
+ pluginHooks,
+ emitter,
+ dispatch,
+ getCurrentData,
+ calendarApi,
+ viewSpec,
+ viewApi,
+ dateProfileGenerator,
+ baseId,
+ registerInteractiveComponent,
+ unregisterInteractiveComponent,
+ };
+ }
+
+ /* eslint max-classes-per-file: off */
+ class PureComponent extends C {
+ // debug: boolean
+ shouldComponentUpdate(nextProps, nextState) {
+ return !isPropsEqualWithMap(this.props, nextProps, this.propEquality /*, this.debug && 'props' */) ||
+ !isPropsEqualWithMap(this.state, nextState, this.stateEquality /*, this.debug && 'state' */);
+ }
+ }
+ PureComponent.addPropsEquality = addPropsEquality;
+ PureComponent.addStateEquality = addStateEquality;
+ PureComponent.contextType = ViewContextType;
+ PureComponent.prototype.propEquality = {};
+ PureComponent.prototype.stateEquality = {};
+ class BaseComponent extends PureComponent {
+ }
+ BaseComponent.contextType = ViewContextType;
+ function addPropsEquality(propEquality) {
+ let hash = Object.create(this.prototype.propEquality);
+ Object.assign(hash, propEquality);
+ this.prototype.propEquality = hash;
+ }
+ function addStateEquality(stateEquality) {
+ let hash = Object.create(this.prototype.stateEquality);
+ Object.assign(hash, stateEquality);
+ this.prototype.stateEquality = hash;
+ }
+ // use other one
+ function setRef(ref, current) {
+ if (typeof ref === 'function') {
+ ref(current);
+ }
+ else if (ref) {
+ // see https://github.com/facebook/react/issues/13029
+ ref.current = current;
+ }
+ }
+
+ class ContentInjector extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.id = guid();
+ this.queuedDomNodes = [];
+ this.currentDomNodes = [];
+ this.handleEl = (el) => {
+ this.el = el;
+ const { options } = this.context;
+ const { generatorName } = this.props;
+ if (!options.customRenderingReplaces || !hasCustomRenderingHandler(generatorName, options)) {
+ this.updateElRef(el);
+ }
+ };
+ this.updateElRef = (el) => {
+ if (this.props.elRef) {
+ setRef(this.props.elRef, el);
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { options } = context;
+ const { customGenerator, defaultGenerator, renderProps } = props;
+ const attrs = buildElAttrs(props, '', this.handleEl);
+ let useDefault = false;
+ let innerContent;
+ let queuedDomNodes = [];
+ let currentGeneratorMeta;
+ if (customGenerator != null) {
+ const customGeneratorRes = typeof customGenerator === 'function' ?
+ customGenerator(renderProps) :
+ customGenerator;
+ if (customGeneratorRes === true) {
+ useDefault = true;
+ // NOTE: see how mergeContentInjectors also uses `true` to signal useDefault
+ }
+ else {
+ const isObject = customGeneratorRes && typeof customGeneratorRes === 'object'; // non-null
+ if (isObject && ('html' in customGeneratorRes)) {
+ attrs.dangerouslySetInnerHTML = { __html: customGeneratorRes.html };
+ }
+ else if (isObject && ('domNodes' in customGeneratorRes)) {
+ queuedDomNodes = Array.prototype.slice.call(customGeneratorRes.domNodes);
+ }
+ else if (isObject
+ ? hn(customGeneratorRes) // vdom node
+ : typeof customGeneratorRes !== 'function' // primitive value (like string or number)
+ ) {
+ // use in vdom
+ innerContent = customGeneratorRes;
+ }
+ else {
+ // an exotic object for handleCustomRendering
+ currentGeneratorMeta = customGeneratorRes;
+ }
+ }
+ }
+ else {
+ useDefault = !hasCustomRenderingHandler(props.generatorName, options);
+ }
+ if (useDefault && defaultGenerator) {
+ innerContent = defaultGenerator(renderProps);
+ }
+ this.queuedDomNodes = queuedDomNodes;
+ this.currentGeneratorMeta = currentGeneratorMeta;
+ return k$1(props.tag, attrs, innerContent);
+ }
+ componentDidMount() {
+ this.applyQueueudDomNodes();
+ this.triggerCustomRendering(true);
+ }
+ componentDidUpdate() {
+ this.applyQueueudDomNodes();
+ this.triggerCustomRendering(true);
+ }
+ componentWillUnmount() {
+ this.triggerCustomRendering(false); // TODO: different API for removal?
+ }
+ triggerCustomRendering(isActive) {
+ const { props, context } = this;
+ const { handleCustomRendering, customRenderingMetaMap } = context.options;
+ if (handleCustomRendering) {
+ const generatorMeta = this.currentGeneratorMeta ??
+ customRenderingMetaMap?.[props.generatorName];
+ if (generatorMeta) {
+ handleCustomRendering({
+ id: this.id,
+ isActive,
+ containerEl: this.el,
+ reportNewContainerEl: this.updateElRef, // front-end framework tells us about new container els
+ generatorMeta,
+ ...props,
+ });
+ }
+ }
+ }
+ applyQueueudDomNodes() {
+ const { queuedDomNodes, currentDomNodes } = this;
+ const { el } = this;
+ if (!isArraysEqual(queuedDomNodes, currentDomNodes)) {
+ for (const domNode of currentDomNodes) {
+ domNode.remove();
+ }
+ for (let newNode of queuedDomNodes) {
+ el.appendChild(newNode);
+ }
+ this.currentDomNodes = queuedDomNodes;
+ }
+ }
+ }
+ ContentInjector.addPropsEquality({
+ renderProps: isPropsEqualShallow,
+ attrs: isNonHandlerPropsEqual,
+ style: isPropsEqualShallow,
+ });
+ // Util
+ /*
+ Does UI-framework provide custom way of rendering that does not use Preact VDOM
+ AND does the calendar's options define custom rendering?
+ AKA. Should we NOT render the default content?
+ */
+ function hasCustomRenderingHandler(generatorName, options) {
+ return Boolean(options.handleCustomRendering &&
+ generatorName &&
+ options.customRenderingMetaMap?.[generatorName]);
+ }
+ function buildElAttrs(props, className, elRef) {
+ const attrs = { ...props.attrs, ref: elRef };
+ if (props.className || className) {
+ attrs.className = joinClassNames(className, props.className, attrs.className);
+ }
+ if (props.style) {
+ attrs.style = props.style;
+ }
+ return attrs;
+ }
+
+ const RenderId = X$1(0);
+
+ class ContentContainer extends C {
+ constructor() {
+ super(...arguments);
+ this.InnerContent = InnerContentInjector.bind(undefined, this);
+ this.handleEl = (el) => {
+ this.el = el;
+ if (this.props.elRef) {
+ setRef(this.props.elRef, el);
+ if (el && this.didMountMisfire) {
+ this.componentDidMount();
+ }
+ }
+ };
+ }
+ render() {
+ const { props } = this;
+ const generatedClassName = generateClassName(props.classNameGenerator, props.renderProps);
+ if (props.children) {
+ const attrs = buildElAttrs(props, generatedClassName, this.handleEl);
+ const children = props.children(this.InnerContent, props.renderProps, attrs);
+ if (props.tag) {
+ return k$1(props.tag, attrs, children);
+ }
+ else {
+ return children;
+ }
+ }
+ else {
+ return k$1((ContentInjector), {
+ ...props,
+ elRef: this.handleEl,
+ tag: props.tag || 'div',
+ className: joinClassNames(props.className, generatedClassName),
+ renderId: this.context,
+ });
+ }
+ }
+ componentDidMount() {
+ if (this.el) {
+ this.props.didMount?.({
+ ...this.props.renderProps,
+ el: this.el,
+ });
+ }
+ else {
+ this.didMountMisfire = true;
+ }
+ }
+ componentWillUnmount() {
+ this.props.willUnmount?.({
+ ...this.props.renderProps,
+ el: this.el,
+ });
+ }
+ }
+ ContentContainer.contextType = RenderId;
+ function InnerContentInjector(containerComponent, props) {
+ const parentProps = containerComponent.props;
+ return k$1((ContentInjector), {
+ renderProps: parentProps.renderProps,
+ generatorName: parentProps.generatorName,
+ customGenerator: parentProps.customGenerator,
+ defaultGenerator: parentProps.defaultGenerator,
+ renderId: containerComponent.context,
+ ...props,
+ });
+ }
+ // Utils
+ function generateClassName(classNameGenerator, renderProps) {
+ return (typeof classNameGenerator === 'function' ?
+ classNameGenerator(renderProps) :
+ classNameGenerator) || ''; // handles undefined
+ }
+ function renderText$1(renderProps) {
+ return renderProps.text;
+ }
+
+ function getIsHeightAuto(options) {
+ return options.height === 'auto' || options.contentHeight === 'auto';
+ }
+ function getTableHeaderSticky(options) {
+ let { tableHeaderSticky } = options;
+ if (tableHeaderSticky == null || tableHeaderSticky === 'auto') {
+ tableHeaderSticky = getIsHeightAuto(options);
+ }
+ return tableHeaderSticky;
+ }
+ function getFooterScrollbarSticky(options) {
+ const isHeightAuto = getIsHeightAuto(options);
+ let { footerScrollbarSticky } = options;
+ if (footerScrollbarSticky == null || footerScrollbarSticky === 'auto') {
+ footerScrollbarSticky = isHeightAuto;
+ }
+ return Boolean(footerScrollbarSticky) && isHeightAuto;
+ }
+ function getScrollerSyncerClass(pluginHooks) {
+ const ScrollerSyncer = pluginHooks.scrollerSyncerClass;
+ if (!ScrollerSyncer) {
+ throw new RangeError('Must import @fullcalendar/scrollgrid');
+ }
+ return ScrollerSyncer;
+ }
+
+ class NowTimerRunner {
+ constructor(handleChange) {
+ this.handleChange = handleChange;
+ this.isMounted = false;
+ this.handleRefresh = () => {
+ let timing = this.computeTiming();
+ if (timing.nowDate.valueOf() !== this.nowDate.valueOf()) {
+ this.nowDate = timing.nowDate;
+ this.todayRange = timing.todayRange;
+ this.handleChange();
+ }
+ this.clearTimeout();
+ this.setTimeout(timing.waitMs);
+ };
+ this.handleVisibilityChange = () => {
+ if (!document.hidden) {
+ this.handleRefresh();
+ }
+ };
+ }
+ update(input) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ // init inputs
+ this.unit = input.unit;
+ this.unitValue = input.unitValue;
+ this.nowIndicatorSnap = input.nowIndicatorSnap;
+ this.nowManager = input.nowManager;
+ this.dateEnv = input.dateEnv;
+ // init outputs
+ const timing = this.computeTiming();
+ this.nowDate = timing.nowDate;
+ this.todayRange = timing.todayRange;
+ // init listeners
+ this.setTimeout();
+ this.nowManager.addResetListener(this.handleRefresh);
+ // fired tab becomes visible after being hidden
+ // SSR check. CalendarDataManager calls top-level sync :(
+ if (typeof document !== 'undefined') {
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ }
+ }
+ else if (input.unit !== this.unit ||
+ input.unitValue !== this.unitValue ||
+ input.nowIndicatorSnap !== this.nowIndicatorSnap ||
+ input.nowManager !== this.nowManager ||
+ input.dateEnv !== this.dateEnv) {
+ // update inputs
+ this.unit = input.unit;
+ this.unitValue = input.unitValue;
+ this.nowIndicatorSnap = input.nowIndicatorSnap;
+ this.nowManager = input.nowManager;
+ this.dateEnv = input.dateEnv;
+ this.clearTimeout();
+ this.setTimeout();
+ }
+ return {
+ nowDate: this.nowDate,
+ todayRange: this.todayRange,
+ };
+ }
+ destroy() {
+ if (this.isMounted) {
+ this.isMounted = false;
+ this.clearTimeout();
+ this.nowManager.removeResetListener(this.handleRefresh);
+ // SSR check. CalendarDataManager calls top-level sync :(
+ if (typeof document !== 'undefined') {
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+ }
+ }
+ }
+ computeTiming() {
+ let unroundedNow = this.nowManager.getDateMarker();
+ let { unit, unitValue, nowIndicatorSnap, dateEnv } = this;
+ if (nowIndicatorSnap === 'auto') {
+ nowIndicatorSnap =
+ // large unit?
+ /year|month|week|day/.test(unit) ||
+ // if slotDuration 30 mins for example, would NOT appear to snap (legacy behavior)
+ (unitValue || 1) === 1;
+ }
+ let nowDate;
+ let waitMs;
+ if (nowIndicatorSnap) {
+ nowDate = dateEnv.startOf(unroundedNow, unit); // aka currentUnitStart
+ let nextUnitStart = dateEnv.add(nowDate, createDuration(1, unit));
+ waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf();
+ }
+ else {
+ nowDate = unroundedNow;
+ waitMs = 1000 * 60; // 1 minute
+ }
+ // there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342)
+ // ensure no longer than a day
+ waitMs = Math.min(1000 * 60 * 60 * 24, waitMs);
+ return {
+ nowDate,
+ todayRange: buildDayRange(nowDate),
+ waitMs,
+ };
+ }
+ setTimeout(waitMs = this.computeTiming().waitMs) {
+ // NOTE: timeout could take longer than expected if tab sleeps,
+ // which is why we listen to 'visibilitychange'
+ this.timeoutId = setTimeout(() => {
+ // NOTE: timeout could also return *earlier* than expected, and we need to wait like 2 ms more
+ // This is why use use same waitMs from computeTiming
+ const timing = this.computeTiming();
+ this.nowDate = timing.nowDate;
+ this.todayRange = timing.todayRange;
+ this.handleChange();
+ this.setTimeout(timing.waitMs);
+ }, waitMs);
+ }
+ clearTimeout() {
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ }
+ }
+ }
+ function buildDayRange(date) {
+ let start = startOfDay(date);
+ let end = addDays(start, 1);
+ return { start, end };
+ }
+
+ class DateProfileGenerator {
+ constructor(props) {
+ this.props = props;
+ this.initHiddenDays();
+ }
+ /* Date Range Computation
+ ------------------------------------------------------------------------------------------------------------------*/
+ // Builds a structure with info about what the dates/ranges will be for the "prev" view.
+ buildPrev(currentDateProfile, currentDate, nowDate, forceToValid) {
+ let { dateEnv } = this.props;
+ let prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
+ currentDateProfile.dateIncrement);
+ return this.build(prevDate, nowDate, -1, forceToValid);
+ }
+ // Builds a structure with info about what the dates/ranges will be for the "next" view.
+ buildNext(currentDateProfile, currentDate, nowDate, forceToValid) {
+ let { dateEnv } = this.props;
+ let nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
+ currentDateProfile.dateIncrement);
+ return this.build(nextDate, nowDate, 1, forceToValid);
+ }
+ // Builds a structure holding dates/ranges for rendering around the given date.
+ // Optional direction param indicates whether the date is being incremented/decremented
+ // from its previous value. decremented = -1, incremented = 1 (default).
+ build(currentDate, nowDate, direction, forceToValid = true) {
+ let { props } = this;
+ let validRange;
+ let currentInfo;
+ let isRangeAllDay;
+ let renderRange;
+ let activeRange;
+ let isValid;
+ validRange = this.buildValidRange(nowDate);
+ validRange = this.trimHiddenDays(validRange);
+ if (forceToValid) {
+ currentDate = constrainMarkerToRange(currentDate, validRange);
+ }
+ currentInfo = this.buildCurrentRangeInfo(currentDate, direction);
+ isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
+ renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay);
+ renderRange = this.trimHiddenDays(renderRange);
+ activeRange = renderRange;
+ if (!props.showNonCurrentDates) {
+ activeRange = intersectRanges(activeRange, currentInfo.range);
+ }
+ activeRange = this.adjustActiveRange(activeRange);
+ activeRange = intersectRanges(activeRange, validRange); // might return null
+ // it's invalid if the originally requested date is not contained,
+ // or if the range is completely outside of the valid range.
+ isValid = rangesIntersect(currentInfo.range, validRange);
+ // HACK: constrain to render-range so `currentDate` is more useful to view rendering
+ if (!rangeContainsMarker(renderRange, currentDate)) {
+ currentDate = renderRange.start;
+ }
+ return {
+ currentDate,
+ // constraint for where prev/next operations can go and where events can be dragged/resized to.
+ // an object with optional start and end properties.
+ validRange,
+ // range the view is formally responsible for.
+ // for example, a month view might have 1st-31st, excluding padded dates
+ currentRange: currentInfo.range,
+ // name of largest unit being displayed, like "month" or "week"
+ currentRangeUnit: currentInfo.unit,
+ isRangeAllDay,
+ // dates that display events and accept drag-n-drop
+ // will be `null` if no dates accept events
+ activeRange,
+ // date range with a rendered skeleton
+ // includes not-active days that need some sort of DOM
+ renderRange,
+ // Duration object that denotes the first visible time of any given day
+ slotMinTime: props.slotMinTime,
+ // Duration object that denotes the exclusive visible end time of any given day
+ slotMaxTime: props.slotMaxTime,
+ isValid,
+ // how far the current date will move for a prev/next operation
+ dateIncrement: this.buildDateIncrement(currentInfo.duration),
+ // pass a fallback (might be null) ^
+ };
+ }
+ // Builds an object with optional start/end properties.
+ // Indicates the minimum/maximum dates to display.
+ // not responsible for trimming hidden days.
+ buildValidRange(nowDate) {
+ let input = this.props.validRangeInput;
+ let simpleInput = typeof input === 'function'
+ ? input.call(this.props.calendarApi, this.props.dateEnv.toDate(nowDate))
+ : input;
+ return this.refineRange(simpleInput) ||
+ { start: null, end: null }; // completely open-ended
+ }
+ // Builds a structure with info about the "current" range, the range that is
+ // highlighted as being the current month for example.
+ // See build() for a description of `direction`.
+ // Guaranteed to have `range` and `unit` properties. `duration` is optional.
+ buildCurrentRangeInfo(date, direction) {
+ let { props } = this;
+ let duration = null;
+ let unit = null;
+ let range = null;
+ let dayCount;
+ if (props.duration) {
+ duration = props.duration;
+ unit = props.durationUnit;
+ range = this.buildRangeFromDuration(date, direction, duration, unit);
+ }
+ else if ((dayCount = this.props.dayCount)) {
+ unit = 'day';
+ range = this.buildRangeFromDayCount(date, direction, dayCount);
+ }
+ else if ((range = this.buildCustomVisibleRange(date))) {
+ unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit;
+ }
+ else {
+ duration = this.getFallbackDuration();
+ unit = greatestDurationDenominator(duration).unit;
+ range = this.buildRangeFromDuration(date, direction, duration, unit);
+ }
+ return { duration, unit, range };
+ }
+ getFallbackDuration() {
+ return createDuration({ day: 1 });
+ }
+ // Returns a new activeRange to have time values (un-ambiguate)
+ // slotMinTime or slotMaxTime causes the range to expand.
+ adjustActiveRange(range) {
+ let { dateEnv, usesMinMaxTime, slotMinTime, slotMaxTime } = this.props;
+ let { start, end } = range;
+ if (usesMinMaxTime) {
+ // expand active range if slotMinTime is negative (why not when positive?)
+ if (asRoughDays(slotMinTime) < 0) {
+ start = startOfDay(start); // necessary?
+ start = dateEnv.add(start, slotMinTime);
+ }
+ // expand active range if slotMaxTime is beyond one day (why not when negative?)
+ if (asRoughDays(slotMaxTime) > 1) {
+ end = startOfDay(end); // necessary?
+ end = addDays(end, -1);
+ end = dateEnv.add(end, slotMaxTime);
+ }
+ }
+ return { start, end };
+ }
+ // Builds the "current" range when it is specified as an explicit duration.
+ // `unit` is the already-computed greatestDurationDenominator unit of duration.
+ buildRangeFromDuration(date, direction, duration, unit) {
+ let { dateEnv, dateAlignment } = this.props;
+ let start;
+ let end;
+ let res;
+ // compute what the alignment should be
+ if (!dateAlignment) {
+ let { dateIncrement } = this.props;
+ if (dateIncrement) {
+ // use the smaller of the two units
+ if (asRoughMs(dateIncrement) < asRoughMs(duration)) {
+ dateAlignment = greatestDurationDenominator(dateIncrement).unit;
+ }
+ else {
+ dateAlignment = unit;
+ }
+ }
+ else {
+ dateAlignment = unit;
+ }
+ }
+ // if the view displays a single day or smaller
+ if (asRoughDays(duration) <= 1) {
+ if (this.isHiddenDay(start)) {
+ start = this.skipHiddenDays(start, direction);
+ start = startOfDay(start);
+ }
+ }
+ function computeRes() {
+ start = dateEnv.startOf(date, dateAlignment);
+ end = dateEnv.add(start, duration);
+ res = { start, end };
+ }
+ computeRes();
+ // if range is completely enveloped by hidden days, go past the hidden days
+ if (!this.trimHiddenDays(res)) {
+ date = this.skipHiddenDays(date, direction);
+ computeRes();
+ }
+ return res;
+ }
+ // Builds the "current" range when a dayCount is specified.
+ buildRangeFromDayCount(date, direction, dayCount) {
+ let { dateEnv, dateAlignment } = this.props;
+ let runningCount = 0;
+ let start = date;
+ let end;
+ if (dateAlignment) {
+ start = dateEnv.startOf(start, dateAlignment);
+ }
+ start = startOfDay(start);
+ start = this.skipHiddenDays(start, direction);
+ end = start;
+ do {
+ end = addDays(end, 1);
+ if (!this.isHiddenDay(end)) {
+ runningCount += 1;
+ }
+ } while (runningCount < dayCount);
+ return { start, end };
+ }
+ // Builds a normalized range object for the "visible" range,
+ // which is a way to define the currentRange and activeRange at the same time.
+ buildCustomVisibleRange(date) {
+ let { props } = this;
+ let input = props.visibleRangeInput;
+ let simpleInput = typeof input === 'function'
+ ? input.call(props.calendarApi, props.dateEnv.toDate(date))
+ : input;
+ let range = this.refineRange(simpleInput);
+ if (range && (range.start == null || range.end == null)) {
+ return null;
+ }
+ return range;
+ }
+ // Computes the range that will represent the element/cells for *rendering*,
+ // but which may have voided days/times.
+ // not responsible for trimming hidden days.
+ buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) {
+ return currentRange;
+ }
+ // Compute the duration value that should be added/substracted to the current date
+ // when a prev/next operation happens.
+ buildDateIncrement(fallback) {
+ let { dateIncrement } = this.props;
+ let customAlignment;
+ if (dateIncrement) {
+ return dateIncrement;
+ }
+ if ((customAlignment = this.props.dateAlignment)) {
+ return createDuration(1, customAlignment);
+ }
+ if (fallback) {
+ return fallback;
+ }
+ return createDuration({ days: 1 });
+ }
+ refineRange(rangeInput) {
+ if (rangeInput) {
+ let range = parseRange(rangeInput, this.props.dateEnv);
+ if (range) {
+ range = computeVisibleDayRange(range);
+ }
+ return range;
+ }
+ return null;
+ }
+ /* Hidden Days
+ ------------------------------------------------------------------------------------------------------------------*/
+ // Initializes internal variables related to calculating hidden days-of-week
+ initHiddenDays() {
+ let hiddenDays = this.props.hiddenDays || []; // array of day-of-week indices that are hidden
+ let isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
+ let dayCnt = 0;
+ let i;
+ if (this.props.weekends === false) {
+ hiddenDays.push(0, 6); // 0=sunday, 6=saturday
+ }
+ for (i = 0; i < 7; i += 1) {
+ if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) {
+ dayCnt += 1;
+ }
+ }
+ if (!dayCnt) {
+ throw new Error('invalid hiddenDays'); // all days were hidden? bad.
+ }
+ this.isHiddenDayHash = isHiddenDayHash;
+ }
+ // Remove days from the beginning and end of the range that are computed as hidden.
+ // If the whole range is trimmed off, returns null
+ trimHiddenDays(range) {
+ let { start, end } = range;
+ if (start) {
+ start = this.skipHiddenDays(start);
+ }
+ if (end) {
+ end = this.skipHiddenDays(end, -1, true);
+ }
+ if (start == null || end == null || start < end) {
+ return { start, end };
+ }
+ return null;
+ }
+ // Is the current day hidden?
+ // `day` is a day-of-week index (0-6), or a Date (used for UTC)
+ isHiddenDay(day) {
+ if (day instanceof Date) {
+ day = day.getUTCDay();
+ }
+ return this.isHiddenDayHash[day];
+ }
+ // Incrementing the current day until it is no longer a hidden day, returning a copy.
+ // DOES NOT CONSIDER validRange!
+ // If the initial value of `date` is not a hidden day, don't do anything.
+ // Pass `isExclusive` as `true` if you are dealing with an end date.
+ // `inc` defaults to `1` (increment one day forward each time)
+ skipHiddenDays(date, inc = 1, isExclusive = false) {
+ while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) {
+ date = addDays(date, inc);
+ }
+ return date;
+ }
+ }
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function computeMajorUnit(dateProfile, dateEnv) {
+ const { currentRange } = dateProfile;
+ if (dateProfile.currentRangeUnit === 'year') {
+ if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) {
+ return 'year';
+ }
+ else {
+ return 'month';
+ }
+ }
+ else if (dateProfile.currentRangeUnit === 'month') {
+ if (dateEnv.diffWholeMonths(currentRange.start, currentRange.end) > 1) {
+ return 'month';
+ }
+ }
+ else if (dateProfile.currentRangeUnit === 'week') {
+ if (diffWholeWeeks(currentRange.start, currentRange.end) > 1) {
+ return 'week';
+ }
+ }
+ else if (dateProfile.currentRangeUnit === 'day') {
+ if (diffWholeDays(currentRange.start, currentRange.end) > 1) {
+ return 'day';
+ }
+ }
+ }
+ function isMajorUnit(dateMarker, majorUnit, dateEnv) {
+ const isStartOfDay = dateMarker.valueOf() === startOfDay(dateMarker).valueOf();
+ if (isStartOfDay) {
+ if (majorUnit === 'year') {
+ return !dateEnv.getMonth(dateMarker) && dateEnv.getDay(dateMarker) === 1;
+ }
+ else if (majorUnit === 'month') {
+ return dateEnv.getDay(dateMarker) === 1;
+ }
+ else if (majorUnit === 'week') {
+ return dateMarker.getUTCDay() === dateEnv.weekDow;
+ }
+ else if (majorUnit === 'day') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function reduceEventStore(eventStore, action, eventSources, dateProfile, context) {
+ switch (action.type) {
+ case 'RECEIVE_EVENTS': // raw
+ return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, context);
+ case 'RESET_RAW_EVENTS':
+ return resetRawEvents(eventStore, eventSources[action.sourceId], action.rawEvents, dateProfile.activeRange, context);
+ case 'ADD_EVENTS': // already parsed, but not expanded
+ return addEvent(eventStore, action.eventStore, // new ones
+ dateProfile ? dateProfile.activeRange : null, context);
+ case 'RESET_EVENTS':
+ return action.eventStore;
+ case 'MERGE_EVENTS': // already parsed and expanded
+ return mergeEventStores(eventStore, action.eventStore);
+ case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
+ case 'NEXT':
+ case 'CHANGE_DATE':
+ case 'CHANGE_VIEW_TYPE':
+ if (dateProfile) {
+ return expandRecurring(eventStore, dateProfile.activeRange, context);
+ }
+ return eventStore;
+ case 'REMOVE_EVENTS':
+ return excludeSubEventStore(eventStore, action.eventStore);
+ case 'REMOVE_EVENT_SOURCE':
+ return excludeEventsBySourceId(eventStore, action.sourceId);
+ case 'REMOVE_ALL_EVENT_SOURCES':
+ return filterEventStoreDefs(eventStore, (eventDef) => (!eventDef.sourceId // only keep events with no source id
+ ));
+ case 'REMOVE_ALL_EVENTS':
+ return createEmptyEventStore();
+ default:
+ return eventStore;
+ }
+ }
+ function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, context) {
+ if (eventSource && // not already removed
+ fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
+ ) {
+ let subset = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context);
+ if (fetchRange) {
+ subset = expandRecurring(subset, fetchRange, context);
+ }
+ return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset);
+ }
+ return eventStore;
+ }
+ function resetRawEvents(existingEventStore, eventSource, rawEvents, activeRange, context) {
+ const { defIdMap, instanceIdMap } = buildPublicIdMaps(existingEventStore);
+ let newEventStore = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context, false, defIdMap, instanceIdMap);
+ return expandRecurring(newEventStore, activeRange, context);
+ }
+ function transformRawEvents(rawEvents, eventSource, context) {
+ let calEachTransform = context.options.eventDataTransform;
+ let sourceEachTransform = eventSource ? eventSource.eventDataTransform : null;
+ if (sourceEachTransform) {
+ rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform);
+ }
+ if (calEachTransform) {
+ rawEvents = transformEachRawEvent(rawEvents, calEachTransform);
+ }
+ return rawEvents;
+ }
+ function transformEachRawEvent(rawEvents, func) {
+ let refinedEvents;
+ if (!func) {
+ refinedEvents = rawEvents;
+ }
+ else {
+ refinedEvents = [];
+ for (let rawEvent of rawEvents) {
+ let refinedEvent = func(rawEvent);
+ if (refinedEvent) {
+ refinedEvents.push(refinedEvent);
+ }
+ else if (refinedEvent == null) {
+ refinedEvents.push(rawEvent);
+ } // if a different falsy value, do nothing
+ }
+ }
+ return refinedEvents;
+ }
+ function addEvent(eventStore, subset, expandRange, context) {
+ if (expandRange) {
+ subset = expandRecurring(subset, expandRange, context);
+ }
+ return mergeEventStores(eventStore, subset);
+ }
+ function rezoneEventStoreDates(eventStore, oldDateEnv, newDateEnv) {
+ let { defs } = eventStore;
+ let instances = mapHash(eventStore.instances, (instance) => {
+ let def = defs[instance.defId];
+ if (def.allDay) {
+ return instance; // isn't dependent on timezone
+ }
+ return {
+ ...instance,
+ range: {
+ start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start)),
+ end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end)),
+ },
+ };
+ });
+ return { defs, instances };
+ }
+ function excludeEventsBySourceId(eventStore, sourceId) {
+ return filterEventStoreDefs(eventStore, (eventDef) => eventDef.sourceId !== sourceId);
+ }
+ // QUESTION: why not just return instances? do a general object-property-exclusion util
+ function excludeInstances(eventStore, removals) {
+ return {
+ defs: eventStore.defs,
+ instances: filterHash(eventStore.instances, (instance) => !removals[instance.instanceId]),
+ };
+ }
+ function buildPublicIdMaps(eventStore) {
+ const { defs, instances } = eventStore;
+ const defIdMap = {};
+ const instanceIdMap = {};
+ for (let defId in defs) {
+ const def = defs[defId];
+ const { publicId } = def;
+ if (publicId) {
+ defIdMap[publicId] = defId;
+ }
+ }
+ for (let instanceId in instances) {
+ const instance = instances[instanceId];
+ const def = defs[instance.defId];
+ const { publicId } = def;
+ if (publicId) {
+ instanceIdMap[publicId] = instanceId;
+ }
+ }
+ return { defIdMap, instanceIdMap };
+ }
+
+ class Interaction {
+ constructor(settings) {
+ this.component = settings.component;
+ this.isHitComboAllowed = settings.isHitComboAllowed || null;
+ }
+ destroy() {
+ }
+ }
+ function parseInteractionSettings(component, input) {
+ return {
+ component,
+ el: input.el,
+ useEventCenter: input.useEventCenter != null ? input.useEventCenter : true,
+ isHitComboAllowed: input.isHitComboAllowed || null,
+ };
+ }
+ function interactionSettingsToStore(settings) {
+ return {
+ [settings.component.uid]: settings,
+ };
+ }
+ // global state
+ const interactionSettingsStore = {};
+
+ class Emitter {
+ constructor() {
+ this.handlers = {};
+ this.thisContext = null;
+ }
+ setThisContext(thisContext) {
+ this.thisContext = thisContext;
+ }
+ setOptions(options) {
+ this.options = options;
+ }
+ on(type, handler) {
+ addToHash(this.handlers, type, handler);
+ }
+ off(type, handler) {
+ removeFromHash(this.handlers, type, handler);
+ }
+ trigger(type, ...args) {
+ let attachedHandlers = this.handlers[type] || [];
+ let optionHandler = this.options && this.options[type];
+ let handlers = [].concat(optionHandler || [], attachedHandlers);
+ for (let handler of handlers) {
+ handler.apply(this.thisContext, args);
+ }
+ }
+ hasHandlers(type) {
+ return Boolean((this.handlers[type] && this.handlers[type].length) ||
+ (this.options && this.options[type]));
+ }
+ }
+ function addToHash(hash, type, handler) {
+ (hash[type] || (hash[type] = []))
+ .push(handler);
+ }
+ function removeFromHash(hash, type, handler) {
+ if (handler) {
+ if (hash[type]) {
+ hash[type] = hash[type].filter((func) => func !== handler);
+ }
+ }
+ else {
+ delete hash[type]; // remove all handler funcs for this type
+ }
+ }
+
+ // TODO: easier way to add new hooks? need to update a million things
+ function refinePluginDef(input) {
+ return {
+ name: input.name,
+ premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
+ 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 || [],
+ eventSourceDefs: input.eventSourceDefs || [],
+ cmdFormatter: input.cmdFormatter,
+ recurringTypes: input.recurringTypes || [],
+ initialView: input.initialView || '',
+ elementDraggingImpl: input.elementDraggingImpl,
+ optionChangeHandlers: input.optionChangeHandlers || {},
+ scrollerSyncerClass: input.scrollerSyncerClass || null,
+ listenerRefiners: input.listenerRefiners || {},
+ optionRefiners: input.optionRefiners || {},
+ optionDefaults: input.optionDefaults ? [input.optionDefaults] : [],
+ propSetHandlers: input.propSetHandlers || {},
+ };
+ }
+ function buildPluginHooks(pluginDefs, globalDefs) {
+ let pluginsByName = {};
+ 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: [],
+ eventSourceDefs: [],
+ cmdFormatter: null,
+ recurringTypes: [],
+ initialView: '',
+ elementDraggingImpl: null,
+ optionChangeHandlers: {},
+ scrollerSyncerClass: null,
+ listenerRefiners: {},
+ optionRefiners: {},
+ optionDefaults: [],
+ propSetHandlers: {},
+ };
+ /*
+ IDs/names, etc
+ */
+ function addDefs(defs) {
+ for (let unrefinedDef of defs) {
+ const { name } = unrefinedDef;
+ if (!name) {
+ throw new Error('Plugin must specify a name');
+ }
+ if (!pluginsByName[name]) {
+ const def = pluginsByName[name] = refinePluginDef(unrefinedDef);
+ hooks = combineHooks(hooks, def);
+ addDefs(unrefinedDef.deps || []);
+ }
+ }
+ }
+ if (pluginDefs) { // how could this be undefined?
+ addDefs(pluginDefs);
+ }
+ addDefs(globalDefs); // GLOBAL plugins
+ 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: { ...hooks0.eventRefiners, ...hooks1.eventRefiners },
+ eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
+ eventSourceRefiners: { ...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: mergeViewOptionsMap(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),
+ eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
+ cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
+ recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
+ initialView: hooks0.initialView || hooks1.initialView, // put earlier plugins FIRST
+ elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
+ optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
+ scrollerSyncerClass: hooks0.scrollerSyncerClass || hooks1.scrollerSyncerClass,
+ listenerRefiners: { ...hooks0.listenerRefiners, ...hooks1.listenerRefiners },
+ optionRefiners: { ...hooks0.optionRefiners, ...hooks1.optionRefiners },
+ optionDefaults: hooks0.optionDefaults.concat(hooks1.optionDefaults),
+ propSetHandlers: { ...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()));
+ }
+
+ 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: mergeCalendarOptions(superDef ? superDef.defaults : {}, defaultConfig ? defaultConfig.rawOptions : {}),
+ overrides: mergeCalendarOptions(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) {
+ component = createViewHookComponent(rawOptions.content);
+ }
+ else if (component && !(component.prototype instanceof BaseComponent)) {
+ // WHY?: people were using `component` property for `content`
+ // TODO: converge on one setting name
+ component = createViewHookComponent(component);
+ }
+ return {
+ superType: rawOptions.type,
+ component: component,
+ rawOptions, // includes type and component too :(
+ };
+ }
+ /*
+ TODO: converge with ViewContainer
+ */
+ function createViewHookComponent(contentGenerator) {
+ return (viewProps) => (u$1(ViewContextType.Consumer, { children: (context) => {
+ const { options, viewSpec } = context;
+ const renderProps = {
+ // the "extra" props, for sliceEvents...
+ ...viewProps,
+ nextDayThreshold: options.nextDayThreshold,
+ // ViewDisplayInfo...
+ ...computeViewBorderless(options),
+ options: { headerToolbar: options.headerToolbar, footerToolbar: options.footerToolbar },
+ isHeightAuto: getIsHeightAuto(options),
+ view: context.viewApi,
+ };
+ return (u$1(ContentContainer, { tag: "div", className: joinClassNames(generateClassName(options.viewClass, renderProps),
+ // WORKAROUND for way calendar's className would get merged into view's className
+ generateClassName(viewSpec.optionDefaults.class, renderProps), generateClassName(viewSpec.optionDefaults.className, renderProps), generateClassName(viewSpec.optionOverrides.class, renderProps), generateClassName(viewSpec.optionOverrides.className, renderProps)), renderProps: renderProps, generatorName: undefined, customGenerator: contentGenerator, didMount: options.didMount || options.viewDidMount, willUnmount: options.willUnmount || options.viewWillUnmount }));
+ } }));
+ }
+
+ function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides) {
+ let defaultConfigs = parseViewConfigs(defaultInputs);
+ let overrideConfigs = parseViewConfigs(optionOverrides.views);
+ let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
+ return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides));
+ }
+ function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides) {
+ 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 : {};
+ }
+ }
+ }
+ return {
+ type: viewDef.type,
+ component: viewDef.component,
+ duration,
+ durationUnit,
+ singleUnit,
+ optionDefaults: viewDef.defaults,
+ optionOverrides: { ...singleUnitOverrides, ...viewDef.overrides },
+ };
+ }
+ // 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 { ...dynamicOptionOverrides, [action.optionName]: action.rawOptionValue };
+ default:
+ return dynamicOptionOverrides;
+ }
+ }
+
+ function reduceDateProfile(currentDateProfile, action, currentDate, nowDate, dateProfileGenerator) {
+ let dp;
+ switch (action.type) {
+ case 'CHANGE_VIEW_TYPE':
+ return dateProfileGenerator.build(action.dateMarker || currentDate, nowDate);
+ case 'CHANGE_DATE':
+ return dateProfileGenerator.build(action.dateMarker, nowDate);
+ case 'PREV':
+ dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate, nowDate);
+ if (dp.isValid) {
+ return dp;
+ }
+ break;
+ case 'NEXT':
+ dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate, nowDate);
+ if (dp.isValid) {
+ return dp;
+ }
+ break;
+ }
+ return currentDateProfile;
+ }
+
+ 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, viewSpecs, calendarApi) {
+ let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, viewSpecs, calendarApi) : null;
+ let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, viewSpecs, calendarApi) : null;
+ return { header, footer };
+ }
+ function parseToolbar(sectionStrHash, calendarOptions, viewSpecs, calendarApi) {
+ let isRtl = calendarOptions.direction === 'rtl';
+ let viewsWithButtons = [];
+ let hasTitle = false;
+ function processSectionStr(sectionStr) {
+ let sectionRes = parseSection(sectionStr, calendarOptions, viewSpecs, calendarApi);
+ viewsWithButtons.push(...sectionRes.viewsWithButtons);
+ hasTitle = hasTitle || sectionRes.hasTitle;
+ return sectionRes.widgets;
+ }
+ const sectionWidgets = {
+ start: processSectionStr(sectionStrHash[isRtl ? 'right' : 'left'] || sectionStrHash.start || ''),
+ center: processSectionStr(sectionStrHash.center || ''),
+ end: processSectionStr(sectionStrHash[isRtl ? 'left' : 'right'] || sectionStrHash.end || ''),
+ };
+ return {
+ sectionWidgets,
+ viewsWithButtons,
+ hasTitle,
+ };
+ }
+ /*
+ BAD: querying icons and text here. should be done at render time
+ */
+ function parseSection(sectionStr, calendarOptions, viewSpecs, calendarApi) {
+ let calendarButtons = calendarOptions.buttons || {};
+ let customElements = calendarOptions.toolbarElements || {};
+ let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
+ let viewsWithButtons = [];
+ let hasTitle = false;
+ let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((name) => {
+ if (name === 'title') {
+ hasTitle = true;
+ return { name };
+ }
+ if (customElements[name]) {
+ return { name, customElement: customElements[name] };
+ }
+ let viewSpec;
+ let buttonInput = calendarButtons[name] || {};
+ let buttonText;
+ let buttonHint;
+ let buttonClick;
+ if ((viewSpec = viewSpecs[name])) {
+ viewsWithButtons.push(name);
+ const buttonTextKey = viewSpec.optionDefaults.buttonTextKey;
+ buttonText = buttonInput.text ||
+ (buttonTextKey ? calendarOptions[buttonTextKey] : '') ||
+ (viewSpec.singleUnit
+ ? (calendarOptions[viewSpec.singleUnit + 'TextLong'] ||
+ calendarOptions[viewSpec.singleUnit + 'Text'])
+ : '') ||
+ name;
+ /*
+ buttons{}.hint(viewButtonText, viewName)
+ viewHint(viewButtonText, viewName)
+ */
+ buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.viewHint, [buttonText, name], // ordinal arguments
+ buttonText);
+ buttonClick = (ev) => {
+ buttonInput?.click?.(ev);
+ if (!ev.defaultPrevented) {
+ calendarApi.changeView(name);
+ }
+ };
+ }
+ else {
+ buttonText = buttonInput.text ||
+ calendarOptions[name + 'TextLong'] ||
+ calendarOptions[name + 'Text'] ||
+ name;
+ /*
+ buttons{}.hint(currentUnitText, currentUnit)
+ prevHint(currentUnitUnitext, currentUnit)
+ nextHint -- same
+ todayHint -- same
+ */
+ if (name === 'prevYear') {
+ buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.prevHint, [calendarOptions.yearText, 'year'], buttonText);
+ }
+ else if (name === 'nextYear') {
+ buttonHint = formatWithOrdinals(buttonInput.hint || calendarOptions.nextHint, [calendarOptions.yearText, 'year'], buttonText);
+ }
+ else {
+ buttonHint = (currentUnit) => {
+ return formatWithOrdinals(buttonInput.hint || calendarOptions[name + 'Hint'], // todayHint/prevHint/nextHint
+ [
+ calendarOptions[currentUnit + 'TextLong'] ||
+ calendarOptions[currentUnit + 'Text'],
+ currentUnit
+ ], buttonText);
+ };
+ }
+ buttonClick = (ev) => {
+ buttonInput?.click?.(ev);
+ if (!ev.defaultPrevented) {
+ calendarApi[name]?.();
+ }
+ };
+ }
+ return {
+ name,
+ isView: Boolean(viewSpec),
+ buttonText,
+ buttonHint,
+ buttonDisplay: buttonInput.display,
+ buttonIconClass: buttonInput.iconClass,
+ buttonIconContent: buttonInput.iconContent,
+ buttonClick,
+ buttonIsPrimary: buttonInput.isPrimary || false,
+ buttonClass: buttonInput.class ?? buttonInput.className,
+ buttonDidMount: buttonInput.didMount,
+ buttonWillUnmount: buttonInput.willUnmount,
+ };
+ })));
+ 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
+ }
+ }
+
+ const DEF_DEFAULTS = {
+ startTime: '09:00',
+ endTime: '17:00',
+ daysOfWeek: [1, 2, 3, 4, 5], // monday - friday
+ display: 'inverse-background',
+ className: '', // TODO: remove
+ groupId: '_businessHours', // so multiple defs get grouped
+ };
+ /*
+ TODO: pass around as EventDefHash!!!
+ */
+ function parseBusinessHours(input, context) {
+ return parseEvents(refineInputs(input), null, context);
+ }
+ function refineInputs(input) {
+ let rawDefs;
+ if (input === true) {
+ rawDefs = [{}]; // will get DEF_DEFAULTS verbatim
+ }
+ else if (Array.isArray(input)) {
+ // if specifying an array, every sub-definition NEEDS a day-of-week
+ rawDefs = input.filter((rawDef) => rawDef.daysOfWeek);
+ }
+ else if (typeof input === 'object' && input) { // non-null object
+ rawDefs = [input];
+ }
+ else { // is probably false
+ rawDefs = [];
+ }
+ rawDefs = rawDefs.map((rawDef) => ({ ...DEF_DEFAULTS, ...rawDef }));
+ return rawDefs;
+ }
+
+ // 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;
+ }
+ let parts;
+ const options = { isEndExclusive: dateProfile.isRangeAllDay };
+ if (viewOptions.titleFormat) {
+ parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(viewOptions.titleFormat), options);
+ }
+ else {
+ parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(buildTitleFormat(dateProfile, viewOptions.disallowAmbigTitle, 'long')), options);
+ if (hasTwoMonths(parts)) {
+ parts = dateEnv.formatRangeToParts(range.start, range.end, createFormatter(buildTitleFormat(dateProfile, viewOptions.disallowAmbigTitle, 'short')), options);
+ }
+ }
+ return joinDateTimeFormatParts(parts);
+ }
+ // 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, disallowAmbigTitle, monthFormat) {
+ const { currentRangeUnit } = dateProfile;
+ if (currentRangeUnit === 'year') {
+ return { year: 'numeric' };
+ }
+ if (currentRangeUnit === 'month') {
+ return { year: 'numeric', month: monthFormat };
+ }
+ if (!disallowAmbigTitle) {
+ const days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
+ if (days !== null && days > 1) {
+ return {
+ year: 'numeric',
+ month: monthFormat,
+ };
+ }
+ }
+ // one day. longer, like "September 9 2014"
+ return { year: 'numeric', month: 'long', day: 'numeric' };
+ }
+ function hasTwoMonths(parts) {
+ let hasStartMonth = false;
+ let hasEndMonth = false;
+ for (const part of parts) {
+ if (part.type === 'month') {
+ if (part.source === 'startRange')
+ hasStartMonth = true;
+ if (part.source === 'endRange')
+ hasEndMonth = true;
+ }
+ }
+ return hasStartMonth && hasEndMonth;
+ }
+
+ /*
+ 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);
+ }
+ }
+
+ class CalendarDataManager {
+ constructor(config) {
+ this.computeCurrentViewData = memoize(this._computeCurrentViewData);
+ this.organizeRawLocales = memoize(organizeRawLocales);
+ this.buildLocale = memoize(buildLocale);
+ this.buildPluginHooks = buildBuildPluginHooks();
+ this.buildDateEnv = memoize(buildDateEnv$1);
+ this.parseToolbars = memoize(parseToolbars);
+ this.buildViewSpecs = memoize(buildViewSpecs);
+ this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator);
+ this.buildViewApi = memoize(buildViewApi);
+ this.buildViewUiProps = memoizeObjArg(buildViewUiProps);
+ this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqualShallow);
+ this.buildEventUiBases = memoize(buildEventUiBases);
+ this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours);
+ this.buildToolbarProps = memoize(buildToolbarProps);
+ this.buildTitle = memoize(buildTitle);
+ this.nowManager = new CalendarNowManager();
+ this.isDrainingActionQueue = false;
+ this.actionQueue = [];
+ this.optionOverrides = {};
+ // used by CalendarApiImpl
+ this.emitter = new Emitter();
+ this.currentCalendarOptionsRefiners = {};
+ this.currentCalendarOptionsInput = {};
+ this.currentCalendarOptionsRefined = {};
+ this.currentViewOptionsInput = {};
+ this.currentViewOptionsRefined = {};
+ this.optionsForRefining = [];
+ this.optionsForHandling = [];
+ this.getCurrentData = () => this.data;
+ this.handleNowChange = () => {
+ this.dispatch({ type: 'UPDATE_NOW' });
+ };
+ this.dispatch = (action) => {
+ this.actionQueue.push(action);
+ if (!this.isDrainingActionQueue) {
+ this.drainActionQueue();
+ }
+ };
+ this.config = config;
+ this.nowManager = new CalendarNowManager();
+ this.nowTimer = new NowTimerRunner(this.handleNowChange);
+ }
+ destroy() {
+ this.nowTimer.destroy();
+ }
+ /*
+ Will NOT trigger onDataChange unless there were other actions in the queue
+ */
+ update(optionOverrides) {
+ this.optionOverrides = optionOverrides;
+ this.actionQueue.push({ type: 'IDLE' }); // ensure reducer gets called
+ this.drainActionQueue();
+ return this.data;
+ }
+ /*
+ WILL trigger onDataChange
+ */
+ resetOptions(optionOverrides, changedOptionNames) {
+ if (changedOptionNames === undefined) {
+ this.optionOverrides = optionOverrides;
+ }
+ else {
+ this.optionOverrides = { ...this.optionOverrides, ...optionOverrides };
+ this.optionsForRefining.push(...changedOptionNames);
+ }
+ this.dispatch({ type: 'RESET_OPTIONS' });
+ }
+ drainActionQueue() {
+ let calendarContext;
+ let { state, data } = this;
+ const isInit = !state;
+ const { actionQueue } = this;
+ const actionsComplete = []; // non-idle
+ this.isDrainingActionQueue = true;
+ while (actionQueue.length) {
+ const action = actionQueue.shift();
+ ({ state, data, calendarContext } = this.reduce(state, data, action));
+ this.state = state;
+ this.data = data;
+ if (action.type !== 'IDLE') {
+ actionsComplete.push(action);
+ }
+ }
+ this.isDrainingActionQueue = false;
+ if (isInit) {
+ const controllerOption = calendarContext.options.controller;
+ if (controllerOption) {
+ controllerOption._setApi(this.config.calendarApi);
+ }
+ }
+ if (!isInit && actionsComplete.length) {
+ const { onDataChange } = this.config;
+ if (onDataChange) {
+ onDataChange(this.data, actionsComplete);
+ }
+ }
+ }
+ reduce(prevState, prevData, action) {
+ let { config } = this;
+ let isInit = !prevState;
+ // === Compute options and view data ===
+ let dynamicOptionOverrides = isInit
+ ? {}
+ : reduceDynamicOptionOverrides(prevState.dynamicOptionOverrides, action);
+ let optionsData = this.computeOptionsData(this.optionOverrides, dynamicOptionOverrides, config.calendarApi);
+ let currentViewType = isInit
+ ? (optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView)
+ : reduceViewType(prevState.currentViewType, action);
+ let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, this.optionOverrides, dynamicOptionOverrides);
+ // === Wire things up ===
+ config.calendarApi.currentDataManager = this;
+ this.emitter.setThisContext(config.calendarApi);
+ this.emitter.setOptions(currentViewData.options);
+ // === Build calendarContext ===
+ let calendarContext = {
+ nowManager: this.nowManager,
+ dateEnv: optionsData.dateEnv,
+ options: optionsData.calendarOptions,
+ pluginHooks: optionsData.pluginHooks,
+ calendarApi: config.calendarApi,
+ dispatch: this.dispatch,
+ emitter: this.emitter,
+ getCurrentData: this.getCurrentData,
+ };
+ // === Update now timer ===
+ let { nowDate } = this.nowTimer.update({
+ unit: 'day',
+ unitValue: 1,
+ nowIndicatorSnap: 'auto',
+ nowManager: this.nowManager,
+ dateEnv: optionsData.dateEnv,
+ });
+ // === Compute currentDate ===
+ let currentDate = isInit
+ ? getInitialDate(optionsData.calendarOptions, optionsData.dateEnv, this.nowManager)
+ : reduceCurrentDate(prevState.currentDate, action);
+ // === Compute dateProfile ===
+ let dateProfile;
+ if (isInit) {
+ dateProfile = currentViewData.dateProfileGenerator.build(currentDate, nowDate);
+ }
+ else {
+ dateProfile = prevState.dateProfile;
+ // Check for generator change
+ if (prevData && prevData.dateProfileGenerator !== currentViewData.dateProfileGenerator) {
+ dateProfile = currentViewData.dateProfileGenerator.build(currentDate, nowDate);
+ }
+ dateProfile = reduceDateProfile(dateProfile, action, currentDate, nowDate, currentViewData.dateProfileGenerator);
+ }
+ // === Adjust currentDate if out of range ===
+ if ((action && (action.type === 'PREV' || action.type === 'NEXT')) ||
+ !rangeContainsMarker(dateProfile.activeRange, currentDate)) {
+ currentDate = dateProfile.currentRange.start;
+ }
+ // === Compute eventSources, eventStore ===
+ let eventSources = isInit
+ ? initEventSources(optionsData.calendarOptions, dateProfile, calendarContext)
+ : reduceEventSources(prevState.eventSources, action, dateProfile, calendarContext);
+ let eventStore = isInit
+ ? createEmptyEventStore()
+ : reduceEventStore(prevState.eventStore, action, eventSources, dateProfile, calendarContext);
+ // === Compute renderableEventStore ===
+ let isEventsLoading = computeEventSourcesLoading(eventSources);
+ let renderableEventStore = isInit
+ ? createEmptyEventStore()
+ : (isEventsLoading && !currentViewData.options.progressiveEventRendering)
+ ? (prevState.renderableEventStore || eventStore)
+ : eventStore;
+ // === UI computation ===
+ let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext);
+ let eventUiBySource = this.buildEventUiBySource(eventSources);
+ let eventUiBases = isInit
+ ? {}
+ : this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
+ // === Build new state ===
+ let newState = {
+ dynamicOptionOverrides,
+ currentViewType,
+ currentDate,
+ dateProfile,
+ eventSources,
+ eventStore,
+ renderableEventStore,
+ selectionConfig,
+ eventUiBases,
+ businessHours: this.parseContextBusinessHours(calendarContext),
+ dateSelection: isInit ? null : reduceDateSelection(prevState.dateSelection, action),
+ eventSelection: isInit ? '' : reduceSelectedEvent(prevState.eventSelection, action),
+ eventDrag: isInit ? null : reduceEventDrag(prevState.eventDrag, action),
+ eventResize: isInit ? null : reduceEventResize(prevState.eventResize, action),
+ nowDate,
+ };
+ // === Plugin reducers ===
+ let contextAndState = { ...calendarContext, ...newState };
+ for (let reducer of optionsData.pluginHooks.reducers) {
+ Object.assign(newState, reducer(prevState, action, contextAndState));
+ }
+ // === Loading state emission ===
+ let wasLoading = prevState ? computeIsLoading(prevState, calendarContext) : false;
+ let isLoading = computeIsLoading(newState, calendarContext);
+ if (!wasLoading && isLoading) {
+ this.emitter.trigger('loading', true);
+ }
+ else if (wasLoading && !isLoading) {
+ this.emitter.trigger('loading', false);
+ }
+ // === Build CalendarData ===
+ let viewTitle = this.buildTitle(dateProfile, currentViewData.options, optionsData.dateEnv);
+ let toolbarProps = this.buildToolbarProps(currentViewData.viewSpec, dateProfile, currentViewData.dateProfileGenerator, currentDate, nowDate, viewTitle);
+ let newData = {
+ viewTitle,
+ nowManager: this.nowManager,
+ calendarApi: config.calendarApi,
+ dispatch: this.dispatch,
+ emitter: this.emitter,
+ getCurrentData: this.getCurrentData,
+ toolbarProps,
+ ...optionsData,
+ ...currentViewData,
+ ...newState,
+ };
+ // === Handle option changes ===
+ let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
+ let prevCalendarOptions = prevData && prevData.calendarOptions;
+ let newCalendarOptions = optionsData.calendarOptions;
+ if (prevCalendarOptions && prevCalendarOptions !== newCalendarOptions) {
+ if (prevCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
+ // HACK
+ newState.eventSources = newData.eventSources = reduceEventSourcesNewTimeZone(newData.eventSources, dateProfile, newData);
+ newState.eventStore = newData.eventStore = rezoneEventStoreDates(newData.eventStore, prevData.dateEnv, newData.dateEnv);
+ newState.renderableEventStore = newData.renderableEventStore = rezoneEventStoreDates(newData.renderableEventStore, prevData.dateEnv, newData.dateEnv);
+ }
+ for (let optionName in changeHandlers) {
+ if (this.optionsForHandling.indexOf(optionName) !== -1 ||
+ prevCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
+ changeHandlers[optionName](newCalendarOptions[optionName], newData);
+ }
+ }
+ }
+ this.optionsForHandling = [];
+ return { state: newState, data: newData, calendarContext };
+ }
+ 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, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
+ let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekTextLong, refinedOptions.weekTextShort, pluginHooks, availableLocaleData);
+ let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides);
+ let toolbarConfig = this.parseToolbars(refinedOptions, viewSpecs, calendarApi);
+ return this.stableCalendarOptionsData = {
+ calendarOptions: refinedOptions,
+ pluginHooks,
+ dateEnv,
+ viewSpecs,
+ toolbarConfig,
+ localeDefaults,
+ availableRawLocales: availableLocaleData.map,
+ };
+ }
+ // always called from behind a memoizer
+ processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
+ let { locales, locale } = mergeCalendarOptions(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 = {
+ ...BASE_OPTION_REFINERS,
+ ...CALENDAR_LISTENER_REFINERS,
+ ...CALENDAR_ONLY_OPTION_REFINERS,
+ ...pluginHooks.listenerRefiners,
+ ...pluginHooks.optionRefiners,
+ };
+ let raw = mergeCalendarOptions(BASE_OPTION_DEFAULTS, ...pluginHooks.optionDefaults, localeDefaults, filterKnownOptions(mergeCalendarOptions(optionOverrides, dynamicOptionOverrides), refiners));
+ 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])) || isMergedPropsEqual(currentRaw[optionName], raw[optionName]))) {
+ refined[optionName] = currentRefined[optionName];
+ }
+ else if (refiners[optionName]) {
+ refined[optionName] = refiners[optionName](raw[optionName], optionName);
+ anyChanges = true;
+ }
+ }
+ 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,
+ };
+ }
+ _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 } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
+ this.nowManager.handleInput(optionsData.dateEnv, refinedOptions.now);
+ let dateProfileGenerator = this.buildDateProfileGenerator({
+ dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
+ duration: viewSpec.duration,
+ durationUnit: viewSpec.durationUnit,
+ usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
+ dateEnv: optionsData.dateEnv,
+ calendarApi: this.config.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 refiners = {
+ ...BASE_OPTION_REFINERS,
+ ...CALENDAR_LISTENER_REFINERS,
+ ...CALENDAR_ONLY_OPTION_REFINERS,
+ ...VIEW_ONLY_OPTION_REFINERS,
+ ...pluginHooks.listenerRefiners,
+ ...pluginHooks.optionRefiners,
+ };
+ let raw = mergeCalendarOptions(BASE_OPTION_DEFAULTS, ...pluginHooks.optionDefaults, viewSpec.optionDefaults, localeDefaults, filterKnownOptions(mergeCalendarOptions(optionOverrides, viewSpec.optionOverrides, dynamicOptionOverrides), refiners));
+ let refined = {};
+ let currentRaw = this.currentViewOptionsInput;
+ let currentRefined = this.currentViewOptionsRefined;
+ let anyChanges = false;
+ for (let optionName in raw) {
+ if (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
+ COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName])) || isMergedPropsEqual(currentRaw[optionName], raw[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], optionName);
+ }
+ anyChanges = true;
+ }
+ }
+ if (anyChanges) {
+ this.currentViewOptionsInput = raw;
+ this.currentViewOptionsRefined = refined;
+ }
+ return {
+ rawOptions: this.currentViewOptionsInput,
+ refinedOptions: this.currentViewOptionsRefined,
+ };
+ }
+ }
+ function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekTextLong, weekTextShort, pluginHooks, availableLocaleData) {
+ let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
+ return new DateEnv({
+ calendarSystem: 'gregory', // TODO: make this a setting
+ timeZone,
+ locale,
+ weekNumberCalculation,
+ firstDay,
+ weekTextLong,
+ weekTextShort,
+ cmdFormatter: pluginHooks.cmdFormatter,
+ });
+ }
+ 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);
+ }
+ /*
+ The result of this is processed by compileEventUi
+ */
+ function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
+ let eventUiBases = {
+ '': eventUiSingleBase, // fallback
+ };
+ 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) {
+ const { options } = calendarContext;
+ return {
+ eventUiSingleBase: createEventUi({
+ display: options.eventDisplay,
+ editable: options.editable, // without "event" at start
+ startEditable: options.eventStartEditable,
+ durationEditable: options.eventDurationEditable,
+ constraint: options.eventConstraint,
+ overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
+ allow: options.eventAllow,
+ // color: options.eventColor, // StandardEvent/BgEvent will handle this
+ // contrastColor: options.eventContrastColor, // StandardEvent/BgEvent will handle this
+ // className: options.eventClass // 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);
+ }
+ const warnedUnknownOptions = {};
+ function filterKnownOptions(options, optionRefiners) {
+ const knownOptions = {};
+ for (const optionName in options) {
+ if (optionRefiners[optionName]) {
+ knownOptions[optionName] = options[optionName];
+ }
+ else if (!warnedUnknownOptions[optionName]) {
+ warn(`Unknown option \`${optionName}\`.`);
+ warnedUnknownOptions[optionName] = true;
+ }
+ }
+ return knownOptions;
+ }
+ function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, nowDate, 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(nowDate, nowDate, undefined, /* forceToValid = */ false);
+ let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, nowDate, /* forceToValid = */ false);
+ let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, nowDate, /* forceToValid = */ false);
+ return {
+ title,
+ selectedButton: viewSpec.type,
+ navUnit: viewSpec.singleUnit,
+ isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, nowDate),
+ isPrevEnabled: prevInfo.isValid,
+ isNextEnabled: nextInfo.isValid,
+ };
+ }
+
+ class CalendarApiImpl {
+ getCurrentData() {
+ return this.currentDataManager.getCurrentData();
+ }
+ dispatch(action) {
+ this.currentDataManager.dispatch(action);
+ }
+ get view() { return this.getCurrentData().viewApi; }
+ batchRendering(callback) {
+ callback();
+ }
+ // Options
+ // -----------------------------------------------------------------------------------------------------------------
+ setOption(name, val) {
+ this.dispatch({
+ type: 'SET_OPTION',
+ optionName: name,
+ rawOptionValue: val,
+ });
+ }
+ getOption(name) {
+ return this.currentDataManager.currentCalendarOptionsInput[name];
+ }
+ getAvailableLocaleCodes() {
+ return Object.keys(this.getCurrentData().availableRawLocales);
+ }
+ // Trigger
+ // -----------------------------------------------------------------------------------------------------------------
+ on(handlerName, handler) {
+ let { currentDataManager } = this;
+ if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) {
+ currentDataManager.emitter.on(handlerName, handler);
+ }
+ else {
+ warn(`Unknown listener \`${handlerName}\`.`);
+ }
+ }
+ off(handlerName, handler) {
+ this.currentDataManager.emitter.off(handlerName, handler);
+ }
+ // not meant for public use
+ trigger(handlerName, ...args) {
+ this.currentDataManager.emitter.trigger(handlerName, ...args);
+ }
+ // View
+ // -----------------------------------------------------------------------------------------------------------------
+ changeView(viewType, dateOrRange) {
+ this.batchRendering(() => {
+ this.unselect();
+ if (dateOrRange) {
+ if (dateOrRange.start && dateOrRange.end) { // a range
+ this.dispatch({
+ type: 'CHANGE_VIEW_TYPE',
+ viewType,
+ });
+ this.dispatch({
+ type: 'SET_OPTION',
+ optionName: 'visibleRange',
+ rawOptionValue: dateOrRange,
+ });
+ }
+ else {
+ let { dateEnv } = this.getCurrentData();
+ this.dispatch({
+ type: 'CHANGE_VIEW_TYPE',
+ viewType,
+ dateMarker: dateEnv.createMarker(dateOrRange),
+ });
+ }
+ }
+ else {
+ this.dispatch({
+ type: 'CHANGE_VIEW_TYPE',
+ viewType,
+ });
+ }
+ });
+ }
+ // Forces navigation to a view for the given date.
+ // `viewType` can be a specific view name or a generic one like "week" or "day".
+ // needs to change
+ zoomTo(dateMarker, viewType) {
+ let state = this.getCurrentData();
+ let spec;
+ viewType = viewType || 'day'; // day is default zoom
+ spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType);
+ this.unselect();
+ if (spec) {
+ this.dispatch({
+ type: 'CHANGE_VIEW_TYPE',
+ viewType: spec.type,
+ dateMarker,
+ });
+ }
+ else {
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker,
+ });
+ }
+ }
+ // Given a duration singular unit, like "week" or "day", finds a matching view spec.
+ // Preference is given to views that have corresponding buttons.
+ getUnitViewSpec(unit) {
+ let { viewSpecs, toolbarConfig } = this.getCurrentData();
+ let viewTypes = [].concat(toolbarConfig.header ? toolbarConfig.header.viewsWithButtons : [], toolbarConfig.footer ? toolbarConfig.footer.viewsWithButtons : []);
+ let i;
+ let spec;
+ for (let viewType in viewSpecs) {
+ viewTypes.push(viewType);
+ }
+ for (i = 0; i < viewTypes.length; i += 1) {
+ spec = viewSpecs[viewTypes[i]];
+ if (spec) {
+ if (spec.singleUnit === unit) {
+ return spec;
+ }
+ }
+ }
+ return null;
+ }
+ // Current Date
+ // -----------------------------------------------------------------------------------------------------------------
+ prev() {
+ this.unselect();
+ this.dispatch({ type: 'PREV' });
+ }
+ next() {
+ this.unselect();
+ this.dispatch({ type: 'NEXT' });
+ }
+ prevYear() {
+ let state = this.getCurrentData();
+ this.unselect();
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker: state.dateEnv.addYears(state.currentDate, -1),
+ });
+ }
+ nextYear() {
+ let state = this.getCurrentData();
+ this.unselect();
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker: state.dateEnv.addYears(state.currentDate, 1),
+ });
+ }
+ today() {
+ let state = this.getCurrentData();
+ this.unselect();
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker: state.nowManager.getDateMarker(),
+ });
+ }
+ gotoDate(zonedDateInput) {
+ let state = this.getCurrentData();
+ this.unselect();
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker: state.dateEnv.createMarker(zonedDateInput),
+ });
+ }
+ incrementDate(deltaInput) {
+ let state = this.getCurrentData();
+ let delta = createDuration(deltaInput);
+ if (delta) { // else, warn about invalid input?
+ this.unselect();
+ this.dispatch({
+ type: 'CHANGE_DATE',
+ dateMarker: state.dateEnv.add(state.currentDate, delta),
+ });
+ }
+ }
+ getDate() {
+ let state = this.getCurrentData();
+ return state.dateEnv.toDate(state.currentDate);
+ }
+ // Date Formatting Utils
+ // -----------------------------------------------------------------------------------------------------------------
+ formatDate(d, formatter) {
+ let { dateEnv } = this.getCurrentData();
+ return joinDateTimeFormatParts(dateEnv.formatToParts(dateEnv.createMarker(d), createFormatter(formatter)));
+ }
+ // `settings` is for formatter AND isEndExclusive
+ formatRange(d0, d1, settings) {
+ let { dateEnv } = this.getCurrentData();
+ return joinDateTimeFormatParts(dateEnv.formatRangeToParts(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings), settings));
+ }
+ formatIso(d, omitTime) {
+ let { dateEnv } = this.getCurrentData();
+ return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime });
+ }
+ // Date Selection / Event Selection / DayClick
+ // -----------------------------------------------------------------------------------------------------------------
+ select(dateOrObj, endDate) {
+ let selectionInput;
+ if (endDate == null) {
+ if (dateOrObj.start != null) {
+ selectionInput = dateOrObj;
+ }
+ else {
+ selectionInput = {
+ start: dateOrObj,
+ end: null,
+ };
+ }
+ }
+ else {
+ selectionInput = {
+ start: dateOrObj,
+ end: endDate,
+ };
+ }
+ let state = this.getCurrentData();
+ let selection = parseDateSpan(selectionInput, state.dateEnv, createDuration({ days: 1 }));
+ if (selection) { // throw parse error otherwise?
+ this.dispatch({ type: 'SELECT_DATES', selection });
+ triggerDateSelect(selection, null, state);
+ }
+ }
+ unselect(pev) {
+ let state = this.getCurrentData();
+ if (state.dateSelection) {
+ this.dispatch({ type: 'UNSELECT_DATES' });
+ triggerDateUnselect(pev, state);
+ }
+ }
+ // Public Events API
+ // -----------------------------------------------------------------------------------------------------------------
+ addEvent(eventInput, sourceInput) {
+ if (eventInput instanceof EventImpl) {
+ let def = eventInput._def;
+ let instance = eventInput._instance;
+ let currentData = this.getCurrentData();
+ // not already present? don't want to add an old snapshot
+ if (!currentData.eventStore.defs[def.defId]) {
+ this.dispatch({
+ type: 'ADD_EVENTS',
+ eventStore: eventTupleToStore({ def, instance }), // TODO: better util for two args?
+ });
+ this.triggerEventAdd(eventInput);
+ }
+ return eventInput;
+ }
+ let state = this.getCurrentData();
+ let eventSource;
+ if (sourceInput instanceof EventSourceImpl) {
+ eventSource = sourceInput.internalEventSource;
+ }
+ else if (typeof sourceInput === 'boolean') {
+ if (sourceInput) { // true. part of the first event source
+ [eventSource] = hashValuesToArray(state.eventSources);
+ }
+ }
+ else if (sourceInput != null) { // an ID. accepts a number too
+ let sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function
+ if (!sourceApi) {
+ warn(`Unknown event source ID \`${sourceInput}\`.`); // TODO: test
+ return null;
+ }
+ eventSource = sourceApi.internalEventSource;
+ }
+ let tuple = parseEvent(eventInput, eventSource, state, false);
+ if (tuple) {
+ let newEventApi = new EventImpl(state, tuple.def, tuple.def.recurringDef ? null : tuple.instance);
+ this.dispatch({
+ type: 'ADD_EVENTS',
+ eventStore: eventTupleToStore(tuple),
+ });
+ this.triggerEventAdd(newEventApi);
+ return newEventApi;
+ }
+ return null;
+ }
+ triggerEventAdd(eventApi) {
+ let { emitter } = this.getCurrentData();
+ emitter.trigger('eventAdd', {
+ event: eventApi,
+ relatedEvents: [],
+ revert: () => {
+ this.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: eventApiToStore(eventApi),
+ });
+ },
+ });
+ }
+ // TODO: optimize
+ getEventById(id) {
+ let state = this.getCurrentData();
+ let { defs, instances } = state.eventStore;
+ id = String(id);
+ for (let defId in defs) {
+ let def = defs[defId];
+ if (def.publicId === id) {
+ if (def.recurringDef) {
+ return new EventImpl(state, def, null);
+ }
+ for (let instanceId in instances) {
+ let instance = instances[instanceId];
+ if (instance.defId === def.defId) {
+ return new EventImpl(state, def, instance);
+ }
+ }
+ }
+ }
+ return null;
+ }
+ getEvents() {
+ let currentData = this.getCurrentData();
+ return buildEventApis(currentData.eventStore, currentData);
+ }
+ removeAllEvents() {
+ this.dispatch({ type: 'REMOVE_ALL_EVENTS' });
+ }
+ // Public Event Sources API
+ // -----------------------------------------------------------------------------------------------------------------
+ getEventSources() {
+ let state = this.getCurrentData();
+ let sourceHash = state.eventSources;
+ let sourceApis = [];
+ for (let internalId in sourceHash) {
+ sourceApis.push(new EventSourceImpl(state, sourceHash[internalId]));
+ }
+ return sourceApis;
+ }
+ getEventSourceById(id) {
+ let state = this.getCurrentData();
+ let sourceHash = state.eventSources;
+ id = String(id);
+ for (let sourceId in sourceHash) {
+ if (sourceHash[sourceId].publicId === id) {
+ return new EventSourceImpl(state, sourceHash[sourceId]);
+ }
+ }
+ return null;
+ }
+ addEventSource(sourceInput) {
+ let state = this.getCurrentData();
+ if (sourceInput instanceof EventSourceImpl) {
+ // not already present? don't want to add an old snapshot
+ if (!state.eventSources[sourceInput.internalEventSource.sourceId]) {
+ this.dispatch({
+ type: 'ADD_EVENT_SOURCES',
+ sources: [sourceInput.internalEventSource],
+ });
+ }
+ return sourceInput;
+ }
+ let eventSource = parseEventSource(sourceInput, state);
+ if (eventSource) { // TODO: error otherwise?
+ this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] });
+ return new EventSourceImpl(state, eventSource);
+ }
+ return null;
+ }
+ removeAllEventSources() {
+ this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' });
+ }
+ refetchEvents() {
+ this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true });
+ }
+ // Scroll
+ // -----------------------------------------------------------------------------------------------------------------
+ scrollToTime(timeInput) {
+ let time = createDuration(timeInput);
+ if (time) {
+ this.trigger('_timeScrollRequest', time);
+ }
+ }
+ // Button State
+ // -----------------------------------------------------------------------------------------------------------------
+ getButtonState() {
+ const currentData = this.getCurrentData();
+ const { toolbarProps } = currentData;
+ const options = currentData.calendarOptions;
+ const buttonConfigs = options.buttons || {};
+ const viewSpecs = currentData.viewSpecs;
+ const currentUnit = currentData.viewSpec.singleUnit;
+ const currentHintOrdinal = [
+ currentUnit ? getSingleUnitText(currentUnit, options) : '',
+ currentUnit,
+ ];
+ const buttonState = {
+ today: {
+ text: options.todayText,
+ hint: formatWithOrdinals(options.todayHint, currentHintOrdinal, options.todayText),
+ isDisabled: !toolbarProps.isTodayEnabled,
+ },
+ prev: {
+ text: options.prevText,
+ hint: formatWithOrdinals(options.prevHint, currentHintOrdinal, options.prevText),
+ isDisabled: !toolbarProps.isPrevEnabled,
+ },
+ next: {
+ text: options.nextText,
+ hint: formatWithOrdinals(options.nextHint, currentHintOrdinal, options.nextText),
+ isDisabled: !toolbarProps.isNextEnabled,
+ },
+ prevYear: {
+ text: options.prevYearText,
+ hint: formatWithOrdinals(options.prevHint, [options.yearText, 'year'], options.prevYearText),
+ isDisabled: false,
+ },
+ nextYear: {
+ text: options.prevYearText,
+ hint: formatWithOrdinals(options.nextHint, [options.yearText, 'year'], options.nextYearText),
+ isDisabled: false,
+ },
+ };
+ for (const viewSpecName in viewSpecs) {
+ const viewSpec = viewSpecs[viewSpecName];
+ const { singleUnit } = viewSpec;
+ const buttonTextKey = viewSpec.optionDefaults.buttonTextKey;
+ const buttonText = buttonConfigs[viewSpecName]?.text ||
+ (buttonTextKey ? options[buttonTextKey] : '') ||
+ (singleUnit ? getSingleUnitText(singleUnit, options) : '') ||
+ viewSpecName;
+ const buttonHint = formatWithOrdinals(options.viewHint, [buttonText, viewSpecName], // ordinal arguments
+ buttonText);
+ buttonState[viewSpecName] = {
+ text: buttonText,
+ hint: buttonHint,
+ };
+ }
+ return buttonState;
+ }
+ }
+ function getSingleUnitText(singleUnit, options) {
+ return options[singleUnit + 'TextLong'] || options[singleUnit + 'Text'];
+ }
+
+ class CalendarMediaRoot extends C {
+ constructor() {
+ super(...arguments);
+ this.state = {
+ forPrint: false,
+ };
+ this.handleBeforePrint = () => {
+ bn(() => {
+ this.setState({ forPrint: true });
+ });
+ };
+ this.handleAfterPrint = () => {
+ bn(() => {
+ this.setState({ forPrint: false });
+ });
+ };
+ }
+ render() {
+ return this.props?.children(this.state.forPrint);
+ }
+ componentDidMount() {
+ const { props } = this;
+ const { emitter } = props;
+ emitter.on('_beforeprint', this.handleBeforePrint);
+ emitter.on('_afterprint', this.handleAfterPrint);
+ }
+ componentWillUnmount() {
+ const { props } = this;
+ const { emitter } = props;
+ emitter.off('_beforeprint', this.handleBeforePrint);
+ emitter.off('_afterprint', this.handleAfterPrint);
+ }
+ }
+ function computeRootClassName(options, forPrint) {
+ let borderlessX = options.borderlessX ?? options.borderless;
+ let borderlessTop = options.borderlessTop ?? options.borderless;
+ let borderlessBottom = options.borderlessBottom ?? options.borderless;
+ const calendarDisplayData = {
+ borderlessX: Boolean(borderlessX),
+ borderlessTop: Boolean(borderlessTop),
+ borderlessBottom: Boolean(borderlessBottom),
+ };
+ return joinClassNames(generateClassName(options.class, calendarDisplayData), generateClassName(options.className, calendarDisplayData), classNames.borderBoxRoot, classNames.isolate, classNames.flexCol, forPrint ? classNames.calendarPrintRoot : classNames.calendarScreenRoot);
+ }
+
+ class ButtonIcon extends BaseComponent {
+ render() {
+ const { contentGenerator, className } = this.props;
+ if (contentGenerator) {
+ // TODO: somehow give className to the svg?
+ return (u$1(ContentContainer, { tag: 'span', style: { display: 'contents' }, attrs: { 'aria-hidden': true }, renderProps: {}, generatorName: undefined, customGenerator: contentGenerator }));
+ }
+ if (className !== undefined) {
+ return (u$1("span", { "aria-hidden": true, className: className }));
+ }
+ }
+ }
+
+ class ToolbarSection extends BaseComponent {
+ render() {
+ let { props } = this;
+ let { options } = this.context;
+ let children = props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
+ return k$1('div', {
+ className: generateClassName(options.toolbarSectionClass, { name: props.name }),
+ }, ...children);
+ }
+ renderWidgetGroup(widgetGroup) {
+ let { props, context } = this;
+ let { options } = context;
+ let children = [];
+ let isOnlyButtons = true;
+ let isOnlyView = true;
+ for (const widget of widgetGroup) {
+ const { name, isView } = widget;
+ if (name === 'title') {
+ isOnlyButtons = false;
+ }
+ else if (!isView) {
+ isOnlyView = false;
+ }
+ }
+ for (let widget of widgetGroup) {
+ let { name, customElement, buttonHint } = widget;
+ if (name === 'title') {
+ children.push(u$1("div", { role: 'heading', "aria-level": options.headingLevel, id: props.titleId, className: joinClassNames(options.toolbarTitleClass), children: props.title }));
+ }
+ else if (customElement) {
+ children.push(u$1(ContentContainer, { tag: 'span', style: { display: 'contents' }, renderProps: {}, generatorName: undefined, customGenerator: customElement }));
+ }
+ else {
+ let isSelected = name === props.selectedButton;
+ let isDisabled = (!props.isTodayEnabled && name === 'today') ||
+ (!props.isPrevEnabled && name === 'prev') ||
+ (!props.isNextEnabled && name === 'next');
+ let buttonDisplay = widget.buttonDisplay ?? options.buttonDisplay;
+ if (buttonDisplay === 'auto') {
+ buttonDisplay = (widget.buttonIconContent || widget.buttonIconClass)
+ ? 'icon'
+ : 'text';
+ }
+ let iconNode;
+ if (buttonDisplay !== 'text') {
+ iconNode = (u$1(ButtonIcon, { className: widget.buttonIconClass, contentGenerator: widget.buttonIconContent }));
+ }
+ let inGroup = widgetGroup.length > 1 && isOnlyButtons;
+ let buttonGroup = inGroup ? { hasSelection: isOnlyView } : null;
+ let renderProps = {
+ name,
+ text: widget.buttonText,
+ isPrimary: widget.buttonIsPrimary,
+ isSelected,
+ isDisabled,
+ isIconOnly: buttonDisplay === 'icon',
+ buttonGroup,
+ };
+ children.push(u$1(ContentContainer, { tag: 'button', attrs: {
+ type: 'button',
+ disabled: isDisabled,
+ ...((isOnlyButtons && isOnlyView)
+ ? { 'role': 'tab', 'aria-selected': isSelected }
+ : { 'aria-pressed': isSelected }),
+ 'aria-label': typeof buttonHint === 'function'
+ ? buttonHint(props.navUnit)
+ : buttonHint,
+ onClick: widget.buttonClick,
+ }, className: joinClassNames(generateClassName(options.buttonClass, renderProps), !isDisabled && classNames.cursorPointer, inGroup && joinClassNames(isSelected ? classNames.z1 : classNames.z0, classNames.focusZ2)), renderProps: renderProps, generatorName: undefined, classNameGenerator: widget.buttonClass, didMount: widget.buttonDidMount, willUnmount: widget.buttonWillUnmount, children: () => (buttonDisplay === 'text'
+ ? widget.buttonText
+ : buttonDisplay === 'icon'
+ ? iconNode
+ : buttonDisplay === 'icon-text'
+ ? (u$1(S, { children: [iconNode, widget.buttonText] }))
+ : (u$1(S, { children: [widget.buttonText, iconNode] })) // text-icon
+ ) }));
+ }
+ }
+ if (children.length > 1) {
+ return k$1('div', {
+ role: (isOnlyButtons && isOnlyView) ? 'tablist' : undefined,
+ 'aria-label': (isOnlyButtons && isOnlyView) ? options.viewChangeHint : undefined,
+ className: joinClassNames(generateClassName(options.buttonGroupClass, { hasSelection: isOnlyView }), classNames.isolate),
+ }, ...children);
+ }
+ return children[0];
+ }
+ }
+
+ class Toolbar extends BaseComponent {
+ render() {
+ let { props } = this;
+ let options = this.context.options;
+ let { sectionWidgets } = props.model;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const toolbarClassOption = props.isHeader ? options.headerToolbarClass : options.footerToolbarClass;
+ return (u$1("div", { className: joinClassNames(generateClassName(toolbarClassOption, { borderlessX, borderlessTop, borderlessBottom }), generateClassName(options.toolbarClass, { borderlessX, borderlessTop, borderlessBottom })), children: [this.renderSection('start', sectionWidgets.start), this.renderSection('center', sectionWidgets.center), this.renderSection('end', sectionWidgets.end)] }));
+ }
+ renderSection(name, widgetGroups) {
+ let { props } = this;
+ return (u$1(ToolbarSection, { name: name, widgetGroups: widgetGroups, title: props.title, titleId: props.titleId, navUnit: props.navUnit, selectedButton: props.selectedButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled }, name));
+ }
+ }
+
+ /*
+ 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 eventRange = getElEventRange(segEl);
+ if (eventRange && // might be the surrounding the more link
+ component.isValidSegDownEl(ev.target)) {
+ context.emitter.trigger('eventClick', {
+ el: segEl,
+ event: new EventImpl(component.context, eventRange.def, eventRange.instance),
+ jsEvent: ev, // Is this always a mouse event? See #4655
+ view: context.viewApi,
+ });
+ }
+ };
+ this.destroy = listenBySelector(settings.el, 'click', `.${classNames.internalEvent}`, // 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 (getElEventRange(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, `.${classNames.internalEvent}`, // on both fg and bg events
+ this.handleSegEnter, this.handleSegLeave);
+ }
+ destroy() {
+ this.removeHoverListeners();
+ }
+ triggerEvent(publicEvName, ev, segEl) {
+ let { component } = this;
+ let { context } = component;
+ let eventRange = getElEventRange(segEl);
+ if (!ev || component.isValidSegDownEl(ev.target)) {
+ context.emitter.trigger(publicEvName, {
+ el: segEl,
+ event: new EventImpl(context, eventRange.def, eventRange.instance),
+ jsEvent: ev, // Is this always a mouse event? See #4655
+ view: context.viewApi,
+ });
+ }
+ }
+ }
+
+ class CalendarInner extends PureComponent {
+ constructor() {
+ super(...arguments);
+ this.buildViewContext = memoize(buildViewContext);
+ this.buildViewPropTransformers = memoize(buildViewPropTransformers);
+ this.interactionsStore = {};
+ this.calendarInteractions = [];
+ this.registerInteractiveComponent = (component, settingsInput) => {
+ let settings = parseInteractionSettings(component, settingsInput);
+ let DEFAULT_INTERACTIONS = [
+ EventClicking,
+ EventHovering,
+ ];
+ let interactionClasses = DEFAULT_INTERACTIONS;
+ if (!settingsInput.disableHits) {
+ interactionClasses = interactionClasses.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];
+ };
+ }
+ get viewTitleId() {
+ return this.props.baseId + 'title';
+ }
+ render() {
+ const { props } = this;
+ let { toolbarConfig, options } = props;
+ let viewHeight;
+ let viewHeightLiquid = false;
+ let viewAspectRatio;
+ if (props.forPrint || getIsHeightAuto(options)) ;
+ else if (options.height != null) {
+ viewHeightLiquid = 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.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, props.baseId, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
+ return (u$1(ViewContextType.Provider, { value: viewContext, children: [toolbarConfig.header && (u$1(Toolbar, { model: toolbarConfig.header, isHeader: true, titleId: this.viewTitleId, ...props.toolbarProps })), u$1("div", { className: joinClassNames(classNames.flexCol, classNames.rel,
+ // prevents browsers' "scroll anchoring behavior", which cause scroll thrashing
+ // when clicking "Next" for month-view, because rows would flex-grow while other rows
+ // temporarily removed. This behavior probably universally unhelpful for our uses,
+ // esp with virtualization, but maybe in future put on more specific row-based parents
+ classNames.overflowAnchorNone,
+ // workaround for Safari pushing content area extremely wide after returning from
+ // print-view. probably a good idea regardless, to circumvent 'auto' dimentions
+ classNames.minHeight0, viewHeightLiquid && classNames.liquid), style: {
+ height: viewHeight,
+ aspectRatio: viewAspectRatio != null ? String(viewAspectRatio) : undefined,
+ }, children: [this.renderView(joinClassNames((viewHeightLiquid || viewHeight) && classNames.liquid, viewAspectRatio != null && classNames.fill, classNames.internalView)), this.buildAppendContent()] }), toolbarConfig.footer && (u$1(Toolbar, { model: toolbarConfig.footer, isHeader: false, ...props.toolbarProps }))] }));
+ }
+ renderView(className) {
+ const { props } = this;
+ const { pluginHooks, viewSpec, toolbarConfig, toolbarProps } = props;
+ let viewProps = {
+ className,
+ dateProfile: props.dateProfile,
+ businessHours: props.businessHours,
+ eventStore: props.renderableEventStore, // !
+ eventUiBases: props.eventUiBases,
+ dateSelection: props.dateSelection,
+ eventSelection: props.eventSelection,
+ eventDrag: props.eventDrag,
+ eventResize: props.eventResize,
+ forPrint: props.forPrint,
+ labelId: toolbarConfig.header && toolbarConfig.header.hasTitle ? this.viewTitleId : undefined,
+ labelStr: toolbarConfig.header && toolbarConfig.header.hasTitle ? undefined : toolbarProps.title,
+ };
+ let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
+ let contentProps = {
+ ...props,
+ toolbarProps,
+ forPrint: props.forPrint,
+ };
+ for (let transformer of transformers) {
+ Object.assign(viewProps, transformer.transform(viewProps, contentProps));
+ }
+ let ViewComponent = viewSpec.component;
+ return (u$1(ViewComponent, { ...viewProps }));
+ }
+ buildAppendContent() {
+ const { props } = this;
+ return (u$1(S, { children: props.pluginHooks.viewContainerAppends.map((buildAppendContent, i) => (u$1(S, { children: buildAppendContent(props) }, i))) }));
+ }
+ // BE AWARE React StrictMode might execute this twice
+ componentDidMount() {
+ const { props } = this;
+ this.calendarInteractions = props.pluginHooks.calendarInteractions
+ .map((CalendarInteractionClass) => new CalendarInteractionClass(props));
+ let { propSetHandlers } = props.pluginHooks;
+ for (let propName in propSetHandlers) {
+ propSetHandlers[propName](props[propName], props);
+ }
+ // call contextInit
+ for (let callback of props.pluginHooks.contextInit) {
+ callback(props);
+ }
+ }
+ componentDidUpdate(prevProps) {
+ const { props } = this;
+ let { propSetHandlers } = props.pluginHooks;
+ for (let propName in propSetHandlers) {
+ if (props[propName] !== prevProps[propName]) {
+ propSetHandlers[propName](props[propName], props);
+ }
+ }
+ }
+ // BE AWARE React StrictMode might execute this twice
+ componentWillUnmount() {
+ const { props } = this;
+ for (let interaction of this.calendarInteractions) {
+ interaction.destroy();
+ }
+ this.calendarInteractions = [];
+ // will likely undo what was done by contextInit
+ props.emitter.trigger('_unmount');
+ }
+ }
+ function buildViewPropTransformers(theClasses) {
+ return theClasses.map((TheClass) => new TheClass());
+ }
+
+ function pointInsideRect(point, rect) {
+ return point.left >= rect.left &&
+ point.left < rect.right &&
+ point.top >= rect.top &&
+ point.top < rect.bottom;
+ }
+ // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
+ function intersectRects(rect1, rect2) {
+ let res = {
+ left: Math.max(rect1.left, rect2.left),
+ right: Math.min(rect1.right, rect2.right),
+ top: Math.max(rect1.top, rect2.top),
+ bottom: Math.min(rect1.bottom, rect2.bottom),
+ };
+ if (res.left < res.right && res.top < res.bottom) {
+ return res;
+ }
+ return false;
+ }
+ // Returns a new point that will have been moved to reside within the given rectangle
+ function constrainPoint(point, rect) {
+ return {
+ left: Math.min(Math.max(point.left, rect.left), rect.right),
+ top: Math.min(Math.max(point.top, rect.top), rect.bottom),
+ };
+ }
+ // Returns a point that is the center of the given rectangle
+ function getRectCenter(rect) {
+ return {
+ left: (rect.left + rect.right) / 2,
+ top: (rect.top + rect.bottom) / 2,
+ };
+ }
+ // Subtracts point2's coordinates from point1's coordinates, returning a delta
+ function diffPoints(point1, point2) {
+ return {
+ left: point1.left - point2.left,
+ top: point1.top - point2.top,
+ };
+ }
+
+ function computeEdges(el, getPadding = false) {
+ let computedStyle = window.getComputedStyle(el);
+ let borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0;
+ let borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0;
+ let borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0;
+ let borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
+ let badScrollbarWidths = computeScrollbarWidthsForEl(el); // includes border!
+ let scrollbarLeftRight = badScrollbarWidths.y - borderLeft - borderRight;
+ let scrollbarBottom = badScrollbarWidths.x - borderTop - borderBottom;
+ let res = {
+ borderLeft,
+ borderRight,
+ borderTop,
+ borderBottom,
+ scrollbarBottom,
+ scrollbarLeft: 0,
+ scrollbarRight: 0,
+ };
+ if (computedStyle.direction === 'rtl') {
+ res.scrollbarLeft = scrollbarLeftRight;
+ }
+ else {
+ res.scrollbarRight = scrollbarLeftRight;
+ }
+ if (getPadding) {
+ res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
+ res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
+ res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
+ res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
+ }
+ return res;
+ }
+ function computeInnerRect(el, goWithinPadding = false, doFromWindowViewport) {
+ let outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el);
+ let edges = computeEdges(el, goWithinPadding);
+ let res = {
+ left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,
+ right: outerRect.right - edges.borderRight - edges.scrollbarRight,
+ top: outerRect.top + edges.borderTop,
+ bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom,
+ };
+ if (goWithinPadding) {
+ res.left += edges.paddingLeft;
+ res.right -= edges.paddingRight;
+ res.top += edges.paddingTop;
+ res.bottom -= edges.paddingBottom;
+ }
+ return res;
+ }
+ function computeRect(el) {
+ let rect = el.getBoundingClientRect();
+ return {
+ left: rect.left + window.scrollX,
+ top: rect.top + window.scrollY,
+ right: rect.right + window.scrollX,
+ bottom: rect.bottom + window.scrollY,
+ };
+ }
+ /*
+ Returns relative to viewport origin
+ */
+ function computeClippedClientRect(el) {
+ let clippingParents = getClippingParents(el);
+ let rect = el.getBoundingClientRect();
+ for (let clippingParent of clippingParents) {
+ let intersection = intersectRects(rect, clippingParent.getBoundingClientRect());
+ if (intersection) {
+ rect = intersection;
+ }
+ else {
+ return null;
+ }
+ }
+ return rect;
+ }
+ // does not return window
+ function getClippingParents(el) {
+ let parents = [];
+ while (el instanceof HTMLElement) { // will stop when gets to document or null
+ let computedStyle = window.getComputedStyle(el);
+ if (computedStyle.position === 'fixed') {
+ break;
+ }
+ if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) {
+ parents.push(el);
+ }
+ el = el.parentNode;
+ }
+ return parents;
+ }
+ // WARNING: will include border
+ function computeScrollbarWidthsForEl(el) {
+ return {
+ x: el.offsetHeight - el.clientHeight,
+ y: el.offsetWidth - el.clientWidth,
+ };
+ }
+
+ function getAppendableRoot(el) {
+ const root = el.getRootNode();
+ if (root instanceof Document) {
+ return root.body || root.documentElement; // pick body if available
+ }
+ return root;
+ }
+ function computeElIsRtl(el) {
+ return getComputedStyle(el).direction === 'rtl';
+ }
+ // Style
+ // ----------------------------------------------------------------------------------------------------------------
+ const PIXEL_PROP_RE = /(top|left|right|bottom|width|height)$/i;
+ function applyStyle(el, props) {
+ for (let propName in props) {
+ applyStyleProp(el, propName, props[propName]);
+ }
+ }
+ function applyStyleProp(el, name, val) {
+ if (val == null) {
+ el.style[name] = '';
+ }
+ else if (typeof val === 'number' && PIXEL_PROP_RE.test(name)) {
+ el.style[name] = `${val}px`;
+ }
+ else {
+ el.style[name] = val;
+ }
+ }
+ // Event Handling
+ // ----------------------------------------------------------------------------------------------------------------
+ // if intercepting bubbled events at the document/window/body level,
+ // and want to see originating element (the 'target'), use this util instead
+ // of `ev.target` because it goes within web-component boundaries.
+ function getEventTargetViaRoot(ev) {
+ return ev.composedPath?.()[0] ?? ev.target;
+ }
+
+ class NowTimer extends C {
+ constructor(props, context) {
+ super(props, context);
+ this.handleChange = () => {
+ this.forceUpdate();
+ };
+ this.runner = new NowTimerRunner(this.handleChange);
+ }
+ render() {
+ const { props, context } = this;
+ const { nowDate, todayRange } = this.runner.update({
+ nowManager: context.nowManager,
+ unit: props.unit,
+ unitValue: props.unitValue,
+ nowIndicatorSnap: context.options.nowIndicatorSnap,
+ dateEnv: context.dateEnv,
+ });
+ return props.children(nowDate, todayRange);
+ }
+ componentWillUnmount() {
+ this.runner.destroy();
+ }
+ }
+ NowTimer.contextType = ViewContextType;
+
+ const FULL_DATE_FORMAT = createFormatter({ year: 'numeric', month: 'long', day: 'numeric' });
+ const WEEK_FORMAT = createFormatter({ week: 'long' });
+ const WEEKDAY_ONLY_FORMAT = createFormatter({
+ weekday: 'long',
+ });
+ function findWeekdayText(parts) {
+ for (const part of parts) {
+ if (part.type === 'weekday') {
+ return part.value;
+ }
+ }
+ return '';
+ }
+ function findDayNumberText(parts) {
+ for (const part of parts) {
+ if (part.type === 'day') {
+ return part.value;
+ }
+ }
+ return '';
+ }
+ function findMonthText(parts) {
+ for (const part of parts) {
+ if (part.type === 'month') {
+ return part.value;
+ }
+ }
+ return '';
+ }
+
+ /*
+ TODO: just have this return the string?
+ */
+ function buildDateStr(context, dateMarker, viewType = 'day') {
+ return joinDateTimeFormatParts(context.dateEnv.formatToParts(dateMarker, viewType === 'week' ? WEEK_FORMAT : FULL_DATE_FORMAT));
+ }
+ /*
+ Assumes navLinks enabled
+ Always hidden to screen readers. Do not point aria-labelledby at this. Use aria-label instead.
+ */
+ function buildNavLinkAttrs(context, dateMarker, viewType = 'day', dateStr = buildDateStr(context, dateMarker, viewType), isTabbable = true) {
+ const { dateEnv, options, calendarApi } = context;
+ const zonedDate = dateEnv.toDate(dateMarker);
+ const handleInteraction = (ev) => {
+ let customAction = viewType === 'day' ? options.navLinkDayClick :
+ viewType === 'week' ? options.navLinkWeekClick : null;
+ if (typeof customAction === 'function') {
+ customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev);
+ }
+ else {
+ if (typeof customAction === 'string') {
+ viewType = customAction;
+ }
+ calendarApi.zoomTo(dateMarker, viewType);
+ }
+ };
+ return {
+ 'role': 'link', // TODO
+ 'aria-label': formatWithOrdinals(options.navLinkHint, [dateStr, zonedDate], dateStr),
+ 'className': joinClassNames(options.navLinkClass, classNames.cursorPointer, classNames.internalNavLink),
+ ...(isTabbable
+ ? createAriaClickAttrs(handleInteraction)
+ : { onClick: handleInteraction }),
+ };
+ }
+
+ function getDateMeta(dateMarker, dateEnv, dateProfile, todayRange, nowDate) {
+ const isDisabled = Boolean(dateProfile && (!dateProfile.activeRange || !rangeContainsMarker(dateProfile.activeRange, dateMarker)));
+ return {
+ date: dateEnv.toDate(dateMarker),
+ dow: dateMarker.getUTCDay(),
+ isDisabled,
+ isOther: !isDisabled && Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, dateMarker)),
+ isToday: !isDisabled && Boolean(todayRange && rangeContainsMarker(todayRange, dateMarker)),
+ isPast: !isDisabled && Boolean(nowDate ? (dateMarker < nowDate) : todayRange ? (dateMarker < todayRange.start) : false),
+ isFuture: !isDisabled && Boolean(nowDate ? (dateMarker > nowDate) : todayRange ? (dateMarker >= todayRange.end) : false),
+ };
+ }
+
+ function isDimsEqual(v0, v1) {
+ return v0 != null && (v0 === v1 || Math.abs(v0 - v1) < 0.01);
+ }
+
+ const nativeBorderBoxEnabled = true;
+ const configMap = new Map();
+ const afterSizeCallbacks = new Set();
+ let isHandling = false;
+ let isStalling = false;
+ function afterSize(callback) {
+ afterSizeCallbacks.add(callback);
+ // batch & then flush when not within ResizeObserver handler loop
+ // happens for watchers that die and report `null` as dimension
+ if (!isHandling && !isStalling) {
+ isStalling = true;
+ requestAnimationFrame(() => {
+ isStalling = false;
+ flushAfterSize();
+ });
+ }
+ }
+ function flushAfterSize() {
+ for (const flushedCallback of afterSizeCallbacks.values()) {
+ flushedCallback();
+ afterSizeCallbacks.delete(flushedCallback);
+ }
+ }
+ // Native
+ // -------------------------------------------------------------------------------------------------
+ // Single global ResizeObserver does batching and uses less memory than individuals
+ // Will always fire with delay after DOM mutation, but before repaint,
+ // thus doesn't need !isHandling check like checkConfigMap
+ const globalResizeObserver = typeof ResizeObserver !== 'undefined' && new ResizeObserver((entries) => {
+ isHandling = true;
+ // // debug
+ // console.log('RESIZE-OBSERVER', entries.map((entry) => entry.target))
+ for (let entry of entries) {
+ const el = entry.target;
+ const config = configMap.get(el);
+ let width;
+ let height;
+ if (entry.borderBoxSize && nativeBorderBoxEnabled) {
+ const borderBoxSize = entry.borderBoxSize[0] || entry.borderBoxSize; // HACK for Firefox
+ width = borderBoxSize.inlineSize;
+ height = borderBoxSize.blockSize;
+ }
+ else {
+ ({ width, height } = el.getBoundingClientRect());
+ }
+ let shouldFire = false;
+ if (!isDimsEqual(config.width, width)) {
+ config.width = width;
+ shouldFire = config.watchWidth;
+ }
+ if (!isDimsEqual(config.height, height)) {
+ config.height = height;
+ shouldFire || (shouldFire = config.watchHeight);
+ }
+ if (shouldFire) {
+ config.callback(width, height);
+ }
+ }
+ bn(() => {
+ flushAfterSize();
+ isHandling = false;
+ });
+ });
+ /*
+ PRECONDITION: element can only have one listener attached
+ */
+ function watchSize(el, callback, watchWidth = true, watchHeight = true) {
+ configMap.set(el, { callback, watchWidth, watchHeight });
+ // if statement is for jsdom and other shim environments that execute component effects, but
+ // haven't implemented ResizeObserver. Reference: https://github.com/jsdom/jsdom/issues/3368
+ if (globalResizeObserver) {
+ globalResizeObserver.observe(el, {
+ box: 'border-box'
+ // default is 'content-box'
+ });
+ }
+ return () => {
+ configMap.delete(el);
+ // same reasoning as above
+ if (globalResizeObserver) {
+ globalResizeObserver.unobserve(el);
+ }
+ };
+ }
+ function watchWidth(el, callback) {
+ return watchSize(el, callback,
+ /* watchWidth = */ true);
+ }
+ function watchHeight(el, callback) {
+ return watchSize(el, (_width, height) => callback(height),
+ /* watchWidth = */ false,
+ /* watchHeight = */ true);
+ }
+
+ class ViewContainer extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.refineRenderProps = memoizeObjArg(refineRenderProps$1);
+ }
+ render() {
+ const { props, context } = this;
+ const { options, viewSpec } = context;
+ const renderProps = this.refineRenderProps({
+ ...computeViewBorderless(options),
+ options: { headerToolbar: options.headerToolbar, footerToolbar: options.footerToolbar },
+ isHeightAuto: getIsHeightAuto(options),
+ viewApi: context.viewApi,
+ });
+ return (u$1(ContentContainer, { elRef: props.elRef, tag: props.tag || 'div', attrs: props.attrs, style: props.style, className: joinClassNames(props.className, generateClassName(options.viewClass, renderProps),
+ // WORKAROUND for way calendar's className would get merged into view's className
+ generateClassName(viewSpec.optionDefaults.class, renderProps), generateClassName(viewSpec.optionDefaults.className, renderProps), generateClassName(viewSpec.optionOverrides.class, renderProps), generateClassName(viewSpec.optionOverrides.className, renderProps)), renderProps: renderProps, generatorName: undefined, didMount: options.didMount || options.viewDidMount, willUnmount: options.willUnmount || options.viewWillUnmount, children: () => props.children }));
+ }
+ }
+ function refineRenderProps$1(raw) {
+ return {
+ view: raw.viewApi,
+ borderlessX: raw.borderlessX,
+ borderlessTop: raw.borderlessTop,
+ borderlessBottom: raw.borderlessBottom,
+ options: raw.options,
+ isHeightAuto: raw.isHeightAuto,
+ };
+ }
+
+ /*
+ an INTERACTABLE date component
+
+ PURPOSES:
+ - hook up to fg, fill, and mirror renderers
+ - interface for dragging and hits
+ */
+ class DateComponent extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.uid = guid();
+ }
+ // Hit System
+ // -----------------------------------------------------------------------------------------------------------------
+ prepareHits() {
+ }
+ queryHit(isRtl, positionLeft, positionTop, elWidth, elHeight) {
+ return null; // this should be abstract
+ }
+ // Pointer Interaction Utils
+ // -----------------------------------------------------------------------------------------------------------------
+ isValidSegDownEl(el) {
+ return !this.props.eventDrag && // HACK
+ !this.props.eventResize && // HACK
+ !el.closest(`.${classNames.internalEventMirror}`);
+ }
+ isValidDateDownEl(el) {
+ return !el.closest(`.${classNames.internalEvent}:not(.${classNames.internalBgEvent})`) &&
+ !el.closest(`.${classNames.internalMoreLink}`) &&
+ !el.closest(`.${classNames.internalNavLink}`) &&
+ !el.closest(`.${classNames.internalPopover}`); // hack
+ }
+ }
+
+ class DelayedRunner {
+ constructor(drainedOption) {
+ this.drainedOption = drainedOption;
+ this.isRunning = false;
+ this.isDirty = false;
+ this.pauseDepths = {};
+ this.timeoutId = 0;
+ }
+ request(delay) {
+ this.isDirty = true;
+ if (!this.isPaused()) {
+ this.clearTimeout();
+ if (delay == null) {
+ this.tryDrain();
+ }
+ else {
+ this.timeoutId = setTimeout(// NOT OPTIMAL! TODO: look at debounce
+ this.tryDrain.bind(this), delay);
+ }
+ }
+ }
+ pause(scope = '') {
+ let { pauseDepths } = this;
+ pauseDepths[scope] = (pauseDepths[scope] || 0) + 1;
+ this.clearTimeout();
+ }
+ resume(scope = '', force) {
+ let { pauseDepths } = this;
+ if (scope in pauseDepths) {
+ if (force) {
+ delete pauseDepths[scope];
+ }
+ else {
+ pauseDepths[scope] -= 1;
+ let depth = pauseDepths[scope];
+ if (depth <= 0) {
+ delete pauseDepths[scope];
+ }
+ }
+ this.tryDrain();
+ }
+ }
+ isPaused() {
+ return Object.keys(this.pauseDepths).length;
+ }
+ tryDrain() {
+ if (!this.isRunning && !this.isPaused()) {
+ this.isRunning = true;
+ while (this.isDirty) {
+ this.isDirty = false;
+ this.drained(); // might set isDirty to true again
+ }
+ this.isRunning = false;
+ }
+ }
+ clear() {
+ this.clearTimeout();
+ this.isDirty = false;
+ this.pauseDepths = {};
+ }
+ clearTimeout() {
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = 0;
+ }
+ }
+ drained() {
+ if (this.drainedOption) {
+ this.drainedOption();
+ }
+ }
+ }
+
+ /*
+ NOTE: detection is complicated (w/ touch and wheel) because ScrollerSyncer needs to know about it,
+ but are we sure we can't just ignore programmatic scrollTo() calls with a flag? and determine the
+ the scroll-master simply by who was the newest scroller? Does passive:true do things asynchronously?
+ */
+ class ScrollListener {
+ constructor(el) {
+ this.el = el;
+ this.emitter = new Emitter();
+ this.isScroll = false;
+ this.isScrollRecent = false;
+ this.isWheelRecent = false;
+ this.isMouseDown = false; // user currently has mouse down?
+ this.isTouchDown = false; // user currently has finger down?
+ // accumulated during scroll
+ this.isMouse = false;
+ this.isTouch = false;
+ this.isWheel = false;
+ // Handlers
+ // ----------------------------------------------------------------------------------------------
+ this.handleScroll = () => {
+ this.isScrollRecent = true;
+ if (this.isMouseDown) {
+ this.isMouse = true;
+ }
+ if (this.isTouchDown) {
+ this.isTouch = true;
+ }
+ if (this.isWheelRecent) {
+ this.isWheel = true;
+ }
+ this.startScroll();
+ this.emitter.trigger('scroll', this.getIsDevice());
+ this.scrollWaiter.request(500);
+ };
+ this.handleScrollWait = () => {
+ this.isScrollRecent = false;
+ // only end the scroll if not currently touching.
+ // if touching, the scrolling will end later, on touchend.
+ if (!this.isTouchDown) {
+ this.endScroll();
+ }
+ };
+ // will fire *before* the scroll event is fired (might not cause a scroll!)
+ this.handleWheel = () => {
+ this.isWheelRecent = true;
+ this.wheelWaiter.request(500);
+ };
+ this.handleWheelWait = () => {
+ this.isWheelRecent = false;
+ };
+ this.handleMouseDown = () => {
+ this.isMouseDown = true;
+ };
+ this.handleMouseUp = () => {
+ this.isMouseDown = false;
+ };
+ // will fire *before* the scroll event is fired (might not cause a scroll!)
+ this.handleTouchStart = () => {
+ this.isTouchDown = true;
+ };
+ this.handleTouchEnd = () => {
+ this.isTouchDown = false;
+ // if the user ended their touch, and the scroll area wasn't moving,
+ // we consider this to be the end of the scroll
+ // otherwise, wait for inertia to finish and handleScrollWait to fire
+ if (!this.isScrollRecent) {
+ this.endScroll();
+ }
+ };
+ this.wheelWaiter = new DelayedRunner(this.handleWheelWait);
+ this.scrollWaiter = new DelayedRunner(this.handleScrollWait);
+ el.addEventListener('scroll', this.handleScroll, { passive: true });
+ el.addEventListener('wheel', this.handleWheel, { passive: true });
+ el.addEventListener('mousedown', this.handleMouseDown);
+ el.addEventListener('mouseup', this.handleMouseUp);
+ el.addEventListener('touchstart', this.handleTouchStart, { passive: true });
+ el.addEventListener('touchend', this.handleTouchEnd);
+ }
+ destroy() {
+ let { el } = this;
+ el.removeEventListener('scroll', this.handleScroll, { passive: true });
+ el.removeEventListener('wheel', this.handleWheel, { passive: true });
+ el.removeEventListener('mousedown', this.handleMouseDown);
+ el.removeEventListener('mouseup', this.handleMouseUp);
+ el.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
+ el.removeEventListener('touchend', this.handleTouchEnd);
+ }
+ // Start / Stop
+ // ----------------------------------------------------------------------------------------------
+ startScroll() {
+ if (!this.isScroll) {
+ this.isScroll = true;
+ this.emitter.trigger('scrollStart', this.getIsDevice());
+ }
+ }
+ endScroll() {
+ if (this.isScroll) { // extra protection because might be called publicly
+ this.scrollWaiter.clear(); // (same)
+ this.wheelWaiter.clear(); // (same)
+ this.isScroll = false;
+ this.isWheelRecent = false;
+ this.emitter.trigger('scrollEnd', this.getIsDevice());
+ this.isMouse = false;
+ this.isTouch = false;
+ this.isWheel = false;
+ }
+ }
+ getIsDevice() {
+ return this.isWheel || this.isMouse || this.isTouch;
+ }
+ }
+
+ class Scroller extends DateComponent {
+ constructor() {
+ super(...arguments);
+ this.handleEl = (el) => {
+ if (this.el) {
+ this.el = null;
+ this._isUnmounting = true;
+ this.listener.destroy();
+ }
+ if (el) {
+ this.el = el;
+ this._isUnmounting = false;
+ this.listener = new ScrollListener(el);
+ }
+ };
+ this.handleHRuler = (el) => {
+ if (this.disconnectHRuler) {
+ this.disconnectHRuler();
+ this.disconnectHRuler = undefined;
+ if (this.clientWidth !== undefined) {
+ this.clientWidth = undefined;
+ setRef(this.props.clientWidthRef, null);
+ }
+ }
+ if (el) {
+ this.disconnectHRuler = watchWidth(el, (clientWidth) => {
+ if (this._isUnmounting)
+ return;
+ if (clientWidth !== this.clientWidth) {
+ this.clientWidth = clientWidth;
+ setRef(this.props.clientWidthRef, clientWidth);
+ }
+ });
+ }
+ };
+ this.handleVRuler = (el) => {
+ if (this.disconnectVRuler) {
+ this.disconnectVRuler();
+ this.disconnectVRuler = undefined;
+ if (this.clientHeight !== undefined) {
+ this.clientHeight = undefined;
+ setRef(this.props.clientHeightRef, null);
+ }
+ }
+ if (el) {
+ this.disconnectVRuler = watchHeight(el, (clientHeight) => {
+ if (this._isUnmounting)
+ return;
+ if (clientHeight !== this.clientHeight) {
+ this.clientHeight = clientHeight;
+ setRef(this.props.clientHeightRef, clientHeight);
+ }
+ const bottomScrollbarWidth = Math.round(this.el.getBoundingClientRect().height - clientHeight);
+ if (bottomScrollbarWidth !== this.bottomScrollbarWidth) {
+ this.bottomScrollbarWidth = bottomScrollbarWidth;
+ setRef(this.props.bottomScrollbarWidthRef, bottomScrollbarWidth);
+ }
+ });
+ }
+ };
+ }
+ render() {
+ const { props } = this;
+ // if there's only one axis that needs scrolling, the other axis will unintentionally have
+ // scrollbars too if we don't force to 'hidden'
+ const fallbackOverflow = (props.horizontal || props.vertical) ? 'hidden' : '';
+ return (u$1("div", { ref: this.handleEl, className: joinClassNames(props.className, classNames.noPadding, classNames.rel, // for children fillTop/fillStart
+ props.hideScrollbars && classNames.noScrollbars, classNames.internalScroller), style: {
+ ...props.style,
+ overflowX: (props.horizontal ? 'auto' : fallbackOverflow),
+ overflowY: (props.vertical ? 'auto' : fallbackOverflow),
+ }, children: [props.children, Boolean(props.clientWidthRef) && (u$1("div", { ref: this.handleHRuler, className: classNames.fillTop })), Boolean(props.clientHeightRef || props.bottomScrollbarWidthRef) && (u$1("div", { ref: this.handleVRuler, className: classNames.fillStart }))] }));
+ }
+ endScroll() {
+ this.listener.endScroll();
+ }
+ // Public API
+ // -----------------------------------------------------------------------------------------------
+ get x() {
+ const { el } = this;
+ return el ? getNormalizedScrollX(el) : 0;
+ }
+ get y() {
+ const { el } = this;
+ return el ? el.scrollTop : 0;
+ }
+ scrollTo({ x, y }) {
+ const { el } = this;
+ if (el) {
+ if (y != null) {
+ el.scrollTop = y;
+ }
+ if (x != null) {
+ setNormalizedScrollX(el, x);
+ }
+ }
+ }
+ addScrollStartListener(handler) {
+ this.listener.emitter.on('scrollStart', handler);
+ }
+ removeScrollStartListener(handler) {
+ this.listener.emitter.off('scrollStart', handler);
+ }
+ addScrollEndListener(handler) {
+ this.listener.emitter.on('scrollEnd', handler);
+ }
+ removeScrollEndListener(handler) {
+ this.listener.emitter.off('scrollEnd', handler);
+ }
+ }
+ // Public API
+ // -------------------------------------------------------------------------------------------------
+ // We can drop normalization when support for Chromium-based <86 is dropped (see Notion)
+ function getNormalizedScrollX(el) {
+ const { scrollLeft } = el;
+ const isRtl = computeElIsRtl(el);
+ return isRtl ? getNormalizedRtlScrollX(scrollLeft, el) : scrollLeft;
+ }
+ function setNormalizedScrollX(el, x) {
+ const isRtl = computeElIsRtl(el);
+ el.scrollLeft = isRtl ? getNormalizedRtlScrollLeft(x, el) : x;
+ }
+ /*
+ Returns a value in the 'reverse' system
+ */
+ function getNormalizedRtlScrollX(scrollLeft, el) {
+ switch (getRtlScrollerSystem()) {
+ case 'positive':
+ return el.scrollWidth - el.clientWidth - scrollLeft;
+ case 'negative':
+ return -scrollLeft;
+ }
+ return scrollLeft;
+ }
+ /*
+ Receives a value in the 'reverse' system
+ TODO: is this really the same equations as getNormalizedRtlScrollX??? I think so
+ If so, consolidate. With isRtl check too
+ */
+ function getNormalizedRtlScrollLeft(x, el) {
+ switch (getRtlScrollerSystem()) {
+ case 'positive':
+ return el.scrollWidth - el.clientWidth - x;
+ case 'negative':
+ return -x;
+ }
+ return x;
+ }
+ let _rtlScrollerSystem;
+ function getRtlScrollerSystem() {
+ return _rtlScrollerSystem || (_rtlScrollerSystem = detectRtlScrollerSystem());
+ }
+ function detectRtlScrollerSystem() {
+ let el = document.createElement('div');
+ el.style.position = 'absolute';
+ el.style.top = '-1000px';
+ el.style.width = '100px'; // must be at least the side of scrollbars or you get inaccurate values (#7335)
+ el.style.height = '100px'; // "
+ el.style.overflow = 'scroll';
+ el.style.direction = 'rtl';
+ let innerEl = document.createElement('div');
+ innerEl.style.width = '200px';
+ innerEl.style.height = '200px';
+ el.appendChild(innerEl);
+ document.body.appendChild(el);
+ let system;
+ if (el.scrollLeft > 0) {
+ system = 'positive'; // scroll is a positive number from the left edge
+ }
+ else {
+ el.scrollLeft = 50;
+ if (el.scrollLeft > 0) {
+ system = 'reverse'; // scroll is a positive number from the right edge
+ }
+ else {
+ system = 'negative'; // scroll is a negative number from the right edge
+ }
+ }
+ el.remove();
+ return system;
+ }
+
+ class StandardEvent extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.buildPublicEvent = memoize((context, eventDef, eventInstance) => new EventImpl(context, eventDef, eventInstance));
+ this.handleEl = (el) => {
+ this.el = el;
+ setRef(this.props.elRef, el);
+ if (el) {
+ setElEventRange(el, this.props.eventRange);
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { options } = context;
+ const { eventRange } = props;
+ const eventUi = eventRange.ui;
+ const timeFormat = options.eventTimeFormat || props.defaultTimeFormat;
+ const timeText = props.forcedTimeText ?? buildEventRangeTimeText(timeFormat, eventRange, // just for def/instance
+ props.slicedStart, props.slicedEnd, props.isStart, props.isEnd, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd);
+ const [tag, attrs, isInteractive] = getEventTagAndAttrs(eventRange, context);
+ const eventApi = this.buildPublicEvent(context, eventRange.def, eventRange.instance);
+ const isDraggable = !props.disableDragging && computeEventRangeDraggable(eventRange, context);
+ const isBlock = /row|column/.test(props.display);
+ const subcontentRenderProps = {
+ event: eventApi,
+ isNarrow: props.isNarrow || false,
+ isShort: props.isShort || false,
+ timeText,
+ };
+ const renderProps = {
+ event: eventApi, // make stable. everything else atomic. FYI, eventRange unfortunately gets reconstructed a lot, but def/instance is stable
+ view: context.viewApi,
+ timeText: timeText,
+ color: eventUi.color || options.eventColor,
+ contrastColor: eventUi.contrastColor || options.eventContrastColor,
+ isDraggable,
+ isStartResizable: !props.disableResizing && props.isStart && eventUi.durationEditable && options.eventResizableFromStart,
+ isEndResizable: !props.disableResizing && props.isEnd && eventUi.durationEditable,
+ isMirror: props.isMirror,
+ isStart: Boolean(props.isStart),
+ isEnd: Boolean(props.isEnd),
+ isFirst: Boolean(props.isFirst),
+ isLast: Boolean(props.isLast),
+ isPast: Boolean(props.isPast), // TODO: don't cast. getDateMeta does it
+ isFuture: Boolean(props.isFuture), // TODO: don't cast. getDateMeta does it
+ isToday: Boolean(props.isToday), // TODO: don't cast. getDateMeta does it
+ isSelected: Boolean(props.isSelected),
+ isDragging: Boolean(props.isDragging),
+ isResizing: Boolean(props.isResizing),
+ isInteractive,
+ isNarrow: props.isNarrow || false,
+ isShort: props.isShort || false,
+ level: props.level || 0,
+ timeClass: joinClassNames(generateClassName(options.eventTimeClass, subcontentRenderProps), isBlock && generateClassName(options.blockEventTimeClass, subcontentRenderProps), props.display === 'row' && generateClassName(options.rowEventTimeClass, subcontentRenderProps), props.display === 'column' && generateClassName(options.columnEventTimeClass, subcontentRenderProps), props.display === 'list-item' && generateClassName(options.listItemEventTimeClass, subcontentRenderProps)),
+ titleClass: joinClassNames(generateClassName(options.eventTitleClass, subcontentRenderProps), isBlock && generateClassName(options.blockEventTitleClass, subcontentRenderProps), props.display === 'row' && generateClassName(options.rowEventTitleClass, subcontentRenderProps), props.display === 'column' && generateClassName(options.columnEventTitleClass, subcontentRenderProps), props.display === 'list-item' && generateClassName(options.listItemEventTitleClass, subcontentRenderProps), props.display === 'row' && options.rowEventTitleSticky && classNames.stickyS, props.display === 'column' && options.columnEventTitleSticky && classNames.stickyT),
+ options: { eventOverlap: Boolean(options.eventOverlap) },
+ };
+ const outerClassName = joinClassNames(// already includes eventClass below
+ isBlock && generateClassName(options.blockEventClass, renderProps), props.display === 'row' && generateClassName(options.rowEventClass, renderProps), props.display === 'column' && generateClassName(options.columnEventClass, renderProps), props.display === 'list-item' && generateClassName(options.listItemEventClass, renderProps), eventUi.className, props.className, props.display === 'column'
+ ? classNames.flexCol
+ : classNames.flexRow, (eventRange.def.url || isDraggable) && classNames.cursorPointer, classNames.internalEvent, props.isMirror && classNames.internalEventMirror, isDraggable && classNames.internalEventDraggable, renderProps.isSelected && classNames.internalEventSelected, (renderProps.isStartResizable || renderProps.isEndResizable) && classNames.internalEventResizable);
+ const beforeClassName = joinClassNames(generateClassName(options.eventBeforeClass, renderProps), isBlock && generateClassName(options.blockEventBeforeClass, renderProps), props.display === 'row' && generateClassName(options.rowEventBeforeClass, renderProps), props.display === 'column' && generateClassName(options.columnEventBeforeClass, renderProps), props.display === 'list-item' && generateClassName(options.listItemEventBeforeClass, renderProps));
+ const afterClassName = joinClassNames(generateClassName(options.eventAfterClass, renderProps), isBlock && generateClassName(options.blockEventAfterClass, renderProps), props.display === 'row' && generateClassName(options.rowEventAfterClass, renderProps), props.display === 'column' && generateClassName(options.columnEventAfterClass, renderProps), props.display === 'list-item' && generateClassName(options.listItemEventAfterClass, renderProps));
+ const innerClassName = joinClassNames(generateClassName(options.eventInnerClass, renderProps), isBlock && generateClassName(options.blockEventInnerClass, renderProps), props.display === 'row' && generateClassName(options.rowEventInnerClass, renderProps), props.display === 'column' && generateClassName(options.columnEventInnerClass, renderProps), props.display === 'list-item' && generateClassName(options.listItemEventInnerClass, renderProps), !props.disableLiquid && classNames.liquid);
+ const beforeContent = props.display === 'row' && options.rowEventBeforeContent;
+ const afterContent = props.display === 'row' && options.rowEventAfterContent;
+ return (u$1(ContentContainer, { tag: tag, attrs: {
+ ...props.attrs,
+ ...attrs,
+ // HACK because this event-element gets attached to root during some dragging
+ dir: (props.isDragging && options.direction === 'rtl') ? 'rtl' : undefined,
+ }, className: outerClassName, style: {
+ '--fc-event-color': renderProps.color,
+ '--fc-event-contrast-color': renderProps.contrastColor,
+ }, elRef: this.handleEl, renderProps: renderProps, generatorName: "eventContent", customGenerator: options.eventContent, defaultGenerator: renderInnerContent$2, classNameGenerator: options.eventClass, didMount: options.eventDidMount, willUnmount: options.eventWillUnmount, children: (InnerContent) => (u$1(S, { children: [Boolean(renderProps.isSelected && isBlock) && (u$1("div", { className: props.display === 'column'
+ ? classNames.hitX
+ : classNames.hitY })), (beforeClassName || beforeContent) && (u$1("div", { className: joinClassNames(beforeClassName, !props.disableZindexes && classNames.z1, renderProps.isStartResizable && joinClassNames(props.display === 'column'
+ ? classNames.cursorResizeT
+ : classNames.cursorResizeS,
+ // these classnames required for dnd
+ classNames.internalEventResizer, classNames.internalEventResizerStart)), children: [beforeContent && (u$1(ContentContainer, { tag: 'div', style: { display: 'contents' }, attrs: { 'aria-hidden': true }, renderProps: renderProps, generatorName: undefined, customGenerator: beforeContent })), Boolean(renderProps.isStartResizable && renderProps.isSelected) && (u$1("div", { className: classNames.hit }))] })), u$1(InnerContent, { tag: "div", className: joinClassNames(innerClassName, !props.disableZindexes && classNames.z0) }), (afterClassName || afterContent) && (u$1("div", { className: joinClassNames(afterClassName, !props.disableZindexes && classNames.z1, renderProps.isEndResizable && joinClassNames(props.display === 'column'
+ ? classNames.cursorResizeB
+ : classNames.cursorResizeE,
+ // these classnames required for dnd
+ classNames.internalEventResizer, classNames.internalEventResizerEnd)), children: [afterContent && (u$1(ContentContainer, { tag: 'div', style: { display: 'contents' }, attrs: { 'aria-hidden': true }, renderProps: renderProps, generatorName: undefined, customGenerator: afterContent })), Boolean(renderProps.isEndResizable && renderProps.isSelected) && (u$1("div", { className: classNames.hit }))] }))] })) }));
+ }
+ componentDidUpdate(prevProps) {
+ if (this.el && this.props.eventRange !== prevProps.eventRange) {
+ setElEventRange(this.el, this.props.eventRange);
+ }
+ }
+ }
+ StandardEvent.addPropsEquality({
+ seg: isPropsEqualShallow,
+ });
+ function renderInnerContent$2(innerProps) {
+ return (u$1(S, { children: [innerProps.timeText && (u$1("div", { className: innerProps.timeClass, children: innerProps.timeText })), u$1("div", { className: innerProps.titleClass, children: innerProps.event.title || u$1(S, { children: "\u00A0" }) })] }));
+ }
+
+ class Slicer {
+ constructor() {
+ this.sliceBusinessHours = memoize(this._sliceBusinessHours);
+ this.sliceDateSelection = memoize(this._sliceDateSpan);
+ this.sliceEventStore = memoize(this._sliceEventStore);
+ this.sliceEventDrag = memoize(this._sliceInteraction);
+ this.sliceEventResize = memoize(this._sliceInteraction);
+ this.forceDayIfListItem = false; // hack
+ }
+ sliceProps(props, dateProfile, nextDayThreshold, context, ...extraArgs) {
+ let { eventUiBases } = props;
+ let eventSegs = this.sliceEventStore(props.eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs);
+ return {
+ dateSelectionSegs: this.sliceDateSelection(props.dateSelection, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs),
+ businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, context, ...extraArgs),
+ fgEventSegs: eventSegs.fg,
+ bgEventSegs: eventSegs.bg,
+ eventDrag: this.sliceEventDrag(props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
+ eventResize: this.sliceEventResize(props.eventResize, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
+ eventSelection: props.eventSelection,
+ }; // TODO: give interactionSegs?
+ }
+ sliceNowDate(// does not memoize
+ date, dateProfile, nextDayThreshold, context, ...extraArgs) {
+ return this._sliceDateSpan({ range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range
+ dateProfile, nextDayThreshold, {}, context, ...extraArgs);
+ }
+ _sliceBusinessHours(businessHours, dateProfile, nextDayThreshold, context, ...extraArgs) {
+ if (!businessHours) {
+ return [];
+ }
+ return this._sliceEventStore(expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), context), {}, dateProfile, nextDayThreshold, ...extraArgs).bg;
+ }
+ _sliceEventStore(eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
+ if (eventStore) {
+ let rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
+ return {
+ bg: this.sliceEventRanges(rangeRes.bg, extraArgs),
+ fg: this.sliceEventRanges(rangeRes.fg, extraArgs),
+ };
+ }
+ return { bg: [], fg: [] };
+ }
+ _sliceInteraction(interaction, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
+ if (!interaction) {
+ return null;
+ }
+ let rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
+ return {
+ segs: this.sliceEventRanges(rangeRes.fg, extraArgs),
+ affectedInstances: interaction.affectedEvents.instances,
+ isEvent: interaction.isEvent,
+ };
+ }
+ _sliceDateSpan(dateSpan, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs) {
+ if (!dateSpan) {
+ return [];
+ }
+ let activeRange = computeActiveRange(dateProfile, Boolean(nextDayThreshold));
+ let activeDateSpanRange = intersectRanges(dateSpan.range, activeRange);
+ if (activeDateSpanRange) {
+ dateSpan = { ...dateSpan, range: activeDateSpanRange };
+ let eventRange = fabricateEventRange(dateSpan, eventUiBases, context);
+ let segs = this.sliceRange(dateSpan.range, ...extraArgs);
+ for (let seg of segs) {
+ seg.eventRange = eventRange;
+ }
+ return segs;
+ }
+ return [];
+ }
+ /*
+ "complete" seg means it has component and eventRange
+ */
+ sliceEventRanges(eventRanges, extraArgs) {
+ let segs = [];
+ for (let eventRange of eventRanges) {
+ segs.push(...this.sliceEventRange(eventRange, extraArgs));
+ }
+ return segs;
+ }
+ /*
+ "complete" seg means it has component and eventRange
+ */
+ sliceEventRange(eventRange, extraArgs) {
+ let dateRange = eventRange.range;
+ // hack to make multi-day events that are being force-displayed as list-items to take up only one day
+ if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') {
+ dateRange = {
+ start: dateRange.start,
+ end: addDays(dateRange.start, 1),
+ };
+ }
+ let segs = this.sliceRange(dateRange, ...extraArgs); // !!!
+ for (let seg of segs) {
+ seg.eventRange = eventRange;
+ seg.isStart = eventRange.isStart && seg.isStart;
+ seg.isEnd = eventRange.isEnd && seg.isEnd;
+ }
+ return segs;
+ }
+ }
+ /*
+ for incorporating slotMinTime/slotMaxTime if appropriate
+ TODO: should be part of DateProfile!
+ TimelineDateProfile already does this btw
+ */
+ function computeActiveRange(dateProfile, isComponentAllDay) {
+ let range = dateProfile.activeRange;
+ if (isComponentAllDay) {
+ return range;
+ }
+ return {
+ start: addMs(range.start, dateProfile.slotMinTime.milliseconds),
+ end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day
+ };
+ }
+
+ class DayTableSlicer extends Slicer {
+ constructor() {
+ super(...arguments);
+ this.forceDayIfListItem = true;
+ }
+ sliceRange(dateRange, dayTableModel) {
+ return dayTableModel.sliceRange(dateRange);
+ }
+ }
+
+ // TODO: converge types with DayTableCell and DayCellContainer (the component) and refineRenderProps
+ // the generation of DayTableCell will be distinct (for the BODY cells)
+ // but can share some of the same types/utils
+ // Date Cells
+ // -------------------------------------------------------------------------------------------------
+ const firstSunday = new Date(259200000);
+ function buildDateRowConfigs(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
+ context) {
+ const rowConfig = buildDateRowConfig(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context);
+ const majorUnit = computeMajorUnit(dateProfile, context.dateEnv);
+ // HACK mutate isMajor
+ // Skip 'day' majorUnit: when each header cell IS a day, every cell would match,
+ // so there's no meaningful boundary to highlight (unlike timeline slots which can be sub-day).
+ if (datesRepDistinctDays && majorUnit !== 'day') {
+ for (const dataConfig of rowConfig.dataConfigs) {
+ if (isMajorUnit(dataConfig.dateMarker, majorUnit, context.dateEnv)) {
+ dataConfig.renderProps.isMajor = true;
+ }
+ }
+ }
+ return [rowConfig];
+ }
+ /*
+ Should this receive resource data attributes?
+ Or ResourceApi object itself?
+ */
+ function buildDateRowConfig(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
+ context, colSpan, isMajorMod) {
+ return {
+ isDateRow: true,
+ renderConfig: buildDateRenderConfig(dayHeaderFormat, datesRepDistinctDays, context),
+ dataConfigs: buildDateDataConfigs(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context, colSpan, undefined, undefined, undefined, undefined, isMajorMod)
+ };
+ }
+ /*
+ For header cells: how to connect w/ custom rendering
+ Applies to all cells in a row
+ */
+ function buildDateRenderConfig(dayHeaderFormat, datesRepDistinctDays, context) {
+ const { options } = context;
+ return {
+ generatorName: 'dayHeaderContent',
+ customGenerator: options.dayHeaderContent,
+ classNameGenerator: options.dayHeaderClass,
+ innerClassNameGenerator: options.dayHeaderInnerClass,
+ didMount: options.dayHeaderDidMount,
+ willUnmount: options.dayHeaderWillUnmount,
+ align: options.dayHeaderAlign,
+ sticky: options._dayHeaderSticky,
+ dayHeaderFormat,
+ datesRepDistinctDays,
+ };
+ }
+ const dowDates = [];
+ for (let dow = 0; dow < 7; dow++) {
+ dowDates.push(addDays(new Date(259200000), dow)); // start with Sun, 04 Jan 1970 00:00:00 GMT)
+ }
+ /*
+ For header cells: data
+ */
+ function buildDateDataConfigs(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
+ context, colSpan = 1, keyPrefix = '', extraRenderProps = {}, // TODO
+ extraAttrs = {}, // TODO
+ className = '', isMajorMod) {
+ const { dateEnv, viewApi, options } = context;
+ return datesRepDistinctDays
+ ? dateMarkers.map((dateMarker, i) => {
+ const dateMeta = getDateMeta(dateMarker, dateEnv, dateProfile, todayRange);
+ const isMajor = isMajorMod != null && !(i % isMajorMod);
+ const hasNavLink = options.navLinks && !dateMeta.isDisabled &&
+ dateMarkers.length > 1; // don't show navlink to day if only one day
+ const renderProps = {
+ ...dateMeta,
+ ...extraRenderProps,
+ isMajor,
+ isSticky: false, // HACK. gets overridden
+ inPopover: false,
+ hasNavLink,
+ view: viewApi,
+ };
+ const fullDateStr = buildDateStr(context, dateMarker);
+ // for DayGridHeaderCell
+ return {
+ key: keyPrefix + dateMarker.toUTCString(),
+ dateMarker,
+ renderProps,
+ attrs: {
+ 'aria-label': fullDateStr,
+ ...(dateMeta.isToday ? { 'aria-current': 'date' } : {}), // TODO: assign undefined for nonexistent
+ 'data-date': formatDayString(dateMarker),
+ ...extraAttrs,
+ },
+ // for navlink
+ innerAttrs: hasNavLink
+ ? buildNavLinkAttrs(context, dateMarker, undefined, fullDateStr)
+ : { 'aria-hidden': true }, // label already on cell
+ colSpan,
+ hasNavLink,
+ className,
+ };
+ })
+ : dateMarkers.map((dateMarker, i) => {
+ const dow = dateMarker.getUTCDay();
+ const normDate = addDays(firstSunday, dow);
+ const dateMeta = {
+ date: dateEnv.toDate(dateMarker),
+ dow,
+ isDisabled: false,
+ isFuture: false,
+ isPast: false,
+ isToday: false,
+ isOther: false,
+ };
+ const isMajor = isMajorMod != null && !(i % isMajorMod);
+ const renderProps = {
+ ...dateMeta,
+ date: dowDates[dow],
+ isMajor,
+ isSticky: false, // HACK. gets overridden
+ inPopover: false,
+ hasNavLink: false,
+ view: viewApi,
+ ...extraRenderProps,
+ };
+ const fullWeekDayStr = joinDateTimeFormatParts(dateEnv.formatToParts(normDate, WEEKDAY_ONLY_FORMAT));
+ // for DayGridHeaderCell
+ return {
+ key: keyPrefix + String(dow),
+ dateMarker,
+ renderProps,
+ attrs: {
+ 'aria-label': fullWeekDayStr,
+ ...extraAttrs,
+ },
+ // NOT a navlink
+ innerAttrs: {
+ 'aria-hidden': true, // label already on cell
+ },
+ colSpan,
+ className,
+ };
+ });
+ }
+
+ /*
+ TODO: make API where createRefMap() called
+ */
+ class RefMap {
+ constructor(masterCallback, ignoreDeletes = false) {
+ this.masterCallback = masterCallback;
+ this.ignoreDeletes = ignoreDeletes;
+ this.rev = '';
+ this.current = new Map();
+ this.callbacks = new Map;
+ this.handleValue = (val, key) => {
+ let { current, callbacks } = this;
+ if (val === null) {
+ if (!this.ignoreDeletes) {
+ current.delete(key);
+ callbacks.delete(key);
+ }
+ }
+ else {
+ current.set(key, val);
+ }
+ this.rev = guid();
+ if (this.masterCallback) {
+ this.masterCallback(val, key);
+ }
+ };
+ }
+ createRef(key) {
+ let refCallback = this.callbacks.get(key);
+ if (!refCallback) {
+ refCallback = (val) => {
+ this.handleValue(val, key);
+ };
+ this.callbacks.set(key, refCallback);
+ }
+ return refCallback;
+ }
+ }
+
+ class Ruler extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.elRef = M$1();
+ }
+ render() {
+ return (u$1("div", { ref: this.elRef }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ const { props } = this;
+ const el = this.elRef.current;
+ this.disconnectWidth = watchWidth(el, (width) => {
+ if (this._isUnmounting)
+ return;
+ setRef(props.widthRef, width);
+ });
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.disconnectWidth();
+ const { props } = this;
+ if (props.widthRef) {
+ setRef(props.widthRef, null);
+ }
+ }
+ }
+
+ /*
+ We need really specific keys because RefMap::createRef() which is then given to heightRef
+ unable to change key! As a result, we cannot reuse elements between normal/slice/standin types,
+ but that's okay since they render quite differently
+ */
+ function getEventPartKey(seg) {
+ return getEventKey(seg) + ':' + seg.start +
+ (seg.standinFor ? ':standin' : seg.isSlice ? ':slice' : '');
+ }
+ // DayGridRange utils (TODO: move)
+ // -------------------------------------------------------------------------------------------------
+ function splitSegsByRow(segs, rowCount) {
+ const byRow = [];
+ for (let row = 0; row < rowCount; row++) {
+ byRow[row] = [];
+ }
+ for (const seg of segs) {
+ byRow[seg.row].push(seg);
+ }
+ return byRow;
+ }
+ function splitInteractionByRow(ui, rowCount) {
+ const byRow = [];
+ if (!ui) {
+ for (let row = 0; row < rowCount; row++) {
+ byRow[row] = null;
+ }
+ }
+ else {
+ for (let row = 0; row < rowCount; row++) {
+ byRow[row] = {
+ affectedInstances: ui.affectedInstances,
+ isEvent: ui.isEvent,
+ segs: [],
+ };
+ }
+ for (const seg of ui.segs) {
+ byRow[seg.row].segs.push(seg);
+ }
+ }
+ return byRow;
+ }
+ function sliceSegForCol(seg, col) {
+ return {
+ ...seg,
+ start: col,
+ end: col + 1,
+ isStart: seg.isStart && seg.start === col,
+ isEnd: seg.isEnd && seg.end - 1 === col,
+ standinFor: seg,
+ };
+ }
+
+ class BgEvent extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.buildPublicEvent = memoize((context, eventDef, eventInstance) => new EventImpl(context, eventDef, eventInstance));
+ this.handleEl = (el) => {
+ this.el = el;
+ if (el) {
+ setElEventRange(el, this.props.eventRange);
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { eventRange } = props;
+ const { options } = context;
+ const eventUi = eventRange.ui;
+ const eventApi = this.buildPublicEvent(context, eventRange.def, eventRange.instance);
+ const subcontentRenderProps = {
+ event: eventApi,
+ isNarrow: props.isNarrow || false,
+ isShort: props.isShort || false,
+ };
+ const renderProps = {
+ event: eventApi,
+ view: context.viewApi,
+ timeText: '', // never display time
+ color: eventUi.color || options.backgroundEventColor,
+ contrastColor: eventUi.contrastColor,
+ isDraggable: false,
+ isStartResizable: false,
+ isEndResizable: false,
+ isMirror: false,
+ isStart: props.isStart,
+ isEnd: props.isEnd,
+ isFirst: false,
+ isLast: false,
+ isPast: props.isPast,
+ isFuture: props.isFuture,
+ isToday: props.isToday,
+ isSelected: false,
+ isDragging: false,
+ isResizing: false,
+ isInteractive: false,
+ level: 0,
+ isNarrow: props.isNarrow || false,
+ isShort: props.isShort || false,
+ timeClass: '', // never display time
+ titleClass: generateClassName(options.backgroundEventTitleClass, subcontentRenderProps),
+ options: { eventOverlap: Boolean(options.eventOverlap) },
+ };
+ // does not include backgroundEventClass.. added below
+ const outerClassName = joinClassNames(eventUi.className, classNames.fill, classNames.internalEvent, classNames.internalBgEvent, props.isVertical ? classNames.flexCol : classNames.flexRow);
+ const innerClassName = joinClassNames(generateClassName(options.backgroundEventInnerClass, renderProps), classNames.liquid);
+ return (u$1(ContentContainer, { tag: 'div', className: outerClassName, style: {
+ '--fc-event-color': renderProps.color,
+ '--fc-event-contrast-color': renderProps.contrastColor,
+ }, defaultGenerator: renderInnerContent$1, elRef: this.handleEl, renderProps: renderProps, generatorName: "backgroundEventContent", customGenerator: options.backgroundEventContent, classNameGenerator: options.backgroundEventClass, didMount: options.backgroundEventDidMount, willUnmount: options.backgroundEventWillUnmount, children: (InnerContent) => (u$1(InnerContent, { tag: 'div', className: innerClassName })) }));
+ }
+ componentDidUpdate(prevProps) {
+ if (this.el && this.props.eventRange !== prevProps.eventRange) {
+ setElEventRange(this.el, this.props.eventRange);
+ }
+ }
+ }
+ function renderInnerContent$1(props) {
+ let { title } = props.event;
+ return title && (u$1("div", { className: props.titleClass, children: props.event.title }));
+ }
+ // Other types of fills
+ // -------------------------------------------------------------------------------------------------
+ function renderFill(fillType, options) {
+ return (u$1("div", { className: joinClassNames(fillType === 'non-business' ? options.nonBusinessHoursClass :
+ fillType === 'highlight' ? options.highlightClass : undefined, classNames.fill) }));
+ }
+
+ const SPACE_FROM_VIEWPORT = 10;
+ const ROW_BORDER_WIDTH = 1;
+ class MorePopover extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.getDateMeta = memoize(getDateMeta);
+ this.closeRef = M$1();
+ this.focusStartRef = M$1();
+ this.focusEndRef = M$1();
+ this.handleRootEl = (rootEl) => {
+ this.rootEl = rootEl;
+ if (rootEl) {
+ this.context.registerInteractiveComponent(this, {
+ el: rootEl,
+ useEventCenter: false,
+ });
+ }
+ else {
+ this.context.unregisterInteractiveComponent(this);
+ }
+ };
+ // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
+ this.handleDocumentMouseDown = (ev) => {
+ // only hide the popover if the click happened outside the popover
+ const target = getEventTargetViaRoot(ev);
+ if (!this.rootEl.contains(target)) {
+ this.handleClose();
+ }
+ };
+ this.handleDocumentKeyDown = (ev) => {
+ if (ev.key === 'Escape') {
+ this.handleClose();
+ }
+ };
+ // for many different close techniques
+ // cannot accept params because might receive a browser Event
+ this.handleClose = () => {
+ let { onClose } = this.props;
+ if (onClose) {
+ onClose();
+ }
+ };
+ }
+ render() {
+ let { props, context } = this;
+ let { options, dateEnv, viewApi } = context;
+ let { startDate, todayRange, dateProfile } = props;
+ let dateMeta = this.getDateMeta(startDate, dateEnv, dateProfile, todayRange);
+ let textParts = dateEnv.formatToParts(startDate, options.popoverFormat);
+ let text = joinDateTimeFormatParts(textParts);
+ const dayHeaderRenderProps = {
+ ...dateMeta,
+ isMajor: false,
+ isNarrow: false,
+ isSticky: false,
+ inPopover: true,
+ level: 0,
+ hasNavLink: false,
+ text,
+ textParts,
+ get weekdayText() { return findWeekdayText(textParts); },
+ get dayNumberText() { return findDayNumberText(textParts); },
+ view: viewApi,
+ // TODO: should know about the resource!
+ };
+ const dayCellRenderProps = {
+ ...dateMeta,
+ isMajor: false,
+ isNarrow: false,
+ inPopover: true,
+ hasNavLink: false,
+ get weekdayText() { return findWeekdayText(textParts); },
+ get dayNumberText() { return findDayNumberText(textParts); },
+ get monthText() { return findMonthText(textParts); },
+ view: viewApi,
+ text: '',
+ textParts: [],
+ options: { businessHours: Boolean(options.businessHours) },
+ };
+ const fullDateStr = formatDayString(startDate);
+ /*
+ TODO: DRY with TimelineHeaderCell
+ */
+ const { dayHeaderAlign } = options;
+ const align = typeof dayHeaderAlign === 'function'
+ ? dayHeaderAlign({ level: 0, inPopover: true, isNarrow: false })
+ : dayHeaderAlign;
+ const isRtl = computeElIsRtl(props.alignEl);
+ return $(u$1("div", { "data-date": fullDateStr, id: props.id, role: 'dialog', "aria-labelledby": props.titleId, className: joinClassNames(options.popoverClass, classNames.flexCol, classNames.popoverZ, classNames.abs, classNames.borderBoxRoot, classNames.internalPopover), style: {
+ // positioning is mutated directly in updateSize, HOWEVER, we don't want popover to start
+ // low on screen because might cause unnecessary scrollbars
+ top: 0,
+ left: 0,
+ },
+ // HACK because of portal
+ dir: isRtl ? 'rtl' : undefined, "data-color-scheme": options.colorScheme || undefined, ref: this.handleRootEl, children: [u$1("div", { tabIndex: 0, style: { outline: 'none' }, ref: this.focusStartRef }), u$1("div", { className: joinClassNames(generateClassName(options.dayHeaderClass, dayHeaderRenderProps), classNames.flexCol, classNames.borderOnlyB, align === 'center' ? classNames.alignCenter :
+ align === 'end' ? classNames.alignEnd :
+ classNames.alignStart), children: [u$1("div", { children: u$1(ContentContainer, { tag: "div", attrs: {
+ id: props.titleId,
+ // NOTE: more-popover never has nav-links
+ }, generatorName: "dayHeaderContent", renderProps: dayHeaderRenderProps, customGenerator: options.dayHeaderContent, defaultGenerator: renderText, classNameGenerator: options.dayHeaderInnerClass, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }) }), u$1(ContentContainer, { tag: 'button', attrs: {
+ 'aria-label': options.closeHint,
+ ...createAriaClickAttrs(this.handleClose)
+ }, elRef: this.closeRef, className: joinClassNames(options.popoverCloseClass, classNames.flexRow, classNames.cursorPointer), renderProps: {}, customGenerator: options.popoverCloseContent, generatorName: 'popoverCloseContent' })] }), u$1("div", { className: joinClassNames(generateClassName(options.dayCellClass, dayCellRenderProps), classNames.flexCol, classNames.borderNone), children: u$1("div", { className: generateClassName(options.dayCellInnerClass, dayCellRenderProps), children: props.children }) }), u$1("div", { tabIndex: 0, style: { outline: 'none' }, ref: this.focusEndRef })] }), getAppendableRoot(props.alignEl));
+ }
+ queryHit(isRtl, positionLeft, positionTop, elWidth, elHeight) {
+ let { rootEl, props } = this;
+ if (positionLeft >= 0 && positionLeft < elWidth &&
+ positionTop >= 0 && positionTop < elHeight) {
+ return {
+ dateProfile: props.dateProfile,
+ dateSpan: {
+ allDay: !props.forceTimed,
+ range: {
+ start: props.startDate,
+ end: props.endDate,
+ },
+ ...props.dateSpanProps,
+ },
+ getDayEl: () => rootEl,
+ rect: {
+ left: 0,
+ top: 0,
+ right: elWidth,
+ bottom: elHeight,
+ },
+ layer: 1, // important when comparing with hits from other components
+ };
+ }
+ return null;
+ }
+ componentDidMount() {
+ document.addEventListener('mousedown', this.handleDocumentMouseDown);
+ document.addEventListener('keydown', this.handleDocumentKeyDown);
+ this.focusStartRef.current.addEventListener('focus', this.handleClose);
+ this.focusEndRef.current.addEventListener('focus', this.handleClose);
+ this.closeRef.current.focus({ preventScroll: true });
+ this.updateSize();
+ }
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleDocumentMouseDown);
+ document.removeEventListener('keydown', this.handleDocumentKeyDown);
+ this.focusStartRef.current.removeEventListener('focus', this.handleClose);
+ this.focusEndRef.current.removeEventListener('focus', this.handleClose);
+ }
+ updateSize() {
+ let { alignEl, alignParentTop } = this.props;
+ let { rootEl: popoverEl } = this;
+ const isRtl = computeElIsRtl(alignEl);
+ // position relative to viewport
+ const alignmentRect = computeClippedClientRect(alignEl);
+ if (alignmentRect) {
+ let popoverDims = popoverEl.getBoundingClientRect();
+ // position relative to viewport
+ let popoverVPTop = alignParentTop
+ // HACK: subtract 1 for DayGrid, which has borders on row-bottom. Only view that uses alignParentTop
+ ? alignEl.closest(alignParentTop).getBoundingClientRect().top - ROW_BORDER_WIDTH
+ : alignmentRect.top;
+ let popoverVPLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left;
+ // constrain
+ popoverVPTop = Math.max(popoverVPTop, SPACE_FROM_VIEWPORT);
+ popoverVPLeft = Math.min(popoverVPLeft, document.documentElement.clientWidth - SPACE_FROM_VIEWPORT - popoverDims.width);
+ popoverVPLeft = Math.max(popoverVPLeft, SPACE_FROM_VIEWPORT);
+ const { offsetParent } = popoverEl;
+ // final popover position, relative to offsetParent
+ let top;
+ let left;
+ // TODO: account for RTL
+ if (!offsetParent || offsetParent === document.body) {
+ top = popoverVPTop + window.scrollY;
+ left = popoverVPLeft + window.scrollX;
+ }
+ else {
+ const offsetParentRect = offsetParent.getBoundingClientRect();
+ top = popoverVPTop - offsetParentRect.top + offsetParent.scrollTop;
+ left = popoverVPLeft - offsetParentRect.left + offsetParent.scrollLeft;
+ }
+ applyStyle(popoverEl, { top, left });
+ }
+ }
+ }
+ // TODO: DRY
+ function renderText(renderProps) {
+ return renderProps.text;
+ }
+
+ function doCoordRangesIntersect(r0, r1) {
+ return r0.end > r1.start && r0.start < r1.end;
+ }
+ function intersectCoordRanges(r0, r1) {
+ const start = Math.max(r0.start, r1.start);
+ const end = Math.min(r0.end, r1.end);
+ if (start < end) {
+ return {
+ start,
+ end,
+ isStart: r0.isStart && start === r0.start,
+ isEnd: r0.isEnd && end === r0.end,
+ };
+ }
+ }
+ function joinCoordRanges(r0, r1) {
+ return {
+ start: Math.min(r0.start, r1.start),
+ end: Math.max(r0.end, r1.end),
+ };
+ }
+ function getCoordRangeEnd(r) {
+ return r.end;
+ }
+ // { eventRange }
+ // -------------------------------------------------------------------------------------------------
+ function computeEarliestStart(segs) {
+ return segs.reduce(pickEarliestStart).eventRange.range.start;
+ }
+ function computeLatestEnd(segs) {
+ return segs.reduce(pickLatestEnd).eventRange.range.end;
+ }
+ function pickEarliestStart(r0, r1) {
+ return r0.eventRange.range.start < r1.eventRange.range.start ? r0 : r1;
+ }
+ function pickLatestEnd(r0, r1) {
+ return r0.eventRange.range.end > r1.eventRange.range.end ? r0 : r1;
+ }
+
+ /*
+ IMPORTANT: caller is responsible for injecting moreLinkInnerClass,
+ either on root `classNames` or within inner element
+ */
+ class MoreLinkContainer extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {
+ isPopoverOpen: false,
+ };
+ this.handleLinkEl = (linkEl) => {
+ this.linkEl = linkEl;
+ if (this.props.elRef) {
+ setRef(this.props.elRef, linkEl);
+ }
+ };
+ this.handleClick = (ev) => {
+ let { props, context } = this;
+ let { dateEnv, options } = context;
+ let { moreLinkClick } = options;
+ let date = computeRange(props).start;
+ function buildPublicSeg(seg) {
+ let { def, instance, range } = seg.eventRange;
+ return {
+ event: new EventImpl(context, def, instance),
+ start: dateEnv.toDate(range.start),
+ end: dateEnv.toDate(range.end),
+ isStart: seg.isStart,
+ isEnd: seg.isEnd,
+ };
+ }
+ if (typeof moreLinkClick === 'function') {
+ moreLinkClick = moreLinkClick({
+ date: dateEnv.toDate(date),
+ allDay: Boolean(props.allDayDate),
+ allSegs: props.segs.map(buildPublicSeg),
+ hiddenSegs: props.hiddenSegs.map(buildPublicSeg),
+ jsEvent: ev,
+ view: context.viewApi,
+ });
+ }
+ if (!moreLinkClick || moreLinkClick === 'popover') {
+ this.setState({ isPopoverOpen: true });
+ }
+ else if (typeof moreLinkClick === 'string') { // a view name
+ context.calendarApi.zoomTo(date, moreLinkClick);
+ }
+ };
+ this.handlePopoverClose = () => {
+ if (this.linkEl) { // was null sometimes when initiating drag-n-drop would hide the popover
+ this.linkEl.focus();
+ }
+ this.setState({ isPopoverOpen: false });
+ };
+ }
+ render() {
+ let { props, state } = this;
+ return (u$1(ViewContextType.Consumer, { children: (context) => {
+ let { viewApi, options, calendarApi, baseId } = context;
+ let { moreLinkText } = options;
+ let moreCnt = props.hiddenSegs.length;
+ let range = computeRange(props);
+ let popoverId = baseId + 'popover-' + range.start.toISOString();
+ let numericText = `+${moreCnt}`; // TODO: offer hook or i18n?
+ let longText = typeof moreLinkText === 'function' // TODO: eventually use formatWithOrdinals
+ ? moreLinkText.call(calendarApi, moreCnt)
+ : `${numericText} ${moreLinkText}`;
+ let hint = formatWithOrdinals(options.moreLinkHint, [moreCnt], longText);
+ let renderProps = {
+ num: moreCnt,
+ numericText,
+ longText,
+ text: (props.isMicro || props.display === 'column') ? numericText : longText,
+ isNarrow: props.isNarrow,
+ view: viewApi,
+ };
+ return (u$1(S, { children: [Boolean(moreCnt) && (u$1(ContentContainer, { tag: 'div', elRef: this.handleLinkEl, className: joinClassNames(generateClassName(// will added to moreLinkClass
+ props.display === 'row'
+ ? options.rowMoreLinkClass // row
+ : options.columnMoreLinkClass, // column
+ renderProps), props.className, props.display === 'row'
+ ? classNames.flexRow
+ : classNames.flexCol, classNames.internalMoreLink, classNames.cursorPointer), style: props.style, attrs: {
+ ...props.attrs,
+ ...createAriaClickAttrs(this.handleClick),
+ title: hint,
+ 'role': 'button',
+ 'aria-haspopup': 'dialog',
+ 'aria-expanded': state.isPopoverOpen,
+ 'aria-controls': state.isPopoverOpen ? popoverId : undefined,
+ }, renderProps: renderProps, generatorName: "moreLinkContent", customGenerator: options.moreLinkContent, defaultGenerator: renderMoreLinkText, classNameGenerator: options.moreLinkClass, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount, children: (InnerContent) => (u$1(InnerContent, { tag: 'div', className: joinClassNames(generateClassName(options.moreLinkInnerClass, renderProps), generateClassName(props.display === 'row'
+ ? options.rowMoreLinkInnerClass // row
+ : options.columnMoreLinkInnerClass, // column
+ renderProps), props.display === 'row'
+ ? classNames.stickyS
+ : classNames.stickyT) })) })), state.isPopoverOpen && (u$1(MorePopover, { id: popoverId, titleId: popoverId + '-title', startDate: range.start, endDate: range.end, dateProfile: props.dateProfile, todayRange: props.todayRange, dateSpanProps: props.dateSpanProps, alignEl: props.alignElRef ?
+ props.alignElRef.current :
+ this.linkEl, alignParentTop: props.alignParentTop, forceTimed: props.forceTimed, onClose: this.handlePopoverClose, children: props.popoverContent() }))] }));
+ } }));
+ }
+ }
+ function renderMoreLinkText(props) {
+ return props.text;
+ }
+ function computeRange(props) {
+ if (props.allDayDate) {
+ return {
+ start: props.allDayDate,
+ end: addDays(props.allDayDate, 1),
+ };
+ }
+ return {
+ start: computeEarliestStart(props.hiddenSegs),
+ end: computeLatestEnd(props.hiddenSegs),
+ };
+ }
+
+ const DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: true,
+ meridiem: 'narrow',
+ });
+ function hasListItemDisplay(seg) {
+ let { display } = seg.eventRange.ui;
+ return display === 'list-item' || (display === 'auto' &&
+ !seg.eventRange.def.allDay &&
+ (seg.end - seg.start) === 1 && // single-day
+ seg.isStart && // "
+ seg.isEnd // "
+ );
+ }
+
+ class DayGridMoreLink extends BaseComponent {
+ render() {
+ let { props } = this;
+ return (u$1(MoreLinkContainer, { display: 'row', className: props.className, isNarrow: props.isNarrow, isMicro: props.isMicro, dateProfile: props.dateProfile, todayRange: props.todayRange, allDayDate: props.allDayDate, segs: props.segs, hiddenSegs: props.hiddenSegs, alignElRef: props.alignElRef, alignParentTop: props.alignParentTop, dateSpanProps: props.dateSpanProps, popoverContent: () => (u$1(S, { children: props.segs.map((seg) => {
+ let { eventRange } = seg;
+ let { instanceId } = eventRange.instance;
+ let isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
+ let isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
+ let isInvisible = isDragging || isResizing;
+ return (u$1("div", { style: {
+ visibility: isInvisible ? 'hidden' : undefined,
+ }, children: u$1(StandardEvent, { display: hasListItemDisplay(seg) ? 'list-item' : 'row', eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: false, isSelected: instanceId === props.eventSelection, defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: false, ...getEventRangeMeta(eventRange, props.todayRange) }) }, instanceId));
+ }) })) }));
+ }
+ }
+
+ class DayGridCell extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.getDateMeta = memoize(getDateMeta);
+ this.refineRenderProps = memoizeObjArg(refineRenderProps);
+ // ref
+ this.rootElRef = M$1();
+ this.handleBodyEl = (bodyEl) => {
+ if (this.disconnectBodyHeight) {
+ this.disconnectBodyHeight();
+ this.disconnectBodyHeight = undefined;
+ setRef(this.props.headerHeightRef, null);
+ setRef(this.props.mainHeightRef, null);
+ }
+ if (bodyEl) {
+ // we want to fire on ANY size change, because we do more advanced stuff
+ this.disconnectBodyHeight = watchSize(bodyEl, (_bodyWidth, bodyHeight) => {
+ if (this._isUnmounting)
+ return;
+ const { props } = this;
+ const mainRect = bodyEl.getBoundingClientRect();
+ const rootRect = this.rootElRef.current.getBoundingClientRect();
+ const headerHeight = mainRect.top - rootRect.top;
+ if (!isDimsEqual(this.headerHeight, headerHeight)) {
+ this.headerHeight = headerHeight;
+ setRef(props.headerHeightRef, headerHeight);
+ }
+ if (props.fgLiquidHeight) {
+ setRef(props.mainHeightRef, bodyHeight);
+ }
+ });
+ }
+ };
+ }
+ render() {
+ let { props, context } = this;
+ let { options, dateEnv } = context;
+ // TODO: memoize this
+ const isMonthStart = props.showDayNumber &&
+ shouldDisplayMonthStart(props.date, props.dateProfile.currentRange, dateEnv);
+ const dateMeta = this.getDateMeta(props.date, dateEnv, props.dateProfile, props.todayRange);
+ const baseClassName = joinClassNames(props.borderStart ? classNames.borderOnlyS : classNames.borderNone, props.width != null ? '' : classNames.liquid, classNames.flexCol, classNames.noMargin, classNames.noPadding);
+ const hasNavLink = options.navLinks;
+ const renderProps = this.refineRenderProps({
+ date: props.date,
+ isMajor: props.isMajor,
+ isNarrow: props.isNarrow,
+ dateMeta: dateMeta,
+ hasLabel: props.showDayNumber,
+ hasMonthLabel: isMonthStart,
+ hasNavLink,
+ renderProps: props.renderProps,
+ viewApi: context.viewApi,
+ dateEnv: context.dateEnv,
+ monthStartFormat: options.monthStartFormat,
+ dayCellFormat: options.dayCellFormat,
+ businessHours: Boolean(options.businessHours),
+ });
+ if (dateMeta.isDisabled) {
+ return (u$1("div", { role: 'gridcell', "aria-disabled": true, className: joinClassNames(generateClassName(options.dayCellClass, renderProps), props.className, baseClassName), style: {
+ width: props.width
+ } }));
+ }
+ const fullDateStr = buildDateStr(context, props.date);
+ return (u$1(ContentContainer, { tag: "div", elRef: this.rootElRef, className: joinClassNames(props.className, baseClassName), attrs: {
+ ...props.attrs,
+ role: 'gridcell',
+ 'aria-label': fullDateStr,
+ ...(renderProps.isToday ? { 'aria-current': 'date' } : {}),
+ 'data-date': formatDayString(props.date),
+ }, style: {
+ width: props.width,
+ }, renderProps: renderProps, generatorName: "dayCellTopContent" // !!! for top
+ , customGenerator: options.dayCellTopContent /* !!! for top */, defaultGenerator: renderTopInner, classNameGenerator: options.dayCellClass, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount, children: (InnerContent) => (u$1(S, { children: [u$1("div", { className: joinClassNames(classNames.rel, // puts it above bg-fills, which are positioned on TOP of this component :|
+ generateClassName(options.dayCellTopClass, renderProps)), children: props.showDayNumber && (u$1(InnerContent // the dayCellTopContent
+ , { tag: 'div', attrs: hasNavLink
+ ? buildNavLinkAttrs(context, props.date, undefined, fullDateStr)
+ : { 'aria-hidden': true } // label already on cell
+ , className: generateClassName(options.dayCellTopInnerClass, renderProps) })) }), u$1("div", { className: joinClassNames(classNames.flexCol, props.fgLiquidHeight ? classNames.liquid : classNames.grow), ref: this.handleBodyEl, children: [u$1("div", { className: generateClassName(options.dayCellInnerClass, renderProps), style: { minHeight: props.fgHeight }, children: props.fg }), u$1(DayGridMoreLink, { className: classNames.rel, allDayDate: props.date, segs: props.segs, hiddenSegs: props.hiddenSegs, alignElRef: this.rootElRef, alignParentTop: props.showDayNumber
+ ? '[role=row]'
+ : `.${classNames.internalView}`, dateSpanProps: props.dateSpanProps, dateProfile: props.dateProfile, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, isNarrow: props.isNarrow, isMicro: props.isMicro })] }), u$1("div", { className: joinClassNames(classNames.rel, // puts it above bg-fills
+ generateClassName(options.dayCellBottomClass, renderProps)) })] })) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ }
+ }
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function renderTopInner(props) {
+ return props.text || u$1(S, { children: "\u00A0" }); // TODO: DRY?
+ }
+ function shouldDisplayMonthStart(date, currentRange, dateEnv) {
+ const { start: currentStart, end: currentEnd } = currentRange;
+ const currentEndIncl = addMs(currentEnd, -1);
+ const currentFirstYear = dateEnv.getYear(currentStart);
+ const currentFirstMonth = dateEnv.getMonth(currentStart);
+ const currentLastYear = dateEnv.getYear(currentEndIncl);
+ const currentLastMonth = dateEnv.getMonth(currentEndIncl);
+ // spans more than one month?
+ return !(currentFirstYear === currentLastYear && currentFirstMonth === currentLastMonth) &&
+ Boolean(
+ // first date in current view?
+ date.valueOf() === currentStart.valueOf() ||
+ // a month-start that's within the current range?
+ (dateEnv.getDay(date) === 1 && date.valueOf() < currentEnd.valueOf()));
+ }
+ function refineRenderProps(raw) {
+ let { date, dateEnv, hasLabel, hasMonthLabel, hasNavLink, businessHours } = raw;
+ let textParts = [];
+ let text = '';
+ if (hasLabel) {
+ textParts = dateEnv.formatToParts(date, hasMonthLabel ? raw.monthStartFormat : raw.dayCellFormat);
+ text = joinDateTimeFormatParts(textParts);
+ }
+ return {
+ ...raw.dateMeta,
+ ...raw.renderProps,
+ text,
+ textParts,
+ isMajor: raw.isMajor,
+ isNarrow: raw.isNarrow,
+ inPopover: false,
+ hasNavLink,
+ get weekdayText() { return findWeekdayText(textParts); },
+ get dayNumberText() { return findDayNumberText(textParts); },
+ get monthText() { return findMonthText(textParts); },
+ options: { businessHours },
+ view: raw.viewApi,
+ };
+ }
+
+ class SegHierarchy {
+ constructor(segs, getSegThickness = (seg) => {
+ return 1;
+ }, strictOrder = false, // HACK
+ maxCoord, maxDepth, hiddenConsumes = false, // hidden segs also hide the touchingPlacement?
+ allowSlicing = false) {
+ this.getSegThickness = getSegThickness;
+ this.strictOrder = strictOrder;
+ this.maxCoord = maxCoord;
+ this.maxDepth = maxDepth;
+ this.hiddenConsumes = hiddenConsumes;
+ this.allowSlicing = allowSlicing;
+ this.placementsByLevel = [];
+ this.levelCoords = []; // parallel with placementsByLevel
+ this.hiddenSegs = [];
+ for (const seg of segs) {
+ this.insertSeg(seg, this.getSegThickness(seg));
+ }
+ }
+ insertSeg(seg, segThickness, isSlice) {
+ if (segThickness != null) {
+ const insertion = this.findInsertion(seg, segThickness);
+ if (this.isInsertionValid(insertion, segThickness)) {
+ this.insertSegAt(seg, insertion, segThickness, isSlice);
+ }
+ else {
+ const { touchingPlacement } = insertion;
+ // is there a touching-seg?
+ if (touchingPlacement) {
+ // should we hide or reslice touchingPlacement?
+ if (this.hiddenConsumes && !touchingPlacement.isZombie) {
+ touchingPlacement.isZombie = true; // edit in-place
+ this.hiddenSegs.push(touchingPlacement);
+ if (this.allowSlicing) {
+ const newSeg = Object.assign({}, touchingPlacement); // copy
+ // slice touchingPlacement in-place
+ Object.assign(touchingPlacement, intersectCoordRanges(touchingPlacement, seg));
+ touchingPlacement.isSlice = true;
+ // try to reinsert touchingPlacement's seg
+ this.splitSeg(newSeg, touchingPlacement.thickness, touchingPlacement);
+ }
+ }
+ // record seg as hidden, potentially split by touchingPlacement
+ if (this.allowSlicing) {
+ this.hiddenSegs.push({
+ ...seg,
+ ...intersectCoordRanges(seg, touchingPlacement),
+ });
+ this.splitSeg(seg, segThickness, touchingPlacement);
+ }
+ else {
+ this.hiddenSegs.push(seg);
+ }
+ // not touching anything
+ }
+ else {
+ this.hiddenSegs.push(seg);
+ }
+ }
+ }
+ }
+ /*
+ TODO: inline?
+ */
+ isInsertionValid(insertion, thickness) {
+ return (this.maxCoord == null || insertion.levelCoord + thickness <= this.maxCoord) &&
+ (this.maxDepth == null || insertion.depth < this.maxDepth);
+ }
+ /*
+ Does not add the portion that intersects with barrier to hiddenSegs
+ */
+ splitSeg(seg, segThickness, barrier) {
+ // any leftover seg on the start-side of the barrier?
+ if (seg.start < barrier.start) {
+ this.insertSeg({ ...seg, end: barrier.start, isEnd: false }, segThickness,
+ /* isSlice = */ true);
+ }
+ // any leftover seg on the end-side of the barrier?
+ if (seg.end > barrier.end) {
+ this.insertSeg({ ...seg, start: barrier.end, isStart: false }, segThickness,
+ /* isSlice = */ true);
+ }
+ }
+ /*
+ TODO: inline?
+ */
+ insertSegAt(seg, insertion, segThickness, isSlice) {
+ const placement = {
+ ...seg,
+ thickness: segThickness,
+ depth: insertion.depth,
+ isSlice: isSlice || seg.isSlice || false,
+ isZombie: false,
+ };
+ if (insertion.lateralIndex === -1) {
+ // create a new level
+ insertAt(this.placementsByLevel, insertion.levelIndex, [placement]);
+ insertAt(this.levelCoords, insertion.levelIndex, insertion.levelCoord);
+ }
+ else {
+ // insert into existing level
+ insertAt(this.placementsByLevel[insertion.levelIndex], insertion.lateralIndex, placement);
+ }
+ }
+ /*
+ Ignores limits
+ */
+ findInsertion(seg, segThickness) {
+ let { placementsByLevel, levelCoords } = this;
+ let levelCnt = placementsByLevel.length;
+ let candidateCoord = 0; // a tentative levelCoord for seg's placement
+ let touchingPlacement;
+ let touchingLevelIndex;
+ let depth = 0;
+ // iterate through existing levels
+ for (let currentLevelIndex = 0; currentLevelIndex < levelCnt; currentLevelIndex += 1) {
+ const currentLevelCoord = levelCoords[currentLevelIndex];
+ // if the current level has cleared seg's bottom coord, we have found a good empty space and can stop.
+ // if strictOrder, keep finding more lateral intersections.
+ if (!this.strictOrder && currentLevelCoord >= candidateCoord + segThickness) {
+ break;
+ }
+ let currentLevelSegs = placementsByLevel[currentLevelIndex];
+ let currentSeg;
+ // finds the first possible entry that seg could intersect with
+ let [searchIndex, isExact] = binarySearch(currentLevelSegs, seg.start, getCoordRangeEnd); // find first entry after seg's end
+ let lateralIndex = searchIndex + isExact; // if exact match (which doesn't collide), go to next one
+ // loop through entries that horizontally intersect
+ while ((currentSeg = currentLevelSegs[lateralIndex]) && // but not past the whole entry list
+ currentSeg.start < seg.end // and not entirely past seg
+ ) {
+ let currentEntryBottom = currentLevelCoord + currentSeg.thickness;
+ // intersects into the top of the candidate?
+ if (currentEntryBottom > candidateCoord) {
+ // push it downward so doesn't 'vertically' intersect anymore
+ candidateCoord = currentEntryBottom;
+ // tentatively record as touching
+ touchingPlacement = currentSeg;
+ touchingLevelIndex = currentLevelIndex;
+ }
+ // does current entry butt up against top of candidate?
+ // will obviously happen if just intersected, but can also happen if pushed down previously
+ // because intersected with a sibling
+ // TODO: after automated tests hooked up, see if these gate is unnecessary,
+ // we might just be able to do this for ALL intersecting currentEntries (this whole loop)
+ if (currentEntryBottom === candidateCoord) {
+ // accumulate the highest possible depth of the currentLevelSegs that butt up
+ depth = Math.max(depth, currentSeg.depth + 1);
+ }
+ lateralIndex += 1;
+ }
+ }
+ // the destination level will be after touchingPlacement's level. find it
+ // TODO: can reuse work from above?
+ let destLevelIndex = 0;
+ if (touchingPlacement) {
+ destLevelIndex = touchingLevelIndex + 1;
+ while (destLevelIndex < levelCnt && levelCoords[destLevelIndex] < candidateCoord) {
+ destLevelIndex += 1;
+ }
+ }
+ // if adding to an existing level, find where to insert
+ // TODO: can reuse work from above?
+ let destLateralIndex = -1;
+ if (destLevelIndex < levelCnt && levelCoords[destLevelIndex] === candidateCoord) {
+ [destLateralIndex] = binarySearch(placementsByLevel[destLevelIndex], seg.end, getCoordRangeEnd);
+ }
+ return {
+ touchingPlacement,
+ levelCoord: candidateCoord,
+ levelIndex: destLevelIndex,
+ lateralIndex: destLateralIndex,
+ depth,
+ };
+ }
+ traverseSegs(handler) {
+ const { placementsByLevel, levelCoords } = this;
+ for (let i = 0; i < placementsByLevel.length; i++) {
+ const placements = placementsByLevel[i];
+ const levelCoord = levelCoords[i];
+ for (const placement of placements) {
+ if (!placement.isZombie) {
+ handler(placement, levelCoord);
+ }
+ }
+ }
+ }
+ }
+ /*
+ Returns groups with entries sorted by input order
+ */
+ function groupIntersectingSegs(segs) {
+ let mergedGroups = [];
+ for (let seg of segs) {
+ let filteredGroups = [];
+ let hungryGroup = {
+ segs: [seg],
+ start: seg.start,
+ end: seg.end,
+ };
+ for (let mergedGroup of mergedGroups) {
+ if (doCoordRangesIntersect(mergedGroup, hungryGroup)) {
+ hungryGroup = {
+ ...joinCoordRanges(mergedGroup, hungryGroup),
+ segs: mergedGroup.segs.concat(hungryGroup.segs) // keep preexisting mergedGroup's items first. maintains order
+ };
+ }
+ else {
+ filteredGroups.push(mergedGroup);
+ }
+ }
+ filteredGroups.push(hungryGroup);
+ mergedGroups = filteredGroups;
+ }
+ return mergedGroups.map((mergedGroup) => {
+ return {
+ key: buildIsoString(computeEarliestStart(mergedGroup.segs)),
+ ...mergedGroup
+ };
+ });
+ }
+ // General Utils
+ // -------------------------------------------------------------------------------------------------
+ function insertAt(arr, index, item) {
+ arr.splice(index, 0, item);
+ }
+ function binarySearch(a, searchVal, getItemVal) {
+ let startIndex = 0;
+ let endIndex = a.length; // exclusive
+ if (!endIndex || searchVal < getItemVal(a[startIndex])) { // no items OR before first item
+ return [0, 0];
+ }
+ if (searchVal > getItemVal(a[endIndex - 1])) { // after last item
+ return [endIndex, 0];
+ }
+ while (startIndex < endIndex) {
+ let middleIndex = Math.floor(startIndex + (endIndex - startIndex) / 2);
+ let middleVal = getItemVal(a[middleIndex]);
+ if (searchVal < middleVal) {
+ endIndex = middleIndex;
+ }
+ else if (searchVal > middleVal) {
+ startIndex = middleIndex + 1;
+ }
+ else { // equal!
+ return [middleIndex, 1];
+ }
+ }
+ return [startIndex, 0];
+ }
+
+ function computeFgSegVerticals$1(segs, segHeightMap, cells, maxHeight, strictOrder, allowSlicing = true, dayMaxEvents, dayMaxEventRows) {
+ let maxCoord;
+ let maxDepth;
+ let hiddenConsumes;
+ if (dayMaxEvents === true || dayMaxEventRows === true) {
+ maxCoord = maxHeight;
+ hiddenConsumes = true;
+ }
+ else if (typeof dayMaxEvents === 'number') {
+ maxDepth = dayMaxEvents;
+ hiddenConsumes = false;
+ }
+ else if (typeof dayMaxEventRows === 'number') {
+ maxDepth = dayMaxEventRows;
+ hiddenConsumes = true;
+ }
+ // NOTE: visibleSegsMap and hiddenSegMap map NEVER overlap for a given event
+ // once a seg has a height, the combined potentially-sliced segs will comprise the entire span of the seg
+ // if a seg does not have a height yet, it won't be inserted into either visibleSegsMap/hiddenSegMap
+ const visibleSegMap = new Map();
+ const hiddenSegMap = new Map();
+ const segTops = new Map();
+ const isSlicedMap = new Map();
+ let hierarchy = new SegHierarchy(segs, (seg) => segHeightMap.get(getEventPartKey(seg)), strictOrder, maxCoord, maxDepth, hiddenConsumes, allowSlicing);
+ hierarchy.traverseSegs((seg, segTop) => {
+ addToSegMap(visibleSegMap, seg);
+ segTops.set(getEventPartKey(seg), segTop);
+ if (seg.isSlice) {
+ isSlicedMap.set(seg.eventRange, true);
+ }
+ });
+ for (const hiddenSeg of hierarchy.hiddenSegs) {
+ addToSegMap(hiddenSegMap, hiddenSeg); // hidden main segs
+ }
+ // recompute tops while considering slices
+ // portions of these slices might be added to hiddenSegMap
+ if (isSlicedMap.size) {
+ segTops.clear();
+ hierarchy = new SegHierarchy(compileSegMap(segs, visibleSegMap), (seg) => segHeightMap.get(getEventPartKey(seg)), strictOrder, maxCoord, maxDepth, hiddenConsumes);
+ hierarchy.traverseSegs((seg, segTop) => {
+ segTops.set(getEventPartKey(seg), segTop); // newly-hidden main segs and slices
+ });
+ for (const hiddenSeg of hierarchy.hiddenSegs) {
+ addToSegMap(hiddenSegMap, hiddenSeg);
+ }
+ }
+ const segsByCol = [];
+ const hiddenSegsByCol = [];
+ const renderableSegsByCol = [];
+ const heightsByCol = [];
+ for (let col = 0; col < cells.length; col++) {
+ segsByCol.push([]);
+ hiddenSegsByCol.push([]);
+ renderableSegsByCol.push([]);
+ heightsByCol.push(0);
+ }
+ for (const seg of segs) {
+ const { eventRange } = seg;
+ const visibleSegs = visibleSegMap.get(eventRange) || [];
+ const hiddenSegs = hiddenSegMap.get(eventRange) || [];
+ const isSliced = isSlicedMap.get(eventRange) || false;
+ // add orig to renderable
+ renderableSegsByCol[seg.start].push(seg);
+ // add slices to renderable
+ if (isSliced) {
+ for (const visibleSeg of visibleSegs) {
+ renderableSegsByCol[visibleSeg.start].push(visibleSeg);
+ }
+ }
+ // accumulate segsByCol/heightsByCol for visible segs
+ for (const visibleSeg of visibleSegs) {
+ for (let col = visibleSeg.start; col < visibleSeg.end; col++) {
+ const slice = sliceSegForCol(visibleSeg, col);
+ segsByCol[col].push(slice);
+ }
+ const segKey = getEventPartKey(visibleSeg);
+ const segTop = segTops.get(segKey);
+ if (segTop != null) { // positioned?
+ const segHeight = segHeightMap.get(segKey);
+ for (let col = visibleSeg.start; col < visibleSeg.end; col++) {
+ heightsByCol[col] = Math.max(heightsByCol[col], segTop + segHeight);
+ }
+ }
+ }
+ // accumulate segsByCol/hiddenSegsByCol for hidden segs
+ for (const hiddenSeg of hiddenSegs) {
+ for (let col = hiddenSeg.start; col < hiddenSeg.end; col++) {
+ const slice = sliceSegForCol(hiddenSeg, col);
+ segsByCol[col].push(slice);
+ hiddenSegsByCol[col].push(slice);
+ }
+ }
+ }
+ return [
+ segsByCol, // visible and hidden
+ hiddenSegsByCol,
+ renderableSegsByCol,
+ segTops,
+ heightsByCol,
+ ];
+ }
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function addToSegMap(map, seg) {
+ let list = map.get(seg.eventRange);
+ if (!list) {
+ map.set(seg.eventRange, list = []);
+ }
+ list.push(seg);
+ }
+ /*
+ Ensures relative order of DayRowEventRange stays consistent with segs
+ */
+ function compileSegMap(segs, segMap) {
+ const res = [];
+ for (const seg of segs) {
+ res.push(...(segMap.get(seg.eventRange) || []));
+ }
+ return res;
+ }
+
+ class DaySeriesModel {
+ constructor(range, dateProfileGenerator) {
+ let date = range.start;
+ let { end } = range;
+ let indices = [];
+ let dates = [];
+ let dayIndex = -1;
+ while (date < end) { // loop each day from start to end
+ if (dateProfileGenerator.isHiddenDay(date)) {
+ indices.push(dayIndex + 0.5); // mark that it's between indices
+ }
+ else {
+ dayIndex += 1;
+ indices.push(dayIndex);
+ dates.push(date);
+ }
+ date = addDays(date, 1);
+ }
+ this.dates = dates;
+ this.indices = indices;
+ this.cnt = dates.length;
+ }
+ sliceRange(range) {
+ let firstIndex = this.getDateDayIndex(range.start); // inclusive first index
+ let lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index
+ let clippedFirstIndex = Math.max(0, firstIndex);
+ let clippedLastIndex = Math.min(this.cnt - 1, lastIndex);
+ // deal with in-between indices
+ clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell
+ clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell
+ if (clippedFirstIndex <= clippedLastIndex) {
+ return {
+ start: clippedFirstIndex,
+ end: clippedLastIndex + 1, // make exclusive
+ isStart: firstIndex === clippedFirstIndex,
+ isEnd: lastIndex === clippedLastIndex,
+ };
+ }
+ return null;
+ }
+ // Given a date, returns its chronolocial cell-index from the first cell of the grid.
+ // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
+ // If before the first offset, returns a negative number.
+ // If after the last offset, returns an offset past the last cell offset.
+ // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
+ getDateDayIndex(date) {
+ let { indices } = this;
+ let dayOffset = Math.floor(diffDays(this.dates[0], date));
+ if (dayOffset < 0) {
+ return indices[0] - 1;
+ }
+ if (dayOffset >= indices.length) {
+ return indices[indices.length - 1] + 1;
+ }
+ return indices[dayOffset];
+ }
+ }
+
+ class DayTableModel {
+ constructor(daySeries, breakOnWeeks, dateEnv, majorUnit = '') {
+ this.dateEnv = dateEnv;
+ this.majorUnit = majorUnit;
+ let { dates } = daySeries;
+ let daysPerRow;
+ let firstDay;
+ let rowCount;
+ if (breakOnWeeks) {
+ // count columns until the day-of-week repeats
+ firstDay = dates[0].getUTCDay();
+ for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) {
+ if (dates[daysPerRow].getUTCDay() === firstDay) {
+ break;
+ }
+ }
+ rowCount = Math.ceil(dates.length / daysPerRow);
+ }
+ else {
+ rowCount = 1;
+ daysPerRow = dates.length;
+ }
+ this.rowCount = rowCount;
+ this.colCount = daysPerRow;
+ this.daySeries = daySeries;
+ this.cellRows = this.buildCells();
+ this.headerDates = this.buildHeaderDates();
+ }
+ buildCells() {
+ let rows = [];
+ for (let row = 0; row < this.rowCount; row += 1) {
+ let cells = [];
+ for (let col = 0; col < this.colCount; col += 1) {
+ cells.push(this.buildCell(row, col));
+ }
+ rows.push(cells);
+ }
+ return rows;
+ }
+ buildCell(row, col) {
+ let date = this.daySeries.dates[row * this.colCount + col];
+ return {
+ key: date.toISOString(),
+ date,
+ isMajor: this.cellIsMajor(date),
+ };
+ }
+ cellIsMajor(dateMarker) {
+ return this.majorUnit ? isMajorUnit(dateMarker, this.majorUnit, this.dateEnv) : false;
+ }
+ buildHeaderDates() {
+ let dates = [];
+ for (let col = 0; col < this.colCount; col += 1) {
+ dates.push(this.cellRows[0][col].date);
+ }
+ return dates;
+ }
+ sliceRange(range) {
+ let { colCount } = this;
+ let seriesSeg = this.daySeries.sliceRange(range);
+ let segs = [];
+ if (seriesSeg) {
+ const { start, end } = seriesSeg;
+ let index = start;
+ while (index < end) {
+ let row = Math.floor(index / colCount);
+ let nextIndex = Math.min((row + 1) * colCount, end);
+ segs.push({
+ row,
+ start: index % colCount,
+ end: (nextIndex - 1) % colCount + 1,
+ isStart: seriesSeg.isStart && index === start,
+ isEnd: seriesSeg.isEnd && nextIndex === end,
+ });
+ index = nextIndex;
+ }
+ }
+ return segs;
+ }
+ }
+
+ function buildDayTableModel(dateProfile, dateProfileGenerator, dateEnv) {
+ const daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
+ const breakOnWeeks = /year|month|week/.test(dateProfile.currentRangeUnit);
+ const majorUnit = !breakOnWeeks && computeMajorUnit(dateProfile, dateEnv);
+ // Exclude 'day': when cells are themselves days, all would match and the boundary
+ // distinction is meaningless (unlike timeline slots which can be sub-day).
+ return new DayTableModel(daySeries, breakOnWeeks, dateEnv, majorUnit !== 'day' ? majorUnit : undefined);
+ }
+ function computeColWidth(colCount, colMinWidth, viewportWidth) {
+ if (viewportWidth == null) {
+ return [undefined, undefined];
+ }
+ const colTempWidth = viewportWidth / colCount;
+ if (colTempWidth < colMinWidth) {
+ return [colMinWidth * colCount, colMinWidth];
+ }
+ return [viewportWidth, undefined];
+ }
+ // Positioning
+ // -------------------------------------------------------------------------------------------------
+ /*
+ TODO: handle hidden-days better. If current day is hidden day, scrolls to way bottom
+ */
+ function computeTopFromDate(date, cellRows, rowHeightMap) {
+ let top = 0;
+ for (const cells of cellRows) {
+ const key = cells[0].key;
+ const start = cells[0].date;
+ const end = cells[cells.length - 1].date; // inclusive end
+ if (date >= start && date <= end) {
+ return top;
+ }
+ const rowHeight = rowHeightMap.get(key);
+ if (rowHeight == null) {
+ return; // denote unknown
+ }
+ top += rowHeight;
+ }
+ return top;
+ }
+ /*
+ FYI, `width` is not dependable for aligning completely to farside
+ */
+ function computeHorizontalsFromSeg(seg, colWidth, colCount) {
+ let fromStart;
+ let fromEnd;
+ if (colWidth != null) {
+ fromStart = seg.start * colWidth;
+ fromEnd = (colCount - seg.end) * colWidth;
+ }
+ else {
+ const colWidthFrac = 1 / colCount;
+ fromStart = fracToCssDim(seg.start * colWidthFrac);
+ fromEnd = fracToCssDim(1 - seg.end * colWidthFrac);
+ }
+ return { insetInlineStart: fromStart, insetInlineEnd: fromEnd };
+ }
+ function computeColFromPosition(positionLeft, elWidth, colWidth, colCount, isRtl) {
+ const realColWidth = colWidth != null ? colWidth : elWidth / colCount;
+ const colFromLeft = Math.floor(positionLeft / realColWidth);
+ const col = isRtl ? (colCount - colFromLeft - 1) : colFromLeft;
+ const left = colFromLeft * realColWidth;
+ const right = left + realColWidth;
+ return { col, left, right };
+ }
+ function computeRowFromPosition(positionTop, cellRows, rowHeightMap) {
+ let row = 0;
+ let top = 0;
+ let bottom = 0;
+ for (const cells of cellRows) {
+ const key = cells[0].key;
+ top = bottom;
+ bottom = top + rowHeightMap.get(key);
+ if (positionTop < bottom) {
+ break;
+ }
+ row++;
+ }
+ return { row, top, bottom };
+ }
+ // Hit Element
+ // -------------------------------------------------------------------------------------------------
+ function getRowEl(rootEl, row) {
+ return rootEl.querySelectorAll('[role=row]')[row];
+ }
+ function getCellEl(rowEl, col) {
+ return rowEl.querySelectorAll('[role=gridcell]')[col];
+ }
+ // Header Formatting
+ // -------------------------------------------------------------------------------------------------
+ const dayMicroWidth = 60;
+ const dayHeaderMicroFormat = createFormatter({
+ weekday: 'narrow'
+ });
+ function createDayHeaderFormatter(explicitFormat, datesRepDistinctDays, dateCnt) {
+ return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt);
+ }
+ // Computes a default column header formatting string if `colFormat` is not explicitly defined
+ function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) {
+ // if more than one week row, or if there are a lot of columns with not much space,
+ // put just the day numbers will be in each cell
+ if (!datesRepDistinctDays) {
+ return createFormatter({ weekday: 'short' }); // "Sat"
+ }
+ if (dayCnt > 1) {
+ return createFormatter({
+ weekday: 'short',
+ weekdayJustify: 'start',
+ day: 'numeric',
+ omitCommas: true,
+ omitTrailing: true,
+ });
+ }
+ return createFormatter({
+ weekday: 'long',
+ weekdayJustify: 'start',
+ day: 'numeric',
+ omitCommas: true,
+ omitTrailing: true,
+ });
+ }
+
+ class DayGridEventHarness extends C {
+ constructor() {
+ super(...arguments);
+ // ref
+ this.rootElRef = M$1();
+ }
+ render() {
+ const { props } = this;
+ return (u$1("div", { className: joinClassNames(props.className, classNames.abs), style: props.style, ref: this.rootElRef, children: props.children }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ const rootEl = this.rootElRef.current; // TODO: make dynamic with useEffect
+ this.disconnectHeight = watchHeight(rootEl, (height) => {
+ if (this._isUnmounting)
+ return;
+ setRef(this.props.heightRef, height);
+ });
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.disconnectHeight();
+ setRef(this.props.heightRef, null);
+ }
+ }
+
+ const DEFAULT_WEEK_NUM_FORMAT$1 = createFormatter({ week: 'narrow' });
+ class DayGridRow extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.headerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleSegPositioning);
+ });
+ this.mainHeightRefMap = new RefMap(() => {
+ const fgLiquidHeight = this.props.dayMaxEvents === true || this.props.dayMaxEventRows === true;
+ if (fgLiquidHeight) {
+ afterSize(this.handleSegPositioning);
+ }
+ });
+ this.segHeightRefMap = new RefMap(() => {
+ afterSize(this.handleSegPositioning);
+ });
+ // memo
+ this.buildWeekNumberRenderProps = memoize(buildWeekNumberRenderProps);
+ this.handleRootEl = (rootEl) => {
+ this.rootEl = rootEl;
+ setRef(this.props.rootElRef, rootEl);
+ };
+ this.handleSegPositioning = () => {
+ if (this._isUnmounting)
+ return;
+ this.forceUpdate();
+ };
+ }
+ render() {
+ const { props, context, headerHeightRefMap, mainHeightRefMap } = this;
+ const { cells } = props;
+ const { options } = context;
+ const weekDateMarker = props.cells[0].date;
+ const fgLiquidHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true;
+ // TODO: memoize? sort all types of segs?
+ const fgEventSegs = sortEventSegs(props.fgEventSegs, options.eventOrder);
+ // TODO: memoize?
+ const [maxMainTop, minMainHeight] = this.computeFgDims(); // uses headerHeightRefMap/mainHeightRefMap
+ const [segsByCol, hiddenSegsByCol, renderableSegsByCol, segTops, simpleHeightsByCol] = computeFgSegVerticals$1(fgEventSegs, this.segHeightRefMap.current, cells, fgLiquidHeight ? minMainHeight : undefined, // if not defined in first run, will unlimited!?
+ options.eventOrderStrict, options.eventSlicing, props.dayMaxEvents, props.dayMaxEventRows);
+ const heightsByCol = [];
+ if (maxMainTop != null) {
+ let col = 0;
+ for (const cell of cells) { // uses headerHeightRefMap/maxMainTop/simpleHeightsByCol
+ const cellHeaderHeight = headerHeightRefMap.current.get(cell.key);
+ if (cellHeaderHeight != null) {
+ const extraFgHeight = maxMainTop - cellHeaderHeight;
+ heightsByCol.push(simpleHeightsByCol[col] + extraFgHeight);
+ }
+ else {
+ heightsByCol.push(undefined);
+ }
+ col++;
+ }
+ }
+ const highlightSegs = this.getHighlightSegs();
+ const mirrorSegs = this.getMirrorSegs();
+ const hasNavLink = options.navLinks;
+ const fullWeekStr = buildDateStr(context, weekDateMarker, 'week');
+ const weekNumberRenderProps = this.buildWeekNumberRenderProps(weekDateMarker, context, props.cellIsNarrow, hasNavLink);
+ return (u$1("div", { role: props.role /* !!! */, "aria-label": props.role === 'row' // HACK
+ ? fullWeekStr
+ : undefined // can't have label on non-role div
+ , className: joinClassNames(options.dayRowClass, props.className, classNames.flexRow, classNames.rel, // origin for inlineWeekNumber?
+ classNames.isolate, (props.forPrint && props.basis !== undefined) && // basis implies siblings (must share height)
+ classNames.printSiblingRow), style: {
+ flexBasis: props.basis,
+ }, ref: this.handleRootEl, children: [(props.showWeekNumbers && !props.cellIsMicro) && (u$1(ContentContainer, { tag: 'div', attrs: {
+ ...(hasNavLink
+ ? buildNavLinkAttrs(context, weekDateMarker, 'week', fullWeekStr, /* isTabbable = */ false)
+ : {}),
+ 'role': undefined, // HACK: a 'link' role can't be child of 'row' role
+ 'aria-hidden': true, // HACK: never part of a11y tree because row already has label and role not allowed
+ },
+ // put above all cells (TODO: put explicit z0 on each cell?)
+ className: classNames.z1, renderProps: weekNumberRenderProps, generatorName: "inlineWeekNumberContent", customGenerator: options.inlineWeekNumberContent, defaultGenerator: renderText$1, classNameGenerator: options.inlineWeekNumberClass, didMount: options.inlineWeekNumberDidMount, willUnmount: options.inlineWeekNumberWillUnmount })), this.renderFillSegs(props.businessHourSegs, 'non-business'), this.renderFillSegs(props.bgEventSegs, 'bg-event'), this.renderFillSegs(highlightSegs, 'highlight'), props.cells.map((cell, col) => {
+ const normalFgNodes = this.renderFgSegs(maxMainTop, renderableSegsByCol[col], segTops, props.todayRange,
+ /* isMirror = */ false);
+ return (u$1(DayGridCell, { dateProfile: props.dateProfile, todayRange: props.todayRange, date: cell.date, isMajor: cell.isMajor, showDayNumber: props.showDayNumbers, isNarrow: props.cellIsNarrow, isMicro: props.cellIsMicro, borderStart: Boolean(col),
+ // content
+ segs: segsByCol[col], hiddenSegs: hiddenSegsByCol[col], fgLiquidHeight: fgLiquidHeight, fg: (u$1(S, { children: normalFgNodes })), eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection,
+ // render hooks
+ renderProps: cell.renderProps, dateSpanProps: cell.dateSpanProps, attrs: cell.attrs, className: cell.className,
+ // dimensions
+ fgHeight: heightsByCol[col], width: props.colWidth,
+ // refs
+ headerHeightRef: headerHeightRefMap.createRef(cell.key), mainHeightRef: mainHeightRefMap.createRef(cell.key) }, cell.key));
+ }), this.renderFgSegs(maxMainTop, mirrorSegs, segTops, props.todayRange,
+ /* isMirror = */ true)] }));
+ }
+ renderFgSegs(headerHeight, segs, segTops, todayRange, isMirror) {
+ const { props, segHeightRefMap } = this;
+ const { colWidth, eventSelection, cellIsMicro } = props;
+ const colCount = props.cells.length;
+ const defaultDisplayEventEnd = props.cells.length === 1;
+ const nodes = [];
+ for (const seg of segs) {
+ const key = getEventPartKey(seg);
+ const { standinFor, eventRange } = seg;
+ const { instanceId } = eventRange.instance;
+ if (standinFor) {
+ continue;
+ }
+ const { insetInlineStart, insetInlineEnd } = computeHorizontalsFromSeg(seg, colWidth, colCount);
+ const localTop = segTops.get(standinFor ? getEventPartKey(standinFor) : key) ?? (isMirror ? 0 : undefined);
+ const top = headerHeight != null && localTop != null
+ ? headerHeight + localTop
+ : undefined;
+ const isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
+ const isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
+ const isInvisible = !isMirror && (isDragging || isResizing || standinFor || top == null);
+ const isListItem = hasListItemDisplay(seg);
+ const isSelected = instanceId === eventSelection;
+ nodes.push(u$1(DayGridEventHarness, { className: seg.start ? classNames.fakeBorderS : '', style: {
+ visibility: isInvisible ? 'hidden' : undefined,
+ top,
+ insetInlineStart,
+ insetInlineEnd,
+ zIndex: isSelected ? 1000 : 0, // container inner z-indexes; HACK: relies on hardcoded z-index offset; fragile if stacking context changes
+ }, heightRef: (!standinFor && !isMirror)
+ ? segHeightRefMap.createRef(key)
+ : null, children: u$1(StandardEvent, { display: isListItem ? 'list-item' : 'row', eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: isMirror, isSelected: isSelected, isNarrow: props.cellIsNarrow, defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: defaultDisplayEventEnd, disableResizing: isListItem, forcedTimeText: cellIsMicro ? '' : undefined, ...getEventRangeMeta(eventRange, todayRange) }) }, key));
+ }
+ return nodes;
+ }
+ renderFillSegs(segs, fillType) {
+ const { props, context } = this;
+ const { todayRange, colWidth } = props;
+ const colCount = props.cells.length;
+ const nodes = [];
+ for (const seg of segs) {
+ const key = seg.start + ':' + seg.end; // NOTE: don't use date, because could be multiple of same (w/ resources)
+ const { insetInlineStart, insetInlineEnd } = computeHorizontalsFromSeg(seg, colWidth, colCount);
+ const isVisible = !seg.standinFor;
+ nodes.push(u$1("div", { className: classNames.fillY, style: {
+ visibility: (isVisible ? '' : 'hidden'),
+ insetInlineStart,
+ insetInlineEnd,
+ }, children: fillType === 'bg-event' ?
+ u$1(BgEvent, { eventRange: seg.eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isNarrow: props.cellIsNarrow, isVertical: false, ...getEventRangeMeta(seg.eventRange, todayRange) }) : (renderFill(fillType, context.options)) }, key));
+ }
+ return u$1(S, { children: nodes });
+ }
+ // Sizing
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ const { rootEl } = this; // TODO: make dynamic with useEffect
+ this.disconnectHeight = watchHeight(rootEl, (contentHeight) => {
+ setRef(this.props.heightRef, contentHeight);
+ });
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.disconnectHeight();
+ setRef(this.props.heightRef, null);
+ }
+ computeFgDims() {
+ const { cells } = this.props;
+ const headerHeightMap = this.headerHeightRefMap.current;
+ const mainHeightMap = this.mainHeightRefMap.current;
+ let maxMainTop;
+ let minMainBottom;
+ for (const cell of cells) {
+ const mainTop = headerHeightMap.get(cell.key);
+ const mainHeight = mainHeightMap.get(cell.key);
+ if (mainTop != null) {
+ if (maxMainTop === undefined || mainTop > maxMainTop) {
+ maxMainTop = mainTop;
+ }
+ if (mainHeight != null) {
+ const mainBottom = mainTop + mainHeight;
+ if (minMainBottom === undefined || mainBottom < minMainBottom) {
+ minMainBottom = mainBottom;
+ }
+ }
+ }
+ }
+ return [
+ maxMainTop,
+ minMainBottom != null && maxMainTop != null
+ ? minMainBottom - maxMainTop
+ : undefined,
+ ];
+ }
+ // Internal Utils
+ // -----------------------------------------------------------------------------------------------
+ getMirrorSegs() {
+ let { props } = this;
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return [];
+ }
+ getHighlightSegs() {
+ let { props } = this;
+ if (props.eventDrag && props.eventDrag.segs.length) { // messy check
+ return props.eventDrag.segs;
+ }
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return props.dateSelectionSegs;
+ }
+ }
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function buildWeekNumberRenderProps(weekDateMarker, context, isNarrow, hasNavLink) {
+ const { dateEnv, options } = context;
+ const weekNum = dateEnv.computeWeekNumber(weekDateMarker);
+ const weekNumTextParts = dateEnv.formatToParts(weekDateMarker, options.weekNumberFormat || DEFAULT_WEEK_NUM_FORMAT$1);
+ const weekNumText = joinDateTimeFormatParts(weekNumTextParts);
+ const weekDateZoned = dateEnv.toDate(weekDateMarker);
+ return {
+ num: weekNum,
+ text: weekNumText,
+ textParts: weekNumTextParts,
+ date: weekDateZoned,
+ isNarrow,
+ hasNavLink,
+ };
+ }
+
+ class DayGridRows extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.splitBusinessHourSegs = memoize(splitSegsByRow);
+ this.splitBgEventSegs = memoize(splitAllDaySegsByRow);
+ this.splitFgEventSegs = memoize(splitSegsByRow);
+ this.splitDateSelectionSegs = memoize(splitSegsByRow);
+ this.splitEventDrag = memoize(splitInteractionByRow);
+ this.splitEventResize = memoize(splitInteractionByRow);
+ // internal
+ this.rowHeightRefMap = new RefMap((height, key) => {
+ // HACKy way of syncing RefMap results with prop
+ const { rowHeightRefMap } = this.props;
+ if (rowHeightRefMap) {
+ rowHeightRefMap.handleValue(height, key);
+ }
+ });
+ this.handleRootEl = (rootEl) => {
+ this.rootEl = rootEl;
+ if (rootEl) {
+ this.context.registerInteractiveComponent(this, {
+ el: rootEl,
+ isHitComboAllowed: this.props.isHitComboAllowed,
+ });
+ }
+ else {
+ this.context.unregisterInteractiveComponent(this);
+ }
+ };
+ }
+ render() {
+ let { props, context, rowHeightRefMap } = this;
+ let { options } = context;
+ let { cellRows } = props;
+ let rowCount = cellRows.length;
+ // Will cause rows to not be reused across months
+ let firstCellKey = cellRows[0]?.[0]?.key || '';
+ let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCount);
+ let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCount);
+ let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCount);
+ let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCount);
+ let eventDragByRow = this.splitEventDrag(props.eventDrag, rowCount);
+ let eventResizeByRow = this.splitEventResize(props.eventResize, rowCount);
+ let isHeightAuto = getIsHeightAuto(options);
+ let rowHeightsRedistribute = !props.forPrint && !isHeightAuto;
+ let rowBasis = computeRowBasis(props.visibleWidth, rowCount, isHeightAuto, options);
+ return (u$1("div", { role: 'rowgroup', className: joinClassNames(props.className,
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
+ // https://stackoverflow.com/a/60256345
+ !props.forPrint && classNames.flexCol), style: { width: props.width }, ref: this.handleRootEl, children: cellRows.map((cells, row) => (u$1(DayGridRow, { role: 'row', dateProfile: props.dateProfile, todayRange: props.todayRange, cells: cells, cellIsNarrow: props.cellIsNarrow, cellIsMicro: props.cellIsMicro, showDayNumbers: rowCount > 1, showWeekNumbers: rowCount > 1 && options.weekNumbers, forPrint: props.forPrint,
+ // if not auto-height, distribute height of container somewhat evently to rows
+ className: joinClassNames(rowHeightsRedistribute && classNames.grow, rowCount > 1 && classNames.breakInsideAvoid, // don't avoid breaks for single tall row
+ row < rowCount - 1 ? classNames.borderOnlyB : classNames.borderNone),
+ // content
+ fgEventSegs: fgEventSegsByRow[row], bgEventSegs: bgEventSegsByRow[row], businessHourSegs: businessHourSegsByRow[row], dateSelectionSegs: dateSelectionSegsByRow[row], eventSelection: props.eventSelection, eventDrag: eventDragByRow[row], eventResize: eventResizeByRow[row], dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows,
+ // dimensions
+ colWidth: props.colWidth, basis: rowBasis,
+ // refs
+ heightRef: rowHeightRefMap.createRef(cells[0].key) }, firstCellKey + ':' + cells[0].key))) }));
+ }
+ // Hit System
+ // -----------------------------------------------------------------------------------------------
+ queryHit(isRtl, positionLeft, positionTop, elWidth) {
+ const { props } = this;
+ const colCount = props.cellRows[0].length;
+ const { col, left, right } = computeColFromPosition(positionLeft, elWidth, props.colWidth, colCount, isRtl);
+ const { row, top, bottom } = computeRowFromPosition(positionTop, props.cellRows, this.rowHeightRefMap.current);
+ const cell = props.cellRows[row][col];
+ const cellStartDate = cell.date;
+ const cellEndDate = addDays(cellStartDate, 1);
+ return {
+ dateProfile: props.dateProfile,
+ dateSpan: {
+ range: {
+ start: cellStartDate,
+ end: cellEndDate,
+ },
+ allDay: true,
+ ...cell.dateSpanProps,
+ },
+ getDayEl: () => getCellEl(getRowEl(this.rootEl, row), col),
+ rect: {
+ left,
+ right,
+ top,
+ bottom,
+ },
+ layer: 0,
+ };
+ }
+ }
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function isSegAllDay(seg) {
+ return seg.eventRange.def.allDay;
+ }
+ function splitAllDaySegsByRow(segs, rowCnt) {
+ return splitSegsByRow(segs.filter(isSegAllDay), rowCnt);
+ }
+ /*
+ Amount of height a row should consume prior to expanding
+ We don't want to use min-height with flexbox because we leverage min-height:auto,
+ which yields value based on natural height of events
+ */
+ function computeRowBasis(visibleWidth, // should INCLUDE any scrollbar width to avoid oscillation
+ rowCount, isHeightAuto, options) {
+ if (visibleWidth != null) {
+ // ensure a consistent row min-height modelled after a month with 6 rows respecting aspectRatio
+ // will result in same minHeight regardless of weekends, dayMinWidth, height:auto
+ const rowBasis = visibleWidth / options.aspectRatio / 6;
+ // don't give minHeight when single-month non-auto-height
+ // TODO: better way to detect this with DateProfile?
+ return (rowCount > 6 || isHeightAuto) ? rowBasis : 0;
+ }
+ return 0;
+ }
+
+ class DayGridHeaderCell extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ // memo
+ this.buildDayHeaderText = memoize(buildDayHeaderText);
+ this.handleInnerEl = (innerEl) => {
+ if (this.disconnectSize) {
+ this.disconnectSize();
+ this.disconnectSize = undefined;
+ }
+ if (innerEl) {
+ this.disconnectSize = watchSize(innerEl, (width, height) => {
+ if (this._isUnmounting)
+ return;
+ setRef(this.props.innerHeightRef, height);
+ this.setState({ innerWidth: width });
+ });
+ }
+ else {
+ setRef(this.props.innerHeightRef, null);
+ }
+ };
+ }
+ render() {
+ const { props, state, context } = this;
+ const { renderConfig, dataConfig } = props;
+ const totalColWidth = props.colWidth != null
+ ? props.colWidth * (dataConfig.colSpan || 1)
+ : undefined;
+ // HACK
+ const isDisabled = dataConfig.renderProps.isDisabled;
+ const finalRenderProps = renderConfig.dayHeaderFormat
+ ? this.buildDayHeaderRenderProps(dataConfig.renderProps, props.cellIsNarrow, props.rowLevel, props.cellIsMicro, dataConfig.dateMarker, renderConfig.dayHeaderFormat, Boolean(renderConfig.datesRepDistinctDays), context.dateEnv)
+ : {
+ ...dataConfig.renderProps,
+ isNarrow: props.cellIsNarrow,
+ level: props.rowLevel,
+ };
+ /*
+ TODO: DRY with TimelineHeaderCell
+ */
+ const alignInput = renderConfig.align;
+ const align = // normalized string-enum value
+ typeof alignInput === 'function'
+ ? alignInput({ level: props.rowLevel, inPopover: dataConfig.renderProps.inPopover, isNarrow: props.cellIsNarrow })
+ : alignInput;
+ const stickyInput = renderConfig.sticky;
+ const isSticky = props.rowLevel > 0 &&
+ stickyInput !== false && (
+ // if center-aligned, and wants to be sticky, must be >75% viewport width,
+ // to avoid looking awkwardly aligned
+ align !== 'center' || (totalColWidth != null &&
+ props.viewportWidth != null &&
+ totalColWidth > props.viewportWidth * 0.75));
+ let edgeCoord;
+ if (isSticky) {
+ if (align === 'center') {
+ if (state.innerWidth != null) {
+ edgeCoord = `calc(50% - ${state.innerWidth / 2}px)`;
+ }
+ }
+ else {
+ edgeCoord = (typeof stickyInput === 'number' ||
+ typeof stickyInput === 'string') ? stickyInput : 0;
+ }
+ }
+ return (u$1(ContentContainer, { tag: 'div', attrs: {
+ role: 'columnheader',
+ 'aria-colspan': dataConfig.colSpan,
+ ...dataConfig.attrs,
+ }, className: joinClassNames(dataConfig.className, classNames.noMargin, classNames.noPadding, classNames.flexCol, props.borderStart ? classNames.borderOnlyS : classNames.borderNone, align === 'center' ? classNames.alignCenter :
+ align === 'end' ? classNames.alignEnd :
+ classNames.alignStart, props.colWidth == null && classNames.liquid, !isSticky && classNames.crop), style: {
+ width: totalColWidth,
+ }, renderProps: finalRenderProps, generatorName: renderConfig.generatorName, customGenerator: renderConfig.customGenerator, defaultGenerator: renderText$1, classNameGenerator:
+ // don't use custom classNames if disabled
+ // TODO: make DRY with DayCellContainer
+ isDisabled ? undefined : renderConfig.classNameGenerator, didMount: renderConfig.didMount, willUnmount: renderConfig.willUnmount, children: (InnerContainer) => (u$1("div", { ref: this.handleInnerEl, className: joinClassNames(classNames.flexCol, classNames.noShrink, classNames.whiteSpaceNoWrap, isSticky && classNames.sticky), style: {
+ left: edgeCoord,
+ right: edgeCoord,
+ }, children: u$1(InnerContainer, { tag: 'div', attrs: dataConfig.innerAttrs, className: generateClassName(renderConfig.innerClassNameGenerator, finalRenderProps) }) })) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ }
+ buildDayHeaderRenderProps(renderProps, cellIsNarrow, rowLevel, cellIsMicro, dateMarker, dayHeaderFormat, datesRepDistinctDays, dateEnv) {
+ const baseText = this.buildDayHeaderText(datesRepDistinctDays ? dateMarker : renderProps.date, dayHeaderFormat, datesRepDistinctDays, dateEnv);
+ const textData = cellIsMicro
+ ? this.buildDayHeaderText(dateMarker, dayHeaderMicroFormat, false, dateEnv)
+ : baseText;
+ return {
+ ...renderProps,
+ isNarrow: cellIsNarrow,
+ level: rowLevel,
+ text: textData.text,
+ textParts: textData.textParts,
+ weekdayText: cellIsMicro ? textData.text : baseText.weekdayText,
+ dayNumberText: baseText.dayNumberText,
+ };
+ }
+ }
+ function buildDayHeaderText(date, formatter, includeDayNumber, dateEnv) {
+ const textParts = dateEnv.formatToParts(date, formatter);
+ return {
+ text: joinDateTimeFormatParts(textParts),
+ textParts,
+ weekdayText: findWeekdayText(textParts),
+ dayNumberText: includeDayNumber ? findDayNumberText(textParts) : '',
+ };
+ }
+
+ class DayGridHeaderRow extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // ref
+ this.innerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleInnerHeights);
+ });
+ this.handleInnerHeights = () => {
+ if (this._isUnmounting)
+ return;
+ const innerHeightMap = this.innerHeightRefMap.current;
+ let max = 0;
+ for (const innerHeight of innerHeightMap.values()) {
+ max = Math.max(max, innerHeight);
+ }
+ if (this.currentInnerHeight !== max) {
+ this.currentInnerHeight = max;
+ setRef(this.props.innerHeightRef, max);
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { options } = context;
+ return (u$1("div", { role: props.role /* !!! */, "aria-rowindex": props.rowIndex != null ? 1 + props.rowIndex : undefined, className: joinClassNames(options.dayHeaderRowClass, props.className, classNames.flexRow, classNames.contentBox, props.borderBottom ? classNames.borderOnlyB : classNames.borderNone), style: {
+ height: props.height,
+ }, children: props.dataConfigs.map((dataConfig, cellI) => (u$1(DayGridHeaderCell, { renderConfig: props.renderConfig, dataConfig: dataConfig, borderStart: Boolean(cellI), colWidth: props.colWidth, viewportWidth: props.viewportWidth, innerHeightRef: this.innerHeightRefMap.createRef(dataConfig.key), cellIsNarrow: props.cellIsNarrow, cellIsMicro: props.cellIsMicro, rowLevel: props.rowLevel }, dataConfig.key))) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.currentInnerHeight = undefined;
+ setRef(this.props.innerHeightRef, null);
+ }
+ }
+
+ /*
+ TODO: kill this class in favor of DayGridHeaderRows?
+ */
+ class DayGridHeader extends BaseComponent {
+ render() {
+ const { props } = this;
+ const { headerTiers } = props;
+ return (u$1("div", { role: 'rowgroup', className: joinClassNames(props.className, classNames.flexCol, props.width == null && classNames.liquid), style: {
+ width: props.width,
+ }, children: headerTiers.map((rowConfig, i) => (k$1(DayGridHeaderRow, { ...rowConfig, key: i, role: 'row', borderBottom: i < headerTiers.length - 1, colWidth: props.colWidth, viewportWidth: props.viewportWidth, cellIsNarrow: props.cellIsNarrow, cellIsMicro: props.cellIsMicro, rowLevel: headerTiers.length - i - 1 }))) }));
+ }
+ }
+
+ class DayGridLayoutNormal extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ this.handleScroller = (scroller) => {
+ setRef(this.props.scrollerRef, scroller);
+ };
+ this.handleTotalWidth = (totalWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ totalWidth });
+ };
+ this.handleClientWidth = (clientWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientWidth });
+ };
+ }
+ render() {
+ const { props, state, context } = this;
+ const { options } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const { totalWidth, clientWidth } = state;
+ let endScrollbarWidth = (totalWidth != null && clientWidth != null)
+ ? totalWidth - clientWidth
+ : undefined;
+ // HACK when clientWidth does NOT include body-border, compared to totalWidth
+ if (endScrollbarWidth < 3) {
+ endScrollbarWidth = 0;
+ }
+ const verticalScrollbars = !props.forPrint && !getIsHeightAuto(options);
+ const tableHeaderSticky = !props.forPrint && getTableHeaderSticky(options);
+ const colCount = props.cellRows[0].length;
+ const cellWidth = clientWidth != null ? clientWidth / colCount : undefined;
+ const cellIsMicro = cellWidth != null && cellWidth <= dayMicroWidth;
+ const cellIsNarrow = cellIsMicro || (cellWidth != null && cellWidth <= options.dayNarrowWidth);
+ return (u$1(S, { children: [options.dayHeaders && (u$1("div", { className: joinClassNames(generateClassName(options.tableHeaderClass, {
+ isSticky: tableHeaderSticky,
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }), classNames.printHeader, // either flexCol or table-header-group
+ tableHeaderSticky && classNames.tableHeaderSticky), children: [u$1("div", { className: classNames.flexRow, children: [u$1(DayGridHeader, { headerTiers: props.headerTiers, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), u$1("div", { className: generateClassName(options.dayHeaderDividerClass, {
+ isSticky: tableHeaderSticky,
+ multiMonthColumns: 0,
+ options: { allDaySlot: Boolean(options.allDaySlot) },
+ }) })] })), u$1(Scroller, { vertical: verticalScrollbars, className: joinClassNames(generateClassName(options.tableBodyClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }),
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
+ // https://stackoverflow.com/a/60256345
+ !props.forPrint && classNames.flexCol, verticalScrollbars && classNames.liquid), ref: this.handleScroller, clientWidthRef: this.handleClientWidth, children: u$1(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: props.cellRows, forPrint: props.forPrint, isHitComboAllowed: props.isHitComboAllowed, className: classNames.grow, dayMaxEvents: props.forPrint ? undefined : options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows,
+ // content
+ fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection,
+ // dimensions
+ visibleWidth: totalWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
+ // refs
+ rowHeightRefMap: props.rowHeightRefMap }) }), u$1(Ruler, { widthRef: this.handleTotalWidth })] }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ }
+ }
+
+ class FooterScrollbar extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.rootElRef = M$1();
+ }
+ render() {
+ const { props } = this;
+ // NOTE: we need a wrapper around the Scroller because if scrollbars appear/hide,
+ // the outer dimensions change, but the inner dimensions do not. The Scroller's
+ // dimension-watching, when used in ponyfill-mode, can't fire on border-box change, so we
+ // workaround it by monitoring dimensions of a wrapper instead
+ return (u$1("div", { ref: this.rootElRef, className: joinClassNames(classNames.footerScrollbar, props.isSticky && classNames.footerScrollbarSticky), children: u$1(Scroller, { horizontal: true, ref: props.scrollerRef, children: u$1("div", { style: { minWidth: props.canvasWidth } }) }) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.disconnectHeight = watchHeight(this.rootElRef.current, (height) => {
+ if (this._isUnmounting)
+ return;
+ setRef(this.props.scrollbarWidthRef, height);
+ });
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.disconnectHeight();
+ setRef(this.props.scrollbarWidthRef, null);
+ }
+ }
+
+ class DayGridLayoutPannable extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ this.headerScrollerRef = M$1();
+ this.bodyScrollerRef = M$1();
+ this.footerScrollerRef = M$1();
+ // Sizing
+ // -----------------------------------------------------------------------------------------------
+ this.handleTotalWidth = (totalWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ totalWidth });
+ };
+ this.handleClientWidth = (clientWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientWidth });
+ };
+ }
+ render() {
+ const { props, state, context } = this;
+ const { options } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const { totalWidth, clientWidth } = state;
+ const endScrollbarWidth = (totalWidth != null && clientWidth != null)
+ ? totalWidth - clientWidth
+ : undefined;
+ const verticalScrollbars = !props.forPrint && !getIsHeightAuto(options);
+ const tableHeaderSticky = !props.forPrint && getTableHeaderSticky(options);
+ const footerScrollbarSticky = !props.forPrint && getFooterScrollbarSticky(options);
+ const colCount = props.cellRows[0].length;
+ const [canvasWidth, colWidth] = computeColWidth(colCount, props.dayMinWidth, clientWidth);
+ const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
+ return (u$1(S, { children: [options.dayHeaders && (u$1("div", { className: joinClassNames(generateClassName(options.tableHeaderClass, {
+ isSticky: tableHeaderSticky,
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }), classNames.printHeader, // either flexCol or table-header-group
+ tableHeaderSticky && classNames.tableHeaderSticky), children: [u$1(Scroller, { horizontal: true, hideScrollbars: true, className: classNames.flexRow, ref: this.headerScrollerRef, children: [u$1(DayGridHeader, { headerTiers: props.headerTiers, colWidth: colWidth, viewportWidth: clientWidth, width: canvasWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), u$1("div", { className: generateClassName(options.dayHeaderDividerClass, {
+ isSticky: tableHeaderSticky,
+ multiMonthColumns: 0,
+ options: { allDaySlot: Boolean(options.allDaySlot) },
+ }) })] })), u$1(Scroller, { vertical: verticalScrollbars, horizontal: true, hideScrollbars: footerScrollbarSticky ||
+ props.forPrint // prevents blank space in print-view on Safari
+ , className: joinClassNames(generateClassName(options.tableBodyClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }),
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
+ // https://stackoverflow.com/a/60256345
+ !props.forPrint && classNames.flexCol, verticalScrollbars && classNames.liquid), ref: this.bodyScrollerRef, clientWidthRef: this.handleClientWidth, children: u$1(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: props.cellRows, forPrint: props.forPrint, isHitComboAllowed: props.isHitComboAllowed, className: classNames.grow, dayMaxEvents: props.forPrint ? undefined : options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows,
+ // content
+ fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection,
+ // dimensions
+ colWidth: colWidth, width: canvasWidth, visibleWidth: totalWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
+ // refs
+ rowHeightRefMap: props.rowHeightRefMap }) }), Boolean(footerScrollbarSticky) && (u$1(FooterScrollbar, { isSticky: true, canvasWidth: canvasWidth, scrollerRef: this.footerScrollerRef })), u$1(Ruler, { widthRef: this.handleTotalWidth })] }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ // scroller
+ const ScrollerSyncer = getScrollerSyncerClass(this.context.pluginHooks);
+ this.syncedScroller = new ScrollerSyncer(true); // horizontal=true
+ setRef(this.props.scrollerRef, this.syncedScroller);
+ this.updateSyncedScroller();
+ }
+ componentDidUpdate() {
+ // scroller
+ this.updateSyncedScroller();
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ // scroller
+ this.syncedScroller.destroy();
+ }
+ // Scrolling
+ // -----------------------------------------------------------------------------------------------
+ updateSyncedScroller() {
+ this.syncedScroller.handleChildren([
+ this.headerScrollerRef.current,
+ this.bodyScrollerRef.current,
+ this.footerScrollerRef.current,
+ ]);
+ }
+ }
+
+ class DayGridLayout extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // ref
+ this.scrollerRef = M$1();
+ this.rowHeightRefMap = new RefMap(() => {
+ afterSize(this.updateScrollY);
+ });
+ this.scrollDate = null;
+ this.updateScrollY = () => {
+ if (this._isUnmounting)
+ return;
+ const rowHeightMap = this.rowHeightRefMap.current;
+ const scroller = this.scrollerRef.current;
+ // Since updateScrollY is called by rowHeightRefMap, could be called with null during cleanup,
+ // and the scroller might not exist
+ if (scroller && this.scrollDate) {
+ let scrollTop = computeTopFromDate(this.scrollDate, this.props.cellRows, rowHeightMap);
+ if (scrollTop != null) {
+ if (scrollTop) {
+ scrollTop++; // clear top border
+ }
+ scroller.scrollTo({ y: scrollTop });
+ }
+ }
+ };
+ this.handleScrollEnd = (isDevice) => {
+ if (isDevice) {
+ this.scrollDate = null;
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { options } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const businessHourSegs = props.forPrint ? [] : props.businessHourSegs;
+ const dateSelectionSegs = props.forPrint ? [] : props.dateSelectionSegs;
+ const eventDrag = props.forPrint ? null : props.eventDrag;
+ const eventResize = props.forPrint ? null : props.eventResize;
+ const commonLayoutProps = {
+ ...props,
+ businessHourSegs,
+ dateSelectionSegs,
+ eventDrag,
+ eventResize,
+ scrollerRef: this.scrollerRef,
+ rowHeightRefMap: this.rowHeightRefMap,
+ };
+ return (u$1(ViewContainer, { viewSpec: context.viewSpec, attrs: {
+ role: 'grid',
+ 'aria-rowcount': props.headerTiers.length + props.cellRows.length,
+ 'aria-colcount': props.cellRows[0].length,
+ 'aria-labelledby': props.labelId,
+ 'aria-label': props.labelStr,
+ }, className: joinClassNames(props.className, classNames.printRoot, // either flexCol or table
+ generateClassName(options.tableClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ })), children: options.dayMinWidth ? (u$1(DayGridLayoutPannable, { ...commonLayoutProps, dayMinWidth: options.dayMinWidth })) : (u$1(DayGridLayoutNormal, { ...commonLayoutProps })) }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.resetScroll();
+ this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd);
+ }
+ componentDidUpdate(prevProps) {
+ if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
+ this.resetScroll();
+ }
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd);
+ }
+ // Scrolling
+ // -----------------------------------------------------------------------------------------------
+ resetScroll() {
+ this.scrollDate = this.props.dateProfile.currentDate;
+ this.updateScrollY();
+ const scroller = this.scrollerRef.current;
+ scroller.scrollTo({ x: 0 });
+ }
+ }
+
+ const EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere
+ class Splitter {
+ constructor() {
+ this.getKeysForEventDefs = memoize(this._getKeysForEventDefs);
+ this.splitDateSelection = memoize(this._splitDateSpan);
+ this.splitEventStore = memoize(this._splitEventStore);
+ this.splitIndividualUi = memoize(this._splitIndividualUi);
+ this.splitEventDrag = memoize(this._splitInteraction);
+ this.splitEventResize = memoize(this._splitInteraction);
+ this.eventUiBuilders = {}; // TODO: typescript protection
+ }
+ splitProps(props) {
+ let keyInfos = this.getKeyInfo(props);
+ let defKeys = this.getKeysForEventDefs(props.eventStore);
+ let dateSelections = this.splitDateSelection(props.dateSelection);
+ let individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases*
+ let eventStores = this.splitEventStore(props.eventStore, defKeys);
+ let eventDrags = this.splitEventDrag(props.eventDrag);
+ let eventResizes = this.splitEventResize(props.eventResize);
+ let splitProps = {};
+ this.eventUiBuilders = mapHash(keyInfos, (info, key) => this.eventUiBuilders[key] || memoize(buildEventUiForKey));
+ for (let key in keyInfos) {
+ let keyInfo = keyInfos[key];
+ let eventStore = eventStores[key] || EMPTY_EVENT_STORE;
+ let buildEventUi = this.eventUiBuilders[key];
+ splitProps[key] = {
+ businessHours: keyInfo.businessHours || props.businessHours,
+ dateSelection: dateSelections[key] || null,
+ eventStore,
+ eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
+ eventDrag: eventDrags[key] || null,
+ eventResize: eventResizes[key] || null,
+ eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
+ };
+ }
+ return splitProps;
+ }
+ _splitDateSpan(dateSpan) {
+ let dateSpans = {};
+ if (dateSpan) {
+ let keys = this.getKeysForDateSpan(dateSpan);
+ for (let key of keys) {
+ dateSpans[key] = dateSpan;
+ }
+ }
+ return dateSpans;
+ }
+ _getKeysForEventDefs(eventStore) {
+ return mapHash(eventStore.defs, (eventDef) => this.getKeysForEventDef(eventDef));
+ }
+ _splitEventStore(eventStore, defKeys) {
+ let { defs, instances } = eventStore;
+ let splitStores = {};
+ for (let defId in defs) {
+ for (let key of defKeys[defId]) {
+ if (!splitStores[key]) {
+ splitStores[key] = createEmptyEventStore();
+ }
+ splitStores[key].defs[defId] = defs[defId];
+ }
+ }
+ for (let instanceId in instances) {
+ let instance = instances[instanceId];
+ for (let key of defKeys[instance.defId]) {
+ if (splitStores[key]) { // must have already been created
+ splitStores[key].instances[instanceId] = instance;
+ }
+ }
+ }
+ return splitStores;
+ }
+ _splitIndividualUi(eventUiBases, defKeys) {
+ let splitHashes = {};
+ for (let defId in eventUiBases) {
+ if (defId) { // not the '' key
+ for (let key of defKeys[defId]) {
+ if (!splitHashes[key]) {
+ splitHashes[key] = {};
+ }
+ splitHashes[key][defId] = eventUiBases[defId];
+ }
+ }
+ }
+ return splitHashes;
+ }
+ _splitInteraction(interaction) {
+ let splitStates = {};
+ if (interaction) {
+ let affectedStores = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents));
+ // can't rely on defKeys because event data is mutated
+ let mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents);
+ let mutatedStores = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId);
+ let populate = (key) => {
+ if (!splitStates[key]) {
+ splitStates[key] = {
+ affectedEvents: affectedStores[key] || EMPTY_EVENT_STORE,
+ mutatedEvents: mutatedStores[key] || EMPTY_EVENT_STORE,
+ isEvent: interaction.isEvent,
+ };
+ }
+ };
+ for (let key in affectedStores) {
+ populate(key);
+ }
+ for (let key in mutatedStores) {
+ populate(key);
+ }
+ }
+ return splitStates;
+ }
+ }
+ function buildEventUiForKey(allUi, eventUiForKey, individualUi) {
+ let baseParts = [];
+ if (allUi) {
+ baseParts.push(allUi);
+ }
+ if (eventUiForKey) {
+ baseParts.push(eventUiForKey);
+ }
+ let stuff = {
+ '': combineEventUis(baseParts),
+ };
+ if (individualUi) {
+ Object.assign(stuff, individualUi);
+ }
+ return stuff;
+ }
+
+ class AllDaySplitter extends Splitter {
+ getKeyInfo() {
+ return {
+ allDay: {},
+ timed: {},
+ };
+ }
+ getKeysForDateSpan(dateSpan) {
+ if (dateSpan.allDay) {
+ return ['allDay'];
+ }
+ return ['timed'];
+ }
+ getKeysForEventDef(eventDef) {
+ if (!eventDef.allDay) {
+ return ['timed'];
+ }
+ if (hasBgRendering(eventDef)) {
+ return ['timed', 'allDay'];
+ }
+ return ['allDay'];
+ }
+ }
+
+ class DayTimeColsSlicer extends Slicer {
+ sliceRange(range, dayRanges) {
+ let segs = [];
+ for (let col = 0; col < dayRanges.length; col += 1) {
+ let segRange = intersectRanges(range, dayRanges[col]);
+ if (segRange) {
+ segs.push({
+ startDate: segRange.start,
+ endDate: segRange.end,
+ isStart: segRange.start.valueOf() === range.start.valueOf(),
+ isEnd: segRange.end.valueOf() === range.end.valueOf(),
+ col,
+ });
+ }
+ }
+ return segs;
+ }
+ }
+
+ /*
+ TODO: more DRY with daygrid?
+ can be given null/undefined!
+ */
+ function organizeSegsByCol(segs, colCount) {
+ let segsByCol = [];
+ let i;
+ for (i = 0; i < colCount; i += 1) {
+ segsByCol.push([]);
+ }
+ if (segs) {
+ for (i = 0; i < segs.length; i += 1) {
+ segsByCol[segs[i].col].push(segs[i]);
+ }
+ }
+ return segsByCol;
+ }
+ /*
+ TODO: more DRY with daygrid?
+ can be given null/undefined!
+ */
+ function splitInteractionByCol(ui, colCount) {
+ let byRow = [];
+ if (!ui) {
+ for (let i = 0; i < colCount; i += 1) {
+ byRow[i] = null;
+ }
+ }
+ else {
+ for (let i = 0; i < colCount; i += 1) {
+ byRow[i] = {
+ affectedInstances: ui.affectedInstances,
+ isEvent: ui.isEvent,
+ segs: [],
+ };
+ }
+ for (let seg of ui.segs) {
+ byRow[seg.col].segs.push(seg);
+ }
+ }
+ return byRow;
+ }
+
+ // potential nice values for the slot-duration and interval-duration
+ // from largest to smallest
+ const STOCK_SUB_DURATIONS = [
+ { hours: 1 },
+ { minutes: 30 },
+ { minutes: 15 },
+ { seconds: 30 },
+ { seconds: 15 },
+ ];
+ function buildSlatMetas(slotMinTime, slotMaxTime, explicitLabelInterval, slotDuration, dateEnv) {
+ let dayStart = new Date(0);
+ let slatTime = slotMinTime;
+ let slatIterator = createDuration(0);
+ let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration);
+ let metas = [];
+ let i = 0;
+ while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) {
+ let date = dateEnv.add(dayStart, slatTime);
+ let isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null;
+ metas.push({
+ date,
+ time: slatTime,
+ key: date.toISOString(), // we can't use the isoTimeStr for uniqueness when minTime/maxTime beyone 0h/24h
+ isoTimeStr: formatIsoTimeString(date),
+ isLabeled,
+ isFirst: i === 0,
+ });
+ slatTime = addDurations(slatTime, slotDuration);
+ slatIterator = addDurations(slatIterator, slotDuration);
+ i += 1;
+ }
+ return metas;
+ }
+ // Computes an automatic value for slotHeaderInterval
+ function computeLabelInterval(slotDuration) {
+ let i;
+ let labelInterval;
+ let slotsPerLabel;
+ // find the smallest stock label interval that results in more than one slots-per-label
+ for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) {
+ labelInterval = createDuration(STOCK_SUB_DURATIONS[i]);
+ slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration);
+ if (slotsPerLabel !== null && slotsPerLabel > 1) {
+ return labelInterval;
+ }
+ }
+ return slotDuration; // fall back
+ }
+
+ class TimeGridAllDayHeader extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // ref
+ this.innerElRef = M$1();
+ }
+ render() {
+ let { props } = this;
+ let { options, viewApi } = this.context;
+ let renderProps = {
+ text: options.allDayText,
+ view: viewApi,
+ isNarrow: props.isNarrow,
+ };
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ role: 'rowheader',
+ }, className: joinClassNames(classNames.flexRow, classNames.noMargin, classNames.noPadding, classNames.contentBox), style: {
+ width: props.width,
+ }, renderProps: renderProps, generatorName: "allDayHeaderContent", customGenerator: options.allDayHeaderContent, defaultGenerator: renderAllDayInner, classNameGenerator: options.allDayHeaderClass, didMount: options.allDayHeaderDidMount, willUnmount: options.allDayHeaderWillUnmount, children: (InnerContent) => (u$1("div", { className: joinClassNames(classNames.flexRow, classNames.noShrink, classNames.whiteSpacePre), ref: this.innerElRef, children: u$1(InnerContent, { tag: 'div', className: generateClassName(options.allDayHeaderInnerClass, renderProps) }) })) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ const { props } = this;
+ const innerEl = this.innerElRef.current; // TODO: make dynamic with useEffect
+ // TODO: only attach this if refs props present
+ this.disconnectInnerWidth = watchWidth(innerEl, (width) => {
+ if (this._isUnmounting)
+ return;
+ setRef(props.innerWidthRef, width);
+ });
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.disconnectInnerWidth();
+ setRef(this.props.innerWidthRef, null);
+ }
+ }
+ function renderAllDayInner(renderProps) {
+ return renderProps.text;
+ }
+
+ class TimeGridAllDayLane extends DateComponent {
+ constructor() {
+ super(...arguments);
+ this.heightRef = M$1();
+ this.handleRootEl = (rootEl) => {
+ this.rootEl = rootEl;
+ if (rootEl) {
+ this.context.registerInteractiveComponent(this, {
+ el: rootEl,
+ });
+ }
+ else {
+ this.context.unregisterInteractiveComponent(this);
+ }
+ };
+ }
+ render() {
+ return (u$1(DayGridRow, { ...this.props,
+ /* BAD: these overwrite the props! caller might want to pass them */
+ rootElRef: this.handleRootEl, heightRef: this.heightRef }));
+ }
+ queryHit(isRtl, positionLeft, positionTop, elWidth) {
+ const { props, heightRef } = this;
+ const colCount = props.cells.length;
+ const { col, left, right } = computeColFromPosition(positionLeft, elWidth, props.colWidth, colCount, isRtl);
+ const cell = props.cells[col];
+ const cellStartDate = cell.date;
+ const cellEndDate = addDays(cellStartDate, 1);
+ return {
+ dateProfile: props.dateProfile,
+ dateSpan: {
+ range: {
+ start: cellStartDate,
+ end: cellEndDate,
+ },
+ allDay: true,
+ ...cell.dateSpanProps,
+ },
+ getDayEl: () => getCellEl(this.rootEl, col),
+ rect: {
+ left,
+ right,
+ top: 0,
+ bottom: heightRef.current,
+ },
+ layer: 0,
+ };
+ }
+ }
+
+ function buildTimeColsModel(dateProfile, dateProfileGenerator, dateEnv) {
+ let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
+ return new DayTableModel(daySeries, false, dateEnv);
+ }
+ function buildDayRanges(dayTableModel, dateProfile, dateEnv) {
+ let ranges = [];
+ for (let date of dayTableModel.headerDates) {
+ ranges.push({
+ start: dateEnv.add(date, dateProfile.slotMinTime),
+ end: dateEnv.add(date, dateProfile.slotMaxTime),
+ });
+ }
+ return ranges;
+ }
+ function computeSlatHeight(expandRows, slatCnt, explicitSlatMinHeight = 0, slatInnerHeight, // from the "inner" i think
+ scrollerHeight) {
+ if (!slatInnerHeight || !scrollerHeight) {
+ return [undefined, false];
+ }
+ const slatMinHeight = Math.max(slatInnerHeight + 1, explicitSlatMinHeight);
+ const slatLiquidHeight = scrollerHeight / slatCnt;
+ let slatLiquid;
+ let slatHeight;
+ if (expandRows && slatLiquidHeight >= slatMinHeight) {
+ slatLiquid = true;
+ slatHeight = slatLiquidHeight;
+ }
+ else {
+ slatLiquid = false;
+ slatHeight = slatMinHeight;
+ }
+ return [slatHeight, slatLiquid];
+ }
+ /*
+ A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
+ */
+ function computeDateTopFrac(date, dateProfile, startOfDayDate) {
+ if (!startOfDayDate) {
+ startOfDayDate = startOfDay(date);
+ }
+ return computeTimeTopFrac(createDuration(date.valueOf() - startOfDayDate.valueOf()), dateProfile);
+ }
+ function computeTimeTopFrac(time, dateProfile) {
+ const startMs = asRoughMs(dateProfile.slotMinTime);
+ const endMs = asRoughMs(dateProfile.slotMaxTime);
+ let frac = (time.milliseconds - startMs) / (endMs - startMs);
+ frac = Math.max(0, frac);
+ frac = Math.min(1, frac);
+ return frac;
+ }
+
+ function computeFgSegVerticals(segs, dateProfile, colDate, slatCnt, slatHeight, // in pixels
+ eventMinHeight, // in pixels
+ eventShortHeight) {
+ const res = [];
+ if (slatHeight != null) {
+ const totalHeight = slatHeight * slatCnt;
+ for (const seg of segs) {
+ const startFrac = computeDateTopFrac(seg.startDate, dateProfile, colDate);
+ const endFrac = computeDateTopFrac(seg.endDate, dateProfile, colDate);
+ const startCoord = startFrac * totalHeight;
+ let endCoord = endFrac * totalHeight;
+ let height = endCoord - startCoord;
+ if (eventMinHeight != null && height < eventMinHeight) {
+ height = eventMinHeight;
+ endCoord = startCoord + height;
+ }
+ res.push({
+ start: startCoord,
+ end: endCoord,
+ size: height,
+ isShort: height <= eventShortHeight
+ });
+ }
+ }
+ return res;
+ }
+
+ /*
+ segs assumed sorted
+ */
+ function buildWebPositioning(segs, segVerticals, strictOrder, maxDepth) {
+ const segRanges = [];
+ // isn't it true that there will either be ALL hcoords or NONE? can optimize
+ for (let i = 0; i < segs.length; i++) {
+ const segVertical = segVerticals[i];
+ if (segVertical) {
+ segRanges.push({
+ ...segs[i],
+ start: segVertical.start,
+ end: segVertical.end,
+ });
+ }
+ }
+ const hierarchy = new SegHierarchy(segRanges, undefined, // 1 thickness for all segs
+ strictOrder, undefined, // maxCoord
+ maxDepth);
+ let web = buildWeb(hierarchy);
+ web = stretchWeb(web, 1); // all levelCoords/thickness will have 0.0-1.0
+ const segRects = webToRects(web);
+ const hiddenGroups = groupIntersectingSegs(hierarchy.hiddenSegs);
+ return [segRects, hiddenGroups];
+ }
+ /*
+ TODO: use SegHierarchy::traverseSegs for this?
+ */
+ function buildWeb(hierarchy) {
+ const { placementsByLevel } = hierarchy;
+ const buildNode = cacheable((level, lateral) => level + ':' + lateral, (level, lateral) => {
+ let siblingRange = findNextLevelSegs(hierarchy, level, lateral);
+ let [nextLevelNodes, maxPressure] = buildNodes(siblingRange, buildNode);
+ let segPlacement = placementsByLevel[level][lateral];
+ return [
+ { ...segPlacement, nextLevelNodes },
+ segPlacement.thickness + maxPressure, // the pressure builds
+ ];
+ });
+ const [topLevelNodes] = buildNodes(placementsByLevel.length
+ ? { level: 0, lateralStart: 0, lateralEnd: placementsByLevel[0].length }
+ : null, buildNode);
+ return topLevelNodes;
+ }
+ function buildNodes(siblingRange, buildNode) {
+ if (!siblingRange) {
+ return [[], 0];
+ }
+ let { level, lateralStart, lateralEnd } = siblingRange;
+ let lateral = lateralStart;
+ let pairs = [];
+ while (lateral < lateralEnd) {
+ pairs.push(buildNode(level, lateral));
+ lateral += 1;
+ }
+ pairs.sort(cmpDescPressures);
+ return [
+ pairs.map(extractNode), // nodes
+ pairs[0][1], // first item's pressure
+ ];
+ }
+ function cmpDescPressures(a, b) {
+ return b[1] - a[1];
+ }
+ function extractNode(a) {
+ return a[0];
+ }
+ function findNextLevelSegs(hierarchy, subjectLevel, subjectLateral) {
+ let { levelCoords, placementsByLevel } = hierarchy;
+ let subjectPlacement = placementsByLevel[subjectLevel][subjectLateral];
+ let afterSubject = levelCoords[subjectLevel] + subjectPlacement.thickness;
+ let levelCnt = levelCoords.length;
+ let level = subjectLevel;
+ // skip past levels that are too high up
+ for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1)
+ ; // do nothing
+ for (; level < levelCnt; level += 1) {
+ let placements = placementsByLevel[level];
+ let placement;
+ let searchIndex = binarySearch(placements, subjectPlacement.start, getCoordRangeEnd);
+ let lateralStart = searchIndex[0] + searchIndex[1]; // if exact match (which doesn't collide), go to next one
+ let lateralEnd = lateralStart;
+ while ( // loop through placements that horizontally intersect
+ (placement = placements[lateralEnd]) && // but not past the whole seg list
+ placement.start < subjectPlacement.end) {
+ lateralEnd += 1;
+ }
+ if (lateralStart < lateralEnd) {
+ return { level, lateralStart, lateralEnd };
+ }
+ }
+ return null;
+ }
+ function stretchWeb(topLevelNodes, totalThickness) {
+ const stretchNode = cacheable((node, startCoord, prevThickness) => getEventKey(node), (node, startCoord, prevThickness) => {
+ let { nextLevelNodes, thickness } = node;
+ let allThickness = thickness + prevThickness;
+ let thicknessFraction = thickness / allThickness;
+ let endCoord;
+ let newChildren = [];
+ if (!nextLevelNodes.length) {
+ endCoord = totalThickness;
+ }
+ else {
+ for (let childNode of nextLevelNodes) {
+ if (endCoord === undefined) {
+ let res = stretchNode(childNode, startCoord, allThickness);
+ endCoord = res[0];
+ newChildren.push(res[1]);
+ }
+ else {
+ let res = stretchNode(childNode, endCoord, 0);
+ newChildren.push(res[1]);
+ }
+ }
+ }
+ let newThickness = (endCoord - startCoord) * thicknessFraction;
+ return [endCoord - newThickness, {
+ ...node,
+ thickness: newThickness,
+ nextLevelNodes: newChildren,
+ }];
+ });
+ return topLevelNodes.map((node) => stretchNode(node, 0, 0)[1]);
+ }
+ // not sorted in any particular order
+ function webToRects(topLevelNodes) {
+ let rectMap = new Map();
+ /*
+ Returns max stackForward of the node's forward children
+ */
+ const processNode = cacheable((node, levelCoord, stackDepth) => getEventKey(node), (node, levelCoord, stackDepth) => {
+ let rect = {
+ ...node,
+ levelCoord,
+ stackDepth,
+ stackForward: 0, // will assign after recursing
+ };
+ rectMap.set(rect.eventRange.instance.instanceId, rect);
+ return (rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1));
+ });
+ /*
+ Returns max stackForward of all `nodes`
+ */
+ function processNodes(nodes, levelCoord, stackDepth) {
+ let stackForward = 0;
+ for (let node of nodes) {
+ stackForward = Math.max(processNode(node, levelCoord, stackDepth) + 1, stackForward);
+ }
+ return stackForward;
+ }
+ processNodes(topLevelNodes, 0, 0);
+ return rectMap;
+ }
+ // TODO: move to general util
+ function cacheable(keyFunc, workFunc) {
+ const cache = {};
+ return (...args) => {
+ let key = keyFunc(...args);
+ return (key in cache)
+ ? cache[key]
+ : (cache[key] = workFunc(...args));
+ };
+ }
+
+ const DEFAULT_TIME_FORMAT$1 = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ meridiem: false,
+ });
+ class TimeGridEvent extends BaseComponent {
+ render() {
+ const { props } = this;
+ return (u$1(StandardEvent, { ...props, display: 'column', level: props.level, isNarrow: props.isNarrow, isShort: props.isShort, className:
+ // see note in TimeGridCol on why we use flexbox
+ props.isLiquid ? classNames.liquid : '', disableLiquid: !props.isLiquid, defaultTimeFormat: DEFAULT_TIME_FORMAT$1 }));
+ }
+ }
+
+ class TimeGridMoreLink extends BaseComponent {
+ render() {
+ let { props } = this;
+ return (u$1("div", { className: joinClassNames(classNames.abs, classNames.flexCol), style: {
+ top: props.top,
+ height: props.height,
+ insetInlineEnd: 0,
+ zIndex: 9999, // HACK. move to className?
+ }, children: u$1(MoreLinkContainer, { className: classNames.liquid, display: 'column', allDayDate: null, segs: props.hiddenSegs, hiddenSegs: props.hiddenSegs, dateSpanProps: props.dateSpanProps, dateProfile: props.dateProfile, todayRange: props.todayRange, popoverContent: () => renderPlainFgSegs(props.hiddenSegs, props, /* isMirror = */ false), forceTimed: true, isNarrow: props.isNarrow, isMicro: props.isMicro }) }));
+ }
+ }
+
+ const NowIndicatorDot = (props) => (u$1(ViewContextType.Consumer, { children: (context) => {
+ let { options } = context;
+ return (u$1("div", { className: joinClassNames(props.className, options.nowIndicatorDotClass), style: props.style }));
+ } }));
+
+ const NowIndicatorLineContainer = (props) => (u$1(ViewContextType.Consumer, { children: (context) => {
+ let { options } = context;
+ let renderProps = {
+ date: context.dateEnv.toDate(props.date),
+ view: context.viewApi,
+ };
+ return (u$1(ContentContainer, { elRef: props.elRef, tag: props.tag || 'div', attrs: props.attrs, className: props.className, style: props.style, renderProps: renderProps, generatorName: "nowIndicatorLineContent", customGenerator: options.nowIndicatorLineContent, classNameGenerator: options.nowIndicatorLineClass, didMount: options.nowIndicatorLineDidMount, willUnmount: options.nowIndicatorLineWillUnmount, children: props.children }));
+ } }));
+
+ /*
+ Renders both the line AND the dot
+ TODO: DRY with other NowIndicator components
+ */
+ function TimeGridNowIndicatorLine(props) {
+ const top = props.totalHeight != null
+ ? props.totalHeight * computeDateTopFrac(props.nowDate, props.dateProfile, props.dayDate)
+ : undefined;
+ return (u$1("div", { className: classNames.fill, style: {
+ zIndex: 2, // inlined from $now-indicator-z
+ pointerEvents: 'none', // TODO: className
+ }, children: [u$1(NowIndicatorLineContainer, { className: joinClassNames(classNames.fillX, classNames.noMarginX, classNames.borderlessX), style: { top }, date: props.nowDate }), (props.showDot ?? true) && (u$1(NowIndicatorDot, { className: joinClassNames(classNames.abs, classNames.start0), style: { top } }))] }));
+ }
+
+ // Firefox is terrible at rendering absolute elements that span across multiple print pages
+ const isBrowserPrintQuirky = /* true || */ (typeof navigator !== 'undefined' &&
+ navigator.userAgent.toLowerCase().includes('firefox'));
+ class TimeGridCol extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.sortEventSegs = memoize(sortEventSegs);
+ this.getDateMeta = memoize(getDateMeta);
+ }
+ render() {
+ let { props, context } = this;
+ let { options, dateEnv } = context;
+ let isSelectMirror = options.selectMirror;
+ let mirrorSegs = // yuck
+ (props.eventDrag && props.eventDrag.segs) ||
+ (props.eventResize && props.eventResize.segs) ||
+ (isSelectMirror && props.dateSelectionSegs) ||
+ [];
+ let dateMeta = this.getDateMeta(props.date, dateEnv, props.dateProfile, props.todayRange);
+ const baseClassName = joinClassNames(props.borderStart ? classNames.borderOnlyS : classNames.borderNone, props.width == null && classNames.liquid, classNames.rel);
+ const baseStyle = {
+ width: props.width,
+ zIndex: 1, // get above slots
+ };
+ const isStack = this.getIsStack();
+ const renderProps = {
+ ...dateMeta,
+ ...props.renderProps,
+ isStack,
+ isNarrow: props.isNarrow,
+ isMajor: props.isMajor,
+ view: context.viewApi,
+ };
+ if (dateMeta.isDisabled) {
+ return (u$1("div", { role: 'gridcell', "aria-disabled": true, className: joinClassNames(generateClassName(options.dayLaneClass, renderProps), baseClassName), style: baseStyle }));
+ }
+ const innerClassName = joinClassNames(generateClassName(options.dayLaneInnerClass, renderProps), !isStack && classNames.fill);
+ const sortedFgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder);
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ ...props.attrs,
+ role: 'gridcell',
+ ...(dateMeta.isToday ? { 'aria-current': 'date' } : {}),
+ 'data-date': formatDayString(props.date),
+ }, className: baseClassName, style: baseStyle, renderProps: renderProps, generatorName: undefined, classNameGenerator: options.dayLaneClass, didMount: options.dayLaneDidMount, willUnmount: options.dayLaneWillUnmount, children: () => (u$1(S, { children: [this.renderFillSegs(props.businessHourSegs, 'non-business'), this.renderFillSegs(props.bgEventSegs, 'bg-event'), this.renderFillSegs(props.dateSelectionSegs, 'highlight'), u$1("div", { className: innerClassName, style: { zIndex: 1 }, children: this.renderFgSegs(sortedFgSegs,
+ /* isMirror = */ false) }), Boolean(mirrorSegs.length) && (
+ // but only show it when there are actual mirror events, to avoid blocking clicks
+ u$1("div", { className: innerClassName, style: { zIndex: 1 }, children: this.renderFgSegs(mirrorSegs,
+ /* isMirror = */ true) })), this.renderNowIndicator(props.nowIndicatorSegs)] })) }));
+ }
+ renderFgSegs(sortedFgSegs, isMirror) {
+ const { props } = this;
+ if (this.getIsStack()) {
+ return renderPlainFgSegs(sortedFgSegs, props, isMirror);
+ }
+ return this.renderPositionedFgSegs(sortedFgSegs, isMirror);
+ }
+ renderPositionedFgSegs(segs, // if not mirror, needs to be sorted
+ isMirror) {
+ let { props, context } = this;
+ let { date, dateProfile, eventSelection, todayRange, nowDate } = props;
+ let { eventMaxStack, eventShortHeight, eventOrderStrict, eventMinHeight } = context.options;
+ // TODO: memoize this?
+ let segVerticals = computeFgSegVerticals(segs, dateProfile, date, props.slatCnt, props.slatHeight, eventMinHeight, eventShortHeight);
+ let [segRects, hiddenGroups] = buildWebPositioning(segs, segVerticals, eventOrderStrict, eventMaxStack);
+ return (u$1(S, { children: [segs.map((seg, index) => {
+ let { eventRange } = seg;
+ let { instanceId } = eventRange.instance; // guaranteed because it's an fg event
+ let segVertical = segVerticals[index] || {};
+ let segRect = segRects.get(instanceId); // for horizontals. could be undefined!? HACK
+ let hStyle = (!isMirror && segRect)
+ ? this.computeSegHStyle(segRect)
+ : { left: 0, right: 0, zIndex: 0 };
+ let isSelected = instanceId === eventSelection;
+ if (isSelected) {
+ hStyle.zIndex += 1000; // HACK: relies on hardcoded z-index offset; fragile if stacking context changes
+ }
+ let isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
+ let isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
+ let isInvisible = !isMirror && (isDragging || isResizing || !segRect);
+ return (u$1("div", {
+ // we would have used classNames.fill, but multi-page spanning breaks in Firefox
+ // we would have used height:100%, but multi-page spanning breaks in Safari
+ className: joinClassNames(classNames.abs, classNames.flexCol), style: {
+ visibility: isInvisible ? 'hidden' : undefined,
+ top: segVertical.start,
+ height: segVertical.size,
+ ...hStyle,
+ }, children: u$1(TimeGridEvent, { eventRange: eventRange, slicedStart: seg.startDate, slicedEnd: seg.endDate, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: isMirror, isSelected: isSelected, level: segRect ? segRect.stackDepth : 0, isNarrow: props.isNarrow, isShort: segVertical.isShort || false, isLiquid: true, ...getEventRangeMeta(eventRange, todayRange, nowDate) }) }, instanceId));
+ }), this.renderHiddenGroups(hiddenGroups)] }));
+ }
+ /*
+ NOTE: will already have eventMinHeight applied because segEntries(?) already had it
+ */
+ renderHiddenGroups(hiddenGroups) {
+ let { dateSpanProps, dateProfile, todayRange, nowDate, eventSelection, eventDrag, eventResize, isNarrow, isMicro } = this.props;
+ return (u$1(S, { children: hiddenGroups.map((hiddenGroup) => {
+ return (u$1(TimeGridMoreLink, { hiddenSegs: hiddenGroup.segs, top: hiddenGroup.start, height: hiddenGroup.end - hiddenGroup.start, isNarrow: isNarrow, isMicro: isMicro, dateSpanProps: dateSpanProps, dateProfile: dateProfile, todayRange: todayRange, nowDate: nowDate, eventSelection: eventSelection, eventDrag: eventDrag, eventResize: eventResize }, hiddenGroup.key));
+ }) }));
+ }
+ renderFillSegs(segs, fillType) {
+ let { props, context } = this;
+ let segVerticals = computeFgSegVerticals(segs, props.dateProfile, props.date, props.slatCnt, props.slatHeight, context.options.eventMinHeight, context.options.eventShortHeight);
+ return (u$1(S, { children: segs.map((seg, index) => {
+ const { eventRange } = seg;
+ const segVertical = segVerticals[index] || {};
+ return (u$1("div", { className: classNames.fillX, style: {
+ top: segVertical.start,
+ height: segVertical.size,
+ // HACK to get bg fills to overlap cell-start border
+ // which matches how dayGrid looks,
+ // which is important because all-day background events, in TimeGrid,
+ // will render on both at the same time
+ marginInlineStart: -1,
+ }, children: fillType === 'bg-event' ?
+ u$1(BgEvent, { eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isNarrow: props.isNarrow, isShort: segVertical.isShort || false, isVertical: true, ...getEventRangeMeta(eventRange, props.todayRange, props.nowDate) }) :
+ renderFill(fillType, context.options) }, buildEventRangeKey(eventRange)));
+ }) }));
+ }
+ renderNowIndicator(segs) {
+ let { props } = this;
+ if (props.forPrint || this.getIsStack()) {
+ return;
+ }
+ return segs.map((seg, i) => (u$1(TimeGridNowIndicatorLine, { nowDate: seg.startDate, dayDate: props.date, dateProfile: props.dateProfile, totalHeight: props.slatHeight != null ? props.slatHeight * props.slatCnt : undefined, showDot: seg.showDot ?? true }, i)));
+ }
+ /*
+ TODO: eventually move to width, not left+right
+ */
+ computeSegHStyle(segRect) {
+ let { options } = this.context;
+ let shouldOverlap = options.slotEventOverlap;
+ let nearCoord = segRect.levelCoord; // the left side if LTR. the right side if RTL. floating-point
+ let farCoord = segRect.levelCoord + segRect.thickness; // the right side if LTR. the left side if RTL. floating-point
+ if (shouldOverlap) {
+ // double the width, but don't go beyond the maximum forward coordinate (1.0)
+ farCoord = Math.min(1, nearCoord + (farCoord - nearCoord) * 2);
+ }
+ let props = {
+ zIndex: segRect.stackDepth + 1, // convert from 0-base to 1-based
+ insetInlineStart: fracToCssDim(nearCoord),
+ insetInlineEnd: fracToCssDim(1 - farCoord),
+ marginInlineEnd: undefined,
+ };
+ if (shouldOverlap && segRect.stackForward) {
+ // add padding to the edge so that forward stacked events don't cover the resizer's icon
+ props.marginInlineEnd = 10 * 2; // 10 is a guesstimate of the icon's width
+ }
+ return props;
+ }
+ getIsStack() {
+ const { eventPrintLayout } = this.context.options;
+ return this.props.forPrint && (eventPrintLayout === 'stack' ||
+ (eventPrintLayout !== 'grid' /* aka 'auto' */ && isBrowserPrintQuirky));
+ }
+ }
+ function renderPlainFgSegs(sortedFgSegs, { todayRange, nowDate, eventSelection, eventDrag, eventResize }, isMirror) {
+ return (u$1(S, { children: sortedFgSegs.map((seg) => {
+ let { eventRange } = seg;
+ let { instanceId } = eventRange.instance;
+ let isDragging = Boolean(eventDrag && eventDrag.affectedInstances[instanceId]);
+ let isResizing = Boolean(eventResize && eventResize.affectedInstances[instanceId]);
+ let isInvisible = isDragging || isResizing;
+ return (u$1("div", { className: classNames.breakInsideAvoid, style: { visibility: isInvisible ? 'hidden' : undefined }, children: u$1(TimeGridEvent, { eventRange: eventRange, slicedStart: seg.startDate, slicedEnd: seg.endDate, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: isMirror, isSelected: instanceId === eventSelection, level: 0, isShort: false, isNarrow: false, disableResizing: true, ...getEventRangeMeta(eventRange, todayRange, nowDate) }) }, instanceId));
+ }) }));
+ }
+
+ class TimeGridCols extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.processSlotOptions = memoize(processSlotOptions);
+ this.handleRootEl = (el) => {
+ this.rootEl = el;
+ if (el) {
+ this.context.registerInteractiveComponent(this, {
+ el,
+ isHitComboAllowed: this.props.isHitComboAllowed,
+ });
+ }
+ else {
+ this.context.unregisterInteractiveComponent(this);
+ }
+ };
+ }
+ render() {
+ const { props } = this;
+ return (u$1("div", { role: props.role /* !!! */, className: joinClassNames(props.className, classNames.flexRow), ref: this.handleRootEl, children: props.cells.map((cell, col) => (u$1(TimeGridCol, { dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, date: cell.date, isMajor: cell.isMajor, slatCnt: props.slatCnt, renderProps: cell.renderProps, attrs: cell.attrs, dateSpanProps: cell.dateSpanProps, forPrint: props.forPrint, borderStart: Boolean(col), isNarrow: props.cellIsNarrow, isMicro: props.cellIsMicro,
+ // content
+ fgEventSegs: props.fgEventSegsByCol[col], bgEventSegs: props.bgEventSegsByCol[col], businessHourSegs: props.businessHourSegsByCol[col], nowIndicatorSegs: props.nowIndicatorSegsByCol[col], dateSelectionSegs: props.dateSelectionSegsByCol[col], eventDrag: props.eventDragByCol[col], eventResize: props.eventResizeByCol[col], eventSelection: props.eventSelection,
+ // dimensions
+ width: props.colWidth, slatHeight: props.slatHeight }, cell.key))) }));
+ }
+ queryHit(isRtl, positionLeft, positionTop, elWidth) {
+ const { dateProfile, cells, colWidth, slatHeight } = this.props;
+ const { dateEnv, options } = this.context;
+ const { snapDuration, snapsPerSlot } = this.processSlotOptions(options.slotDuration, options.snapDuration);
+ const colCount = cells.length;
+ const { col, left, right } = computeColFromPosition(positionLeft, elWidth, colWidth, colCount, isRtl);
+ const cell = cells[col];
+ const slatIndex = Math.floor(positionTop / slatHeight);
+ const slatTop = slatIndex * slatHeight;
+ const partial = (positionTop - slatTop) / slatHeight; // floating point number between 0 and 1
+ const localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
+ const snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
+ const time = addDurations(dateProfile.slotMinTime, multiplyDuration(snapDuration, snapIndex));
+ const start = dateEnv.add(cell.date, time);
+ const end = dateEnv.add(start, snapDuration);
+ return {
+ dateProfile,
+ dateSpan: {
+ range: { start, end },
+ allDay: false,
+ ...cell.dateSpanProps,
+ },
+ getDayEl: () => getCellEl(this.rootEl, col),
+ rect: {
+ left,
+ right,
+ top: slatTop,
+ bottom: slatTop + slatHeight,
+ },
+ layer: 0,
+ };
+ }
+ }
+ TimeGridCols.addPropsEquality({
+ style: isPropsEqualShallow,
+ });
+ // Utils
+ // -------------------------------------------------------------------------------------------------
+ function processSlotOptions(slotDuration, snapDurationOverride) {
+ let snapDuration = snapDurationOverride || slotDuration;
+ let snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration);
+ if (snapsPerSlot === null) {
+ snapDuration = slotDuration;
+ snapsPerSlot = 1;
+ // TODO: say warning?
+ }
+ return { snapDuration, snapsPerSlot };
+ }
+
+ const NowIndicatorHeaderContainer = (props) => (u$1(ViewContextType.Consumer, { children: (context) => {
+ let { options } = context;
+ let renderProps = {
+ date: context.dateEnv.toDate(props.date),
+ view: context.viewApi,
+ };
+ return (u$1(ContentContainer, { elRef: props.elRef, tag: props.tag || 'div', attrs: props.attrs, className: props.className, style: props.style, renderProps: renderProps, generatorName: "nowIndicatorHeaderContent", customGenerator: options.nowIndicatorHeaderContent, classNameGenerator: options.nowIndicatorHeaderClass, didMount: options.nowIndicatorHeaderDidMount, willUnmount: options.nowIndicatorHeaderWillUnmount, children: props.children }));
+ } }));
+
+ /*
+ TODO: DRY with other NowIndicator components
+ */
+ function TimeGridNowIndicatorArrow(props) {
+ return (u$1("div", {
+ // crop any overflow that the arrow/line might cause
+ // TODO: just do this on the entire canvas within the scroller
+ className: joinClassNames(classNames.fill, classNames.crop), style: {
+ zIndex: 2, // inlined from $now-indicator-z
+ pointerEvents: 'none', // TODO: className
+ }, children: u$1(NowIndicatorHeaderContainer, { className: classNames.abs, style: {
+ top: props.totalHeight != null
+ ? props.totalHeight * computeDateTopFrac(props.nowDate, props.dateProfile)
+ : undefined
+ }, date: props.nowDate }) }));
+ }
+
+ const DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: true,
+ meridiem: 'short',
+ });
+ /*
+ Always oriented in a column
+ */
+ class TimeGridSlatHeader extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.createRenderProps = memoize(createRenderProps);
+ // ref
+ this.innerElRef = M$1();
+ }
+ render() {
+ let { props, context } = this;
+ let { options } = context;
+ let headerFormat = // TODO: fully pre-parse
+ options.slotHeaderFormat == null ? DEFAULT_SLAT_LABEL_FORMAT :
+ Array.isArray(options.slotHeaderFormat) ? createFormatter(options.slotHeaderFormat[0]) :
+ createFormatter(options.slotHeaderFormat);
+ let renderProps = this.createRenderProps(props.date, props.time, !props.isLabeled, props.isNarrow, props.isFirst, headerFormat, context);
+ let className = joinClassNames(props.liquidHeight && classNames.liquid, classNames.flexRow, classNames.alignStart, classNames.noMargin, classNames.noPadding, props.borderTop ? classNames.borderOnlyT : classNames.borderNone);
+ if (!props.isLabeled) {
+ return (u$1("div", { className: joinClassNames(generateClassName(options.slotHeaderClass, renderProps), className), style: {
+ height: props.height,
+ } }));
+ }
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ 'data-time': props.isoTimeStr,
+ }, style: {
+ height: props.height,
+ }, className: className, renderProps: renderProps, generatorName: "slotHeaderContent", customGenerator: options.slotHeaderContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotHeaderClass, didMount: options.slotHeaderDidMount, willUnmount: options.slotHeaderWillUnmount, children: (InnerContent) => (u$1("div", { ref: this.innerElRef, className: joinClassNames(classNames.noShrink, classNames.whiteSpaceNoWrap, classNames.flexRow), children: u$1(InnerContent, { tag: "div", className: generateClassName(options.slotHeaderInnerClass, renderProps) }) })) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ const { props } = this;
+ const innerEl = this.innerElRef.current; // TODO: make dynamic with useEffect
+ if (innerEl) { // could be null if !isLabeled
+ // TODO: only attach this if refs props present
+ // TODO: fire width/height independently?
+ this.disconnectInnerSize = watchSize(innerEl, (width, height) => {
+ if (this._isUnmounting)
+ return;
+ setRef(props.innerWidthRef, width);
+ setRef(props.innerHeightRef, height);
+ });
+ }
+ }
+ componentWillUnmount() {
+ const { props } = this;
+ this._isUnmounting = true;
+ if (this.disconnectInnerSize) {
+ this.disconnectInnerSize();
+ setRef(props.innerWidthRef, null);
+ setRef(props.innerHeightRef, null);
+ }
+ }
+ }
+ function createRenderProps(date, time, isMinor, isNarrow, isFirst, headerFormat, context) {
+ return {
+ // this is a time-specific slot. not day-specific, so don't do today/nowRange
+ ...getDateMeta(date, context.dateEnv),
+ level: 0, // axis level (for when multiple axes)
+ text: joinDateTimeFormatParts(context.dateEnv.formatToParts(date, headerFormat)),
+ time: time,
+ isMajor: false,
+ isMinor,
+ isTime: true,
+ isNarrow,
+ hasNavLink: false,
+ isFirst,
+ view: context.viewApi,
+ };
+ }
+ function renderInnerContent(props) {
+ return props.text;
+ }
+
+ class TimeGridSlatLane extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.getDateMeta = memoize(getDateMeta);
+ }
+ render() {
+ let { props, context } = this;
+ let { options } = context;
+ let renderProps = {
+ // this is a time-specific slot. not day-specific, so don't do today/nowRange
+ ...this.getDateMeta(props.date, context.dateEnv),
+ time: props.time,
+ isMajor: false,
+ isMinor: !props.isLabeled,
+ view: context.viewApi,
+ };
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ 'data-time': props.isoTimeStr,
+ }, className: joinClassNames(classNames.noMargin, classNames.noPadding, classNames.liquid, props.borderTop ? classNames.borderOnlyT : classNames.borderNone), renderProps: renderProps, generatorName: undefined, classNameGenerator: options.slotLaneClass, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }));
+ }
+ }
+
+ const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' });
+ class TimeGridWeekNumber extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // ref
+ this.innerElRef = M$1();
+ }
+ render() {
+ let { props, context } = this;
+ let { options, dateEnv } = context;
+ let range = props.dateProfile.renderRange;
+ let dayCnt = diffDays(range.start, range.end);
+ // HACK: only make week-number a nav-link when NOT in week-view
+ let hasNavLink = dayCnt === 1 && options.navLinks;
+ let weekDateMarker = range.start;
+ let fullDateStr = buildDateStr(context, weekDateMarker, 'week');
+ let weekNum = dateEnv.computeWeekNumber(weekDateMarker);
+ let weekTextParts = dateEnv.formatToParts(weekDateMarker, options.weekNumberFormat || DEFAULT_WEEK_NUM_FORMAT);
+ let weekText = joinDateTimeFormatParts(weekTextParts);
+ let weekDateZoned = dateEnv.toDate(weekDateMarker);
+ const weekNumberRenderProps = {
+ num: weekNum,
+ text: weekText,
+ textParts: weekTextParts,
+ date: weekDateZoned,
+ isNarrow: props.isNarrow,
+ hasNavLink,
+ options: { dayMinWidth: options.dayMinWidth },
+ };
+ return (u$1(ContentContainer, { tag: 'div', attrs: {
+ role: 'gridcell', // doesn't always describe other cells in row, so make generic
+ 'aria-label': fullDateStr,
+ }, className: joinClassNames(classNames.flexRow, classNames.noMargin, classNames.noPadding, props.isLiquid ? classNames.liquid : classNames.contentBox), style: {
+ width: props.width,
+ }, renderProps: weekNumberRenderProps, generatorName: "weekNumberHeaderContent", customGenerator: options.weekNumberHeaderContent, defaultGenerator: renderText$1, classNameGenerator: options.weekNumberHeaderClass, didMount: options.weekNumberHeaderDidMount, willUnmount: options.weekNumberHeaderWillUnmount, children: (InnerContent) => (u$1("div", { ref: this.innerElRef, className: joinClassNames(classNames.flexRow, classNames.noShrink, classNames.whiteSpaceNoWrap), children: u$1(InnerContent, { tag: 'div', attrs: hasNavLink
+ ? buildNavLinkAttrs(context, range.start, 'week', fullDateStr)
+ : { 'aria-label': fullDateStr }, className: generateClassName(options.weekNumberHeaderInnerClass, weekNumberRenderProps) }) })) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ const { props } = this;
+ const innerEl = this.innerElRef.current; // TODO: make dynamic with useEffect
+ // TODO: only attach this if refs props present
+ // TODO: handle width/height independently?
+ this.disconnectInnerSize = watchSize(innerEl, (width, height) => {
+ if (this._isUnmounting)
+ return;
+ setRef(props.innerWidthRef, width);
+ setRef(props.innerHeightRef, height);
+ });
+ }
+ componentWillUnmount() {
+ const { props } = this;
+ this._isUnmounting = true;
+ this.disconnectInnerSize();
+ setRef(props.innerWidthRef, null);
+ setRef(props.innerHeightRef, null);
+ }
+ }
+
+ function TimeGridAxisEmpty(props) {
+ return (u$1("div", { role: 'gridcell' // is empty so can't be rowheader/columnheader
+ , className: props.isLiquid ? classNames.liquid : classNames.contentBox, style: { width: props.width } }));
+ }
+
+ class TimeGridLayoutPannable extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {
+ headerTierHeights: [],
+ };
+ // refs
+ this.headerLabelInnerWidthRefMap = new RefMap(() => {
+ afterSize(this.handleAxisWidths);
+ });
+ this.headerLabelInnerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleHeaderHeights);
+ });
+ this.headerMainInnerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleHeaderHeights);
+ });
+ this.handleAllDayLabelInnerWidth = (width) => {
+ this.allDayLabelInnerWidth = width;
+ afterSize(this.handleAxisWidths);
+ };
+ this.slatLabelInnerWidthRefMap = new RefMap(() => {
+ afterSize(this.handleAxisWidths);
+ });
+ this.slatLabelInnerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleSlatInnerHeights);
+ });
+ this.headerScrollerRef = M$1();
+ this.allDayScrollerRef = M$1();
+ this.mainScrollerRef = M$1();
+ this.footScrollerRef = M$1();
+ this.axisScrollerRef = M$1();
+ // Sizing
+ // -----------------------------------------------------------------------------------------------
+ this.handleTotalWidth = (totalWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ totalWidth });
+ };
+ this.handleBodyHeight = (bodyHeight) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ bodyHeight });
+ };
+ this.handleClientWidth = (clientWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientWidth });
+ };
+ this.handleClientHeight = (clientHeight) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientHeight });
+ };
+ this.handleStickyBottomScrollbarWidth = (sticykBottomScrollbarWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ sticykBottomScrollbarWidth });
+ };
+ this.handleHeaderHeights = () => {
+ if (this._isUnmounting)
+ return;
+ const headerLabelInnerHeightMap = this.headerLabelInnerHeightRefMap.current;
+ const headerMainInnerHeightMap = this.headerMainInnerHeightRefMap.current;
+ const heights = [];
+ // important to loop using 'main' because 'label' might not be tracking height if empty
+ for (const [tierNum, mainHeight] of headerMainInnerHeightMap.entries()) {
+ heights[tierNum] = Math.max(headerLabelInnerHeightMap.get(tierNum) || 0, mainHeight);
+ }
+ this.setState({ headerTierHeights: heights });
+ };
+ this.handleSlatInnerHeights = () => {
+ if (this._isUnmounting)
+ return;
+ const slatLabelInnerHeightMap = this.slatLabelInnerHeightRefMap.current;
+ let max = 0;
+ for (const slatLabelInnerHeight of slatLabelInnerHeightMap.values()) {
+ max = Math.max(max, slatLabelInnerHeight);
+ }
+ if (this.state.slatInnerHeight !== max) {
+ this.setState({ slatInnerHeight: max });
+ }
+ };
+ this.handleAxisWidths = () => {
+ if (this._isUnmounting)
+ return;
+ const headerLabelInnerWidthMap = this.headerLabelInnerWidthRefMap.current;
+ const slatLabelInnerWidthMap = this.slatLabelInnerWidthRefMap.current;
+ let max = this.allDayLabelInnerWidth || 0; // guard against all-day slot hidden
+ for (const headerLabelInnerWidth of headerLabelInnerWidthMap.values()) {
+ max = Math.max(max, headerLabelInnerWidth);
+ }
+ for (const slatLableInnerWidth of slatLabelInnerWidthMap.values()) {
+ max = Math.max(max, slatLableInnerWidth);
+ }
+ if (this.state.axisWidth !== max) {
+ this.setState({ axisWidth: max });
+ }
+ };
+ }
+ render() {
+ const { props, state, context, headerLabelInnerWidthRefMap, headerLabelInnerHeightRefMap, headerMainInnerHeightRefMap, slatLabelInnerWidthRefMap, slatLabelInnerHeightRefMap, } = this;
+ const { nowDate, headerTiers, forPrint } = props;
+ const nowTimeMs = nowDate.valueOf() - startOfDay(nowDate).valueOf();
+ const { axisWidth, totalWidth, clientWidth, clientHeight, bodyHeight, sticykBottomScrollbarWidth } = state;
+ const { options } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const endScrollbarWidth = (totalWidth != null && clientWidth != null && axisWidth != null)
+ ? totalWidth - clientWidth - (axisWidth + 1) // +1 for hardcoded divider!
+ : undefined;
+ const verticalScrolling = !forPrint && !getIsHeightAuto(options);
+ const tableHeaderSticky = !forPrint && getTableHeaderSticky(options);
+ const footerScrollbarSticky = !forPrint && getFooterScrollbarSticky(options);
+ // TODO: DRY with getIsStack
+ const { eventPrintLayout } = options;
+ const printStackEnabled = (eventPrintLayout === 'stack' ||
+ (eventPrintLayout !== 'grid' /* aka 'auto' */ && isBrowserPrintQuirky));
+ const absPrint = forPrint && !printStackEnabled;
+ const simplePrint = forPrint && printStackEnabled;
+ const colCount = props.cells.length;
+ const [canvasWidth, colWidth] = computeColWidth(colCount, props.dayMinWidth, clientWidth);
+ const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
+ const slatCnt = props.slatMetas.length;
+ const [slatHeight, slatLiquidHeight] = computeSlatHeight(// TODO: memo?
+ verticalScrolling && options.expandRows, slatCnt, options.slotMinHeight, state.slatInnerHeight, clientHeight);
+ this.slatHeight = slatHeight;
+ // TODO: have computeSlatHeight return?
+ const totalSlatHeight = (slatHeight || 0) * slatCnt;
+ const forcedBodyHeight = absPrint ? totalSlatHeight : undefined;
+ const rowsNotExpanding = verticalScrolling && !options.expandRows &&
+ clientHeight != null && clientHeight > totalSlatHeight;
+ const firstBodyRowIndex = options.dayHeaders ? headerTiers.length + 1 : 1;
+ const bottomScrollbarWidth = footerScrollbarSticky
+ ? sticykBottomScrollbarWidth
+ : (bodyHeight != null && clientHeight != null)
+ ? (bodyHeight - clientHeight)
+ : undefined;
+ return (u$1(S, { children: [options.dayHeaders && (u$1("div", { className: joinClassNames(generateClassName(options.tableHeaderClass, {
+ isSticky: tableHeaderSticky,
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }),
+ // see note in TimeGridLayout about why we don't do classNames.printHeader
+ classNames.flexCol, tableHeaderSticky && classNames.tableHeaderSticky), style: {
+ zIndex: 1,
+ }, children: [u$1("div", { className: classNames.flexRow, children: [u$1("div", { role: 'rowgroup', className: classNames.contentBox, style: { width: axisWidth }, children: headerTiers.map((rowConfig, tierNum) => (u$1("div", { role: 'row', "aria-rowindex": tierNum + 1, className: joinClassNames(options.dayHeaderRowClass, classNames.flexRow, classNames.contentBox, tierNum < props.headerTiers.length - 1
+ ? classNames.borderOnlyB
+ : classNames.borderNone), style: {
+ height: state.headerTierHeights[tierNum]
+ }, children: (options.weekNumbers && rowConfig.isDateRow) ? (u$1(TimeGridWeekNumber, { dateProfile: props.dateProfile, innerWidthRef: headerLabelInnerWidthRefMap.createRef(tierNum), innerHeightRef: headerLabelInnerHeightRefMap.createRef(tierNum), width: undefined, isLiquid: true, isNarrow: cellIsNarrow })) : (u$1(TimeGridAxisEmpty, { width: undefined, isLiquid: true })) }, tierNum))) }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: true,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1(Scroller, { horizontal: true, hideScrollbars: true, className: joinClassNames(classNames.flexRow, classNames.liquid), ref: this.headerScrollerRef, children: [u$1("div", { role: 'rowgroup', className: canvasWidth == null ? classNames.liquid : '', style: { width: canvasWidth }, children: props.headerTiers.map((rowConfig, tierNum) => (k$1(DayGridHeaderRow, { ...rowConfig, key: tierNum, role: 'row', rowIndex: tierNum, borderBottom: tierNum < props.headerTiers.length - 1, height: state.headerTierHeights[tierNum], colWidth: colWidth, viewportWidth: clientWidth, innerHeightRef: headerMainInnerHeightRefMap.createRef(tierNum), cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: props.headerTiers.length - tierNum - 1 }))) }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] })] }), u$1("div", { className: generateClassName(options.dayHeaderDividerClass, {
+ isSticky: tableHeaderSticky,
+ multiMonthColumns: 0,
+ options: { allDaySlot: Boolean(options.allDaySlot) },
+ }) })] })), u$1("div", { role: 'rowgroup', className: joinClassNames(generateClassName(options.tableBodyClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }), classNames.flexCol, verticalScrolling && classNames.liquid, classNames.isolate), style: {
+ zIndex: 0,
+ }, children: [options.allDaySlot && (u$1(S, { children: [u$1("div", { role: 'row', "aria-rowindex": firstBodyRowIndex, className: classNames.flexRow, style: { zIndex: 1 }, children: [u$1(TimeGridAllDayHeader, { width: axisWidth, innerWidthRef: this.handleAllDayLabelInnerWidth, isNarrow: cellIsNarrow }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: false,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1(Scroller, { horizontal: true, hideScrollbars: true,
+ // fill remaining width
+ className: joinClassNames(classNames.flexRow, classNames.liquidX), ref: this.allDayScrollerRef, children: [u$1("div", { className: classNames.flexRow, style: { width: canvasWidth }, children: u$1(TimeGridAllDayLane, { dateProfile: props.dateProfile, todayRange: props.todayRange, cells: props.cells, showDayNumbers: false, forPrint: forPrint, isHitComboAllowed: props.isHitComboAllowed, className: joinClassNames(classNames.borderNone, classNames.liquidX), cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
+ // content
+ fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows,
+ // dimensions
+ colWidth: colWidth }) }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: false }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] })] }), u$1("div", { className: joinClassNames(options.allDayDividerClass), style: { zIndex: 2 } })] })), u$1("div", { role: 'row', "aria-rowindex": firstBodyRowIndex + (options.allDaySlot ? 1 : 0), className: joinClassNames(classNames.flexRow, classNames.rel, // for Ruler.fillStart
+ verticalScrolling && classNames.liquid), style: {
+ zIndex: 0,
+ }, children: [u$1(Scroller, { vertical: verticalScrolling, hideScrollbars: true, className: joinClassNames(classNames.flexCol, classNames.contentBox), style: {
+ width: axisWidth,
+ }, ref: this.axisScrollerRef, clientHeightRef: this.handleBodyHeight, children: !simplePrint && (u$1(S, { children: u$1("div", { role: 'rowheader', "aria-label": options.timedText, className: joinClassNames(classNames.flexCol, classNames.grow, classNames.rel), style: {
+ height: forcedBodyHeight,
+ }, children: [u$1("div", { "aria-hidden": true, className: joinClassNames(classNames.flexCol, (verticalScrolling && options.expandRows) && classNames.grow, absPrint && classNames.fillX), children: props.slatMetas.map((slatMeta, slatI) => (k$1(TimeGridSlatHeader, { ...slatMeta /* FYI doesn't need isoTimeStr */, key: slatMeta.key, innerWidthRef: slatLabelInnerWidthRefMap.createRef(slatMeta.key), innerHeightRef: slatLabelInnerHeightRefMap.createRef(slatMeta.key), borderTop: Boolean(slatI), isNarrow: cellIsNarrow, height: slatLiquidHeight ? undefined : slatHeight, liquidHeight: slatLiquidHeight }))) }), !forPrint && options.nowIndicator && rangeContainsMarker(props.dateProfile.currentRange, nowDate) &&
+ nowTimeMs >= props.dateProfile.slotMinTime.milliseconds &&
+ nowTimeMs < props.dateProfile.slotMaxTime.milliseconds && (u$1(TimeGridNowIndicatorArrow, { nowDate: nowDate, dateProfile: props.dateProfile, totalHeight: slatHeight != null ? slatHeight * slatCnt : undefined })), Boolean(rowsNotExpanding || bottomScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: false }), classNames.borderOnlyT, rowsNotExpanding && classNames.liquid), style: {
+ minHeight: bottomScrollbarWidth
+ } }))] }) })) }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: false,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1("div", {
+ // we need this div because it's bad for Scroller to have left/right borders,
+ // AND because we need to containt the FooterScrollbar
+ className: joinClassNames(classNames.flexCol, classNames.liquid), children: [u$1(Scroller, { vertical: verticalScrolling, horizontal: true, hideScrollbars: footerScrollbarSticky || // also means height:auto, so won't need vertical scrollbars anyway
+ forPrint, className: joinClassNames(classNames.flexCol, classNames.rel, // for Ruler.fillStart
+ verticalScrolling && classNames.liquid), ref: this.mainScrollerRef, clientWidthRef: this.handleClientWidth, clientHeightRef: this.handleClientHeight, children: u$1("div", { className: joinClassNames(classNames.flexCol, classNames.grow, classNames.rel), style: {
+ width: canvasWidth,
+ height: forcedBodyHeight,
+ }, children: [u$1(TimeGridCols, { dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, cells: props.cells, slatCnt: slatCnt, forPrint: forPrint, isHitComboAllowed: props.isHitComboAllowed, className: simplePrint ? '' : classNames.fill,
+ // content
+ fgEventSegsByCol: props.fgEventSegsByCol, bgEventSegsByCol: props.bgEventSegsByCol, businessHourSegsByCol: props.businessHourSegsByCol, nowIndicatorSegsByCol: props.nowIndicatorSegsByCol, dateSelectionSegsByCol: props.dateSelectionSegsByCol, eventDragByCol: props.eventDragByCol, eventResizeByCol: props.eventResizeByCol, eventSelection: props.eventSelection,
+ // dimensions
+ colWidth: colWidth, slatHeight: slatHeight, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro }), !simplePrint && (u$1(S, { children: [u$1("div", { "aria-hidden": true, className: joinClassNames(classNames.flexCol, (verticalScrolling && options.expandRows) && classNames.grow, absPrint ? classNames.fillX : classNames.rel), children: props.slatMetas.map((slatMeta, slatI) => (u$1("div", { className: joinClassNames(classNames.flexRow, slatLiquidHeight && classNames.liquid), style: {
+ height: slatLiquidHeight ? '' : slatHeight
+ }, children: k$1(TimeGridSlatLane, { ...slatMeta /* FYI doesn't need isoTimeStr */, key: slatMeta.key, borderTop: Boolean(slatI) }) }, slatMeta.key))) }), rowsNotExpanding && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: false }), classNames.borderOnlyT, classNames.liquid) }))] }))] }) }), Boolean(footerScrollbarSticky) && (u$1(FooterScrollbar, { isSticky: true, canvasWidth: canvasWidth, scrollerRef: this.footScrollerRef, scrollbarWidthRef: this.handleStickyBottomScrollbarWidth }))] })] })] }), u$1(Ruler, { widthRef: this.handleTotalWidth })] }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.initScrollers();
+ this.updateSlatHeight();
+ }
+ componentDidUpdate() {
+ this.updateScrollers();
+ this.updateSlatHeight();
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.destroyScrollers();
+ this.prevSlatHeight = undefined;
+ setRef(this.props.slatHeightRef, null);
+ }
+ updateSlatHeight() {
+ if (this.prevSlatHeight !== this.slatHeight) {
+ setRef(this.props.slatHeightRef, this.prevSlatHeight = this.slatHeight);
+ }
+ }
+ // Scrolling
+ // -----------------------------------------------------------------------------------------------
+ initScrollers() {
+ const ScrollerSyncer = getScrollerSyncerClass(this.context.pluginHooks);
+ this.dayScroller = new ScrollerSyncer(true); // horizontal=true
+ this.timeScroller = new ScrollerSyncer(); // horizontal=false
+ setRef(this.props.dayScrollerRef, this.dayScroller);
+ setRef(this.props.timeScrollerRef, this.timeScroller);
+ this.updateScrollers();
+ }
+ updateScrollers() {
+ this.dayScroller.handleChildren([
+ this.headerScrollerRef.current,
+ this.allDayScrollerRef.current,
+ this.mainScrollerRef.current,
+ this.footScrollerRef.current,
+ ]);
+ this.timeScroller.handleChildren([
+ this.axisScrollerRef.current,
+ this.mainScrollerRef.current,
+ ]);
+ }
+ destroyScrollers() {
+ setRef(this.props.dayScrollerRef, null);
+ setRef(this.props.timeScrollerRef, null);
+ }
+ }
+ TimeGridLayoutPannable.addPropsEquality({
+ headerTierHeights: isArraysEqual,
+ });
+
+ class TimeGridLayoutNormal extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ // refs
+ this.headerLabelInnerWidthRefMap = new RefMap(() => {
+ afterSize(this.handleAxisInnerWidths);
+ });
+ this.handleAllDayLabelInnerWidth = (width) => {
+ this.allDayLabelInnerWidth = width;
+ afterSize(this.handleAxisInnerWidths);
+ };
+ this.handleWeekNumberInnerWidth = (width) => {
+ this.weekNumberInnerWidth = width;
+ afterSize(this.handleAxisInnerWidths);
+ };
+ this.slatLabelInnerWidthRefMap = new RefMap(() => {
+ afterSize(this.handleAxisInnerWidths);
+ });
+ this.slatLabelInnerHeightRefMap = new RefMap(() => {
+ afterSize(this.handleSlatInnerHeights);
+ });
+ // Sizing
+ // -----------------------------------------------------------------------------------------------
+ this.handleTotalWidth = (totalWidth) => {
+ if (this._isUnmounting)
+ return;
+ // Must delay the rerender because might change the width of the all-day DayGridRow events,
+ // which shows a ResizeObserver loop warning
+ requestAnimationFrame(() => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ totalWidth });
+ });
+ };
+ this.handleClientWidth = (clientWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientWidth });
+ };
+ this.handleClientHeight = (clientHeight) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ clientHeight });
+ };
+ this.handleAxisInnerWidths = () => {
+ if (this._isUnmounting)
+ return;
+ const headerLabelInnerWidthMap = this.headerLabelInnerWidthRefMap.current;
+ const slatLabelInnerWidthMap = this.slatLabelInnerWidthRefMap.current;
+ let max = Math.max(this.weekNumberInnerWidth || 0, // might not exist
+ this.allDayLabelInnerWidth || 0 // guard against all-day slot hidden
+ );
+ for (const headerLabelInnerWidth of headerLabelInnerWidthMap.values()) {
+ max = Math.max(max, headerLabelInnerWidth);
+ }
+ for (const slatLabelInnerWidth of slatLabelInnerWidthMap.values()) {
+ max = Math.max(max, slatLabelInnerWidth);
+ }
+ if (this.state.axisWidth !== max) {
+ this.setState({ axisWidth: max });
+ }
+ };
+ this.handleSlatInnerHeights = () => {
+ if (this._isUnmounting)
+ return;
+ const slatLabelInnerHeightMap = this.slatLabelInnerHeightRefMap.current;
+ let max = 0;
+ for (const slatLabelInnerHeight of slatLabelInnerHeightMap.values()) {
+ max = Math.max(max, slatLabelInnerHeight);
+ }
+ if (this.state.slatInnerHeight !== max) {
+ this.setState({ slatInnerHeight: max });
+ }
+ };
+ }
+ render() {
+ const { props, state, context, slatLabelInnerWidthRefMap, slatLabelInnerHeightRefMap, headerLabelInnerWidthRefMap } = this;
+ const { nowDate, forPrint } = props;
+ const nowTimeMs = nowDate.valueOf() - startOfDay(nowDate).valueOf();
+ const { axisWidth, clientWidth, totalWidth } = state;
+ const { options } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const endScrollbarWidth = (totalWidth != null && clientWidth != null && !forPrint)
+ ? totalWidth - clientWidth
+ : undefined;
+ const verticalScrolling = !forPrint && !getIsHeightAuto(options);
+ const tableHeaderSticky = !forPrint && getTableHeaderSticky(options);
+ const slatCnt = props.slatMetas.length;
+ const [slatHeight, slatLiquidHeight] = computeSlatHeight(verticalScrolling && options.expandRows, slatCnt, options.slotMinHeight, state.slatInnerHeight, state.clientHeight);
+ this.slatHeight = slatHeight;
+ // TODO: have computeSlatHeight return?
+ const totalSlatHeight = (slatHeight || 0) * slatCnt;
+ const rowsNotExpanding = verticalScrolling && !options.expandRows &&
+ state.clientHeight != null && state.clientHeight > totalSlatHeight;
+ // TODO: DRY with getIsStack
+ const { eventPrintLayout } = options;
+ const printStackEnabled = (eventPrintLayout === 'stack' ||
+ (eventPrintLayout !== 'grid' /* aka 'auto' */ && isBrowserPrintQuirky));
+ const absPrint = forPrint && !printStackEnabled;
+ const simplePrint = forPrint && printStackEnabled;
+ // for printing
+ // in Chrome, slats and columns both need abs positioning within a relative container for them
+ // to sync across pages, and the relative container needs an explicit height
+ // in Firefox, same applies, but the flex-row for the cells has trouble spanning across page,
+ // so we need to set explicit height on flex-row and all parents
+ const forcedBodyHeight = absPrint ? totalSlatHeight : undefined;
+ const colCount = props.cells.length;
+ const colWidth = clientWidth != null ? clientWidth / colCount : undefined;
+ const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
+ return (u$1(S, { children: [options.dayHeaders && (u$1("div", { role: 'rowgroup', className: joinClassNames(generateClassName(options.tableHeaderClass, {
+ isSticky: tableHeaderSticky,
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }),
+ // see note in TimeGridLayout about why we don't do classNames.printHeader
+ classNames.flexCol, tableHeaderSticky && classNames.tableHeaderSticky), style: {
+ zIndex: 1,
+ }, children: [props.headerTiers.map((rowConfig, tierNum) => (u$1("div", { role: 'row', className: classNames.flexRow, children: [u$1("div", { className: joinClassNames(options.dayHeaderRowClass, classNames.flexRow, tierNum < props.headerTiers.length - 1
+ ? classNames.borderOnlyB
+ : classNames.borderNone), children: (options.weekNumbers && rowConfig.isDateRow) ? (u$1(TimeGridWeekNumber, { dateProfile: props.dateProfile, innerWidthRef: this.handleWeekNumberInnerWidth, innerHeightRef: headerLabelInnerWidthRefMap.createRef(tierNum), width: axisWidth, isLiquid: false, isNarrow: cellIsNarrow })) : (u$1(TimeGridAxisEmpty, { width: axisWidth, isLiquid: false })) }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: true,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1(DayGridHeaderRow, { ...rowConfig, className: classNames.liquid, borderBottom: tierNum < props.headerTiers.length - 1, viewportWidth: clientWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: props.headerTiers.length - tierNum - 1 }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: true }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }, tierNum))), u$1("div", { className: generateClassName(options.dayHeaderDividerClass, {
+ isSticky: tableHeaderSticky,
+ multiMonthColumns: 0,
+ options: { allDaySlot: Boolean(options.allDaySlot) },
+ }) })] })), u$1("div", { role: 'rowgroup', className: joinClassNames(generateClassName(options.tableBodyClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }), classNames.flexCol, verticalScrolling && classNames.liquid, classNames.isolate), style: {
+ zIndex: 0,
+ }, children: [options.allDaySlot && (u$1(S, { children: [u$1("div", { role: 'row', className: classNames.flexRow, style: { zIndex: 1 }, children: [u$1(TimeGridAllDayHeader, { width: axisWidth, innerWidthRef: this.handleAllDayLabelInnerWidth, isNarrow: cellIsNarrow }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: false,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1(TimeGridAllDayLane, { dateProfile: props.dateProfile, todayRange: props.todayRange, cells: props.cells, showDayNumbers: false, forPrint: forPrint, isHitComboAllowed: props.isHitComboAllowed, className: joinClassNames(classNames.liquidX, classNames.borderNone), cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro,
+ // content
+ fgEventSegs: props.fgEventSegs, bgEventSegs: props.bgEventSegs, businessHourSegs: props.businessHourSegs, dateSelectionSegs: props.dateSelectionSegs, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows }), Boolean(endScrollbarWidth) && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: false }), classNames.borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), u$1("div", { className: joinClassNames(options.allDayDividerClass), style: { zIndex: 2 } })] })), u$1(Scroller, { vertical: verticalScrolling, className: joinClassNames(classNames.flexCol, classNames.rel, // for Ruler.fillStart
+ verticalScrolling && classNames.liquid), style: {
+ zIndex: 0,
+ }, ref: props.timeScrollerRef, clientWidthRef: this.handleClientWidth, clientHeightRef: this.handleClientHeight, children: u$1("div", { className: joinClassNames(classNames.flexCol, classNames.grow, classNames.rel), style: {
+ // in print mode, this div creates the height and everything is absolutely positioned within
+ // we need to do this so that slats positioning synces with events's positioning
+ // otherwise, get out of sync on second page
+ height: forcedBodyHeight,
+ }, children: [u$1("div", { role: 'row', className: joinClassNames(classNames.flexRow, !simplePrint && classNames.fill), children: [u$1("div", { role: 'rowheader', "aria-label": options.timedText, className: classNames.contentBox, style: { width: axisWidth } }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: false,
+ options: { dayMinWidth: options.dayMinWidth },
+ }) }), u$1(TimeGridCols, { dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, cells: props.cells, slatCnt: slatCnt, forPrint: forPrint, isHitComboAllowed: props.isHitComboAllowed, className: classNames.liquid,
+ // content
+ fgEventSegsByCol: props.fgEventSegsByCol, bgEventSegsByCol: props.bgEventSegsByCol, businessHourSegsByCol: props.businessHourSegsByCol, nowIndicatorSegsByCol: props.nowIndicatorSegsByCol, dateSelectionSegsByCol: props.dateSelectionSegsByCol, eventDragByCol: props.eventDragByCol, eventResizeByCol: props.eventResizeByCol, eventSelection: props.eventSelection,
+ // dimensions
+ slatHeight: slatHeight, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro })] }), !simplePrint && (u$1(S, { children: [u$1("div", { "aria-hidden": true, className: joinClassNames(classNames.flexCol, (verticalScrolling && options.expandRows) && classNames.grow, absPrint
+ ? classNames.fillX // will assume top:0, height will be decided naturally
+ : classNames.rel), children: props.slatMetas.map((slatMeta, slatI) => (u$1("div", { className: joinClassNames(slatLiquidHeight && classNames.liquid, classNames.flexRow), style: {
+ height: slatLiquidHeight ? undefined : slatHeight
+ }, children: [u$1("div", {
+ // the pannable version of TimeGrid has axis labels all consecutive in one column
+ // simulate this for the non-pannable version
+ className: classNames.flexCol, style: { width: axisWidth }, children: k$1(TimeGridSlatHeader, { ...slatMeta /* FYI doesn't need isoTimeStr */, key: slatMeta.key, innerWidthRef: slatLabelInnerWidthRefMap.createRef(slatMeta.key), innerHeightRef: slatLabelInnerHeightRefMap.createRef(slatMeta.key), borderTop: Boolean(slatI), isNarrow: cellIsNarrow }) }), u$1("div", { className: generateClassName(options.slotHeaderDividerClass, {
+ inTableHeader: false,
+ options: { dayMinWidth: options.dayMinWidth },
+ }), style: { visibility: 'hidden' } }), k$1(TimeGridSlatLane, { ...slatMeta /* FYI doesn't need isoTimeStr */, key: slatMeta.key, borderTop: Boolean(slatI) })] }, slatMeta.key))) }), rowsNotExpanding && (u$1("div", { className: joinClassNames(generateClassName(options.fillerClass, { inTableHeader: false }), classNames.borderOnlyT, classNames.liquid) })), !forPrint && options.nowIndicator && rangeContainsMarker(props.dateProfile.currentRange, nowDate) &&
+ nowTimeMs >= props.dateProfile.slotMinTime.milliseconds &&
+ nowTimeMs < props.dateProfile.slotMaxTime.milliseconds && (u$1(TimeGridNowIndicatorArrow, { nowDate: nowDate, dateProfile: props.dateProfile, totalHeight: slatHeight != null ? slatHeight * slatCnt : undefined }))] }))] }) })] }), u$1(Ruler, { widthRef: this.handleTotalWidth })] }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.updateSlatHeight();
+ }
+ componentDidUpdate() {
+ this.updateSlatHeight();
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.prevSlatHeight = undefined;
+ setRef(this.props.slatHeightRef, null);
+ }
+ updateSlatHeight() {
+ if (this.prevSlatHeight !== this.slatHeight) {
+ setRef(this.props.slatHeightRef, this.prevSlatHeight = this.slatHeight);
+ }
+ }
+ }
+
+ function buildEmptySegCols(segsByCol) {
+ return segsByCol.map(() => []);
+ }
+ function buildEmptyInteractionCols(interactionsByCol) {
+ return interactionsByCol.map(() => null);
+ }
+ class TimeGridLayout extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.buildSlatMetas = memoize(buildSlatMetas);
+ // refs
+ this.dayScrollerRef = M$1();
+ this.timeScrollerRef = M$1();
+ this.scrollState = {}; // updated in-place
+ // Sizing
+ // -----------------------------------------------------------------------------------------------
+ this.handleSlatHeight = (slatHeight) => {
+ if (this._isUnmounting)
+ return;
+ this.slatHeight = slatHeight;
+ if (slatHeight != null) {
+ afterSize(this.applyTimeScroll);
+ }
+ };
+ this.handleTimeScrollRequest = (scrollTime) => {
+ this.scrollState.time = scrollTime;
+ this.scrollState.y = undefined;
+ this.applyTimeScroll();
+ };
+ /*
+ Captures current values
+ */
+ this.handleTimeScrollEnd = (isDevice) => {
+ if (isDevice) {
+ const y = this.timeScrollerRef.current.y;
+ // record, but only if not forPrint, which could give bogus values in the case of
+ // TimeGridLayoutPannable, which kills y-scrolling, but retains x-scrolling,
+ // which reports as a 0 y-scroll.
+ if (!this.props.forPrint) {
+ this.scrollState.y = y;
+ this.scrollState.time = undefined;
+ }
+ }
+ };
+ this.applyTimeScroll = () => {
+ const timeScroller = this.timeScrollerRef.current;
+ const { slatHeight, scrollState } = this;
+ let { y, time } = scrollState;
+ if (y == null &&
+ time &&
+ slatHeight != null &&
+ // Since applyTimeScroll is called by handleSlatHeight, could be called with null during cleanup,
+ // and the timeScroller might not exist
+ timeScroller) {
+ y = computeTimeTopFrac(time, this.props.dateProfile)
+ * (slatHeight * this.currentSlatCnt);
+ if (y) {
+ y++; // overcome top border
+ }
+ scrollState.y = y; // HACK: store raw pixel value
+ }
+ if (y != null) {
+ timeScroller.scrollTo({ y });
+ }
+ };
+ }
+ render() {
+ const { props, context } = this;
+ const { dateProfile } = props;
+ const { options, dateEnv } = context;
+ const { dayMinWidth } = options;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotHeaderInterval, options.slotDuration, dateEnv);
+ this.currentSlatCnt = slatMetas.length;
+ const businessHourSegs = props.forPrint ? [] : props.businessHourSegs;
+ const dateSelectionSegs = props.forPrint ? [] : props.dateSelectionSegs;
+ const eventDrag = props.forPrint ? null : props.eventDrag;
+ const eventResize = props.forPrint ? null : props.eventResize;
+ const businessHourSegsByCol = props.forPrint ? buildEmptySegCols(props.businessHourSegsByCol) : props.businessHourSegsByCol;
+ const dateSelectionSegsByCol = props.forPrint ? buildEmptySegCols(props.dateSelectionSegsByCol) : props.dateSelectionSegsByCol;
+ const eventDragByCol = props.forPrint ? buildEmptyInteractionCols(props.eventDragByCol) : props.eventDragByCol;
+ const eventResizeByCol = props.forPrint ? buildEmptyInteractionCols(props.eventResizeByCol) : props.eventResizeByCol;
+ const commonLayoutProps = {
+ dateProfile: dateProfile,
+ nowDate: props.nowDate,
+ todayRange: props.todayRange,
+ cells: props.cells,
+ slatMetas,
+ forPrint: props.forPrint,
+ isHitComboAllowed: props.isHitComboAllowed,
+ // header content
+ headerTiers: props.headerTiers,
+ // all-day content
+ fgEventSegs: props.fgEventSegs,
+ bgEventSegs: props.bgEventSegs,
+ businessHourSegs,
+ dateSelectionSegs,
+ eventDrag,
+ eventResize,
+ ...getAllDayMaxEventProps(options),
+ // timed content
+ fgEventSegsByCol: props.fgEventSegsByCol,
+ bgEventSegsByCol: props.bgEventSegsByCol,
+ businessHourSegsByCol,
+ nowIndicatorSegsByCol: props.nowIndicatorSegsByCol,
+ dateSelectionSegsByCol,
+ eventDragByCol,
+ eventResizeByCol,
+ // universal content
+ eventSelection: props.eventSelection,
+ // refs
+ timeScrollerRef: this.timeScrollerRef,
+ timeScrollState: this.scrollState,
+ slatHeightRef: this.handleSlatHeight,
+ borderlessX,
+ borderlessBottom,
+ };
+ return (u$1(ViewContainer, { attrs: {
+ role: 'grid',
+ 'aria-colcount': props.cells.length,
+ 'aria-labelledby': props.labelId,
+ 'aria-label': props.labelStr,
+ }, className: joinClassNames(props.className, generateClassName(options.tableClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: 0,
+ }),
+ // we don't do classNames.printRoot/classNames.printHeader here because works poorly with print:
+ // - Firefox >85ish CAN have flexboxes within it, but those cannot do absolute positioning
+ // - Chrome works okay, but abs-positioned events cover the repeated header
+ // Also, there's weird padding on the last page at bottom of container, which matches
+ // the height of the repeated header
+ // - Safari was never able to do repeated headers in the first place
+ !props.forPrint && classNames.flexCol, classNames.isolate), viewSpec: context.viewSpec, children: dayMinWidth ? (u$1(TimeGridLayoutPannable, { ...commonLayoutProps, dayMinWidth: dayMinWidth, dayScrollerRef: this.dayScrollerRef })) : (u$1(TimeGridLayoutNormal, { ...commonLayoutProps })) }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.resetScroll();
+ this.context.emitter.on('_timeScrollRequest', this.handleTimeScrollRequest);
+ const timeScroller = this.timeScrollerRef.current;
+ if (timeScroller) {
+ timeScroller.addScrollEndListener(this.handleTimeScrollEnd);
+ }
+ }
+ componentDidUpdate(prevProps) {
+ if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
+ this.resetScroll();
+ }
+ else if (prevProps.forPrint && !this.props.forPrint) {
+ // returning from print
+ // reapply scrolling because scroll-divs were probably restored
+ this.applyTimeScroll();
+ }
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.context.emitter.off('_timeScrollRequest', this.handleTimeScrollRequest);
+ const timeScroller = this.timeScrollerRef.current;
+ if (timeScroller) {
+ timeScroller.removeScrollEndListener(this.handleTimeScrollEnd);
+ }
+ }
+ // Scrolling
+ // -----------------------------------------------------------------------------------------------
+ resetScroll() {
+ this.handleTimeScrollRequest(this.context.options.scrollTime);
+ // also resets day scroll
+ const dayScroller = this.dayScrollerRef.current;
+ if (dayScroller) {
+ dayScroller.scrollTo({ x: 0 });
+ }
+ }
+ }
+ // Utils
+ // -----------------------------------------------------------------------------------------------
+ const AUTO_ALL_DAY_MAX_EVENT_ROWS = 5;
+ function getAllDayMaxEventProps(options) {
+ let { dayMaxEvents, dayMaxEventRows } = options;
+ if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto?
+ dayMaxEvents = undefined;
+ dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS; // make sure "auto" goes to a real number
+ }
+ return { dayMaxEvents, dayMaxEventRows };
+ }
+
+ /*
+ An abstraction for a dragging interaction originating on an event.
+ Does higher-level things than PointerDragger, such as possibly:
+ - a "mirror" that moves with the pointer
+ - a minimum number of pixels or other criteria for a true drag to begin
+
+ subclasses must emit:
+ - pointerdown
+ - dragstart
+ - dragmove
+ - pointerup
+ - dragend
+ */
+ class ElementDragging {
+ constructor(el, selector) {
+ this.emitter = new Emitter();
+ }
+ destroy() {
+ }
+ setMirrorIsVisible(bool) {
+ // optional if subclass doesn't want to support a mirror
+ }
+ setMirrorNeedsRevert(bool) {
+ // optional if subclass doesn't want to support a mirror
+ }
+ setAutoScrollEnabled(bool) {
+ // optional
+ }
+ }
+
+ // TODO: get rid of this in favor of options system,
+ // tho it's really easy to access this globally rather than pass thru options.
+ const config = {};
+
+ // high-level segmenting-aware tester functions
+ // ------------------------------------------------------------------------------------------------------------------------
+ function isInteractionValid(interaction, dateProfile, context) {
+ let { instances } = interaction.mutatedEvents;
+ for (let instanceId in instances) {
+ if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
+ return false;
+ }
+ }
+ return isNewPropsValid({ eventDrag: interaction }, context); // HACK: the eventDrag props is used for ALL interactions
+ }
+ function isDateSelectionValid(dateSelection, dateProfile, context) {
+ if (!rangeContainsRange(dateProfile.validRange, dateSelection.range)) {
+ return false;
+ }
+ return isNewPropsValid({ dateSelection }, context);
+ }
+ function isNewPropsValid(newProps, context) {
+ let calendarState = context.getCurrentData();
+ let props = {
+ businessHours: calendarState.businessHours,
+ dateSelection: '',
+ eventStore: calendarState.eventStore,
+ eventUiBases: calendarState.eventUiBases,
+ eventSelection: '',
+ eventDrag: null,
+ eventResize: null,
+ ...newProps,
+ };
+ return (context.pluginHooks.isPropsValid || isPropsValid)(props, context);
+ }
+ function isPropsValid(state, context, dateSpanMeta = {}, filterConfig) {
+ if (state.eventDrag && !isInteractionPropsValid(state, context, dateSpanMeta, filterConfig)) {
+ return false;
+ }
+ if (state.dateSelection && !isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig)) {
+ return false;
+ }
+ return true;
+ }
+ // Moving Event Validation
+ // ------------------------------------------------------------------------------------------------------------------------
+ function isInteractionPropsValid(state, context, dateSpanMeta, filterConfig) {
+ let currentState = context.getCurrentData();
+ let interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions
+ let subjectEventStore = interaction.mutatedEvents;
+ let subjectDefs = subjectEventStore.defs;
+ let subjectInstances = subjectEventStore.instances;
+ let subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ?
+ state.eventUiBases :
+ { '': currentState.selectionConfig });
+ if (filterConfig) {
+ subjectConfigs = mapHash(subjectConfigs, filterConfig);
+ }
+ // exclude the subject events. TODO: exclude defs too?
+ let otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances);
+ let otherDefs = otherEventStore.defs;
+ let otherInstances = otherEventStore.instances;
+ let otherConfigs = compileEventUis(otherDefs, state.eventUiBases);
+ for (let subjectInstanceId in subjectInstances) {
+ let subjectInstance = subjectInstances[subjectInstanceId];
+ let subjectRange = subjectInstance.range;
+ let subjectConfig = subjectConfigs[subjectInstance.defId];
+ let subjectDef = subjectDefs[subjectInstance.defId];
+ // constraint
+ if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, context)) {
+ return false;
+ }
+ // overlap
+ let { eventOverlap } = context.options;
+ let eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null;
+ for (let otherInstanceId in otherInstances) {
+ let otherInstance = otherInstances[otherInstanceId];
+ // intersect! evaluate
+ if (rangesIntersect(subjectRange, otherInstance.range)) {
+ let otherOverlap = otherConfigs[otherInstance.defId].overlap;
+ // consider the other event's overlap. only do this if the subject event is a "real" event
+ if (otherOverlap === false && interaction.isEvent) {
+ return false;
+ }
+ if (subjectConfig.overlap === false) {
+ return false;
+ }
+ if (eventOverlapFunc && !eventOverlapFunc(new EventImpl(context, otherDefs[otherInstance.defId], otherInstance), // still event
+ new EventImpl(context, subjectDef, subjectInstance))) {
+ return false;
+ }
+ }
+ }
+ // allow (a function)
+ let calendarEventStore = currentState.eventStore; // need global-to-calendar, not local to component (splittable)state
+ for (let subjectAllow of subjectConfig.allows) {
+ let subjectDateSpan = {
+ ...dateSpanMeta,
+ range: subjectInstance.range,
+ allDay: subjectDef.allDay,
+ };
+ let origDef = calendarEventStore.defs[subjectDef.defId];
+ let origInstance = calendarEventStore.instances[subjectInstanceId];
+ let eventApi;
+ if (origDef) { // was previously in the calendar
+ eventApi = new EventImpl(context, origDef, origInstance);
+ }
+ else { // was an external event
+ eventApi = new EventImpl(context, subjectDef); // no instance, because had no dates
+ }
+ if (!subjectAllow(buildDateSpanApiWithContext(subjectDateSpan, context), eventApi)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ // Date Selection Validation
+ // ------------------------------------------------------------------------------------------------------------------------
+ function isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig) {
+ let relevantEventStore = state.eventStore;
+ let relevantDefs = relevantEventStore.defs;
+ let relevantInstances = relevantEventStore.instances;
+ let selection = state.dateSelection;
+ let selectionRange = selection.range;
+ let { selectionConfig } = context.getCurrentData();
+ if (filterConfig) {
+ selectionConfig = filterConfig(selectionConfig);
+ }
+ // constraint
+ if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, context)) {
+ return false;
+ }
+ // overlap
+ let { selectOverlap } = context.options;
+ let selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null;
+ for (let relevantInstanceId in relevantInstances) {
+ let relevantInstance = relevantInstances[relevantInstanceId];
+ // intersect! evaluate
+ if (rangesIntersect(selectionRange, relevantInstance.range)) {
+ if (selectionConfig.overlap === false) {
+ return false;
+ }
+ if (selectOverlapFunc && !selectOverlapFunc(new EventImpl(context, relevantDefs[relevantInstance.defId], relevantInstance), null)) {
+ return false;
+ }
+ }
+ }
+ // allow (a function)
+ for (let selectionAllow of selectionConfig.allows) {
+ let fullDateSpan = { ...dateSpanMeta, ...selection };
+ if (!selectionAllow(buildDateSpanApiWithContext(fullDateSpan, context), null)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ // Constraint Utils
+ // ------------------------------------------------------------------------------------------------------------------------
+ function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, context) {
+ for (let constraint of constraints) {
+ if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, context), subjectRange)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours
+ otherEventStore, // for if constraint is an even group ID
+ businessHoursUnexpanded, // for if constraint is 'businessHours'
+ context) {
+ if (constraint === 'businessHours') {
+ return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, context));
+ }
+ if (typeof constraint === 'string') { // an group ID
+ return eventStoreToRanges(filterEventStoreDefs(otherEventStore, (eventDef) => eventDef.groupId === constraint));
+ }
+ if (typeof constraint === 'object' && constraint) { // non-null object
+ return eventStoreToRanges(expandRecurring(constraint, subjectRange, context));
+ }
+ return []; // if it's false
+ }
+ // TODO: move to event-store file?
+ function eventStoreToRanges(eventStore) {
+ let { instances } = eventStore;
+ let ranges = [];
+ for (let instanceId in instances) {
+ ranges.push(instances[instanceId].range);
+ }
+ return ranges;
+ }
+ // TODO: move to geom file?
+ function anyRangesContainRange(outerRanges, innerRange) {
+ for (let outerRange of outerRanges) {
+ if (rangeContainsRange(outerRange, innerRange)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function debounce(fn, ms) {
+ let timeoutStarted;
+ let timeoutAdded;
+ let timeoutId; // thruthiness indicates whether active timeout
+ function runWithTimeout(timeout) {
+ timeoutStarted = Date.now();
+ timeoutAdded = 0;
+ timeoutId = setTimeout(() => {
+ if (timeoutAdded) {
+ runWithTimeout(timeoutAdded);
+ }
+ else {
+ timeoutId = undefined;
+ fn();
+ }
+ }, timeout);
+ }
+ function request() {
+ if (timeoutId) {
+ timeoutAdded = Date.now() - timeoutStarted;
+ }
+ else {
+ runWithTimeout(ms);
+ }
+ }
+ function cancel() {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ timeoutId = undefined;
+ }
+ }
+ return [request, cancel];
+ }
+
+ class Store {
+ constructor() {
+ this.handlers = [];
+ }
+ set(value) {
+ this.currentValue = value;
+ for (let handler of this.handlers) {
+ handler(value);
+ }
+ }
+ subscribe(handler) {
+ this.handlers.push(handler);
+ if (this.currentValue !== undefined) {
+ handler(this.currentValue);
+ }
+ }
+ }
+
+ /*
+ Subscribers will get a LIST of CustomRenderings
+ */
+ class CustomRenderingStore extends Store {
+ constructor() {
+ super(...arguments);
+ this.map = new Map();
+ }
+ // for consistent order
+ handle(customRendering) {
+ const { map } = this;
+ let updated = false;
+ if (customRendering.isActive) {
+ map.set(customRendering.id, customRendering);
+ updated = true;
+ }
+ else if (map.has(customRendering.id)) {
+ map.delete(customRendering.id);
+ updated = true;
+ }
+ if (updated) {
+ this.set(map);
+ }
+ }
+ }
+
+ var protectedApi = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ CustomRenderingStore: CustomRenderingStore,
+ debounce: debounce,
+ EventImpl: EventImpl,
+ buildEventRangeKey: buildEventRangeKey,
+ combineEventUis: combineEventUis,
+ compareByFieldSpecs: compareByFieldSpecs,
+ computeViewBorderless: computeViewBorderless,
+ computeVisibleDayRange: computeVisibleDayRange,
+ createEventUi: createEventUi,
+ createFormatter: createFormatter,
+ filterHash: filterHash,
+ flexibleCompare: flexibleCompare,
+ getEventKey: getEventKey,
+ getEventRangeMeta: getEventRangeMeta,
+ guid: guid,
+ identity: identity,
+ isArraysEqual: isArraysEqual,
+ isPropsEqualShallow: isPropsEqualShallow,
+ mapHash: mapHash,
+ mergeEventStores: mergeEventStores,
+ parseFieldSpecs: parseFieldSpecs,
+ refineClassName: refineClassName,
+ refineClassNameGenerator: refineClassNameGenerator,
+ refineProps: refineProps,
+ removeExact: removeExact,
+ sortEventSegs: sortEventSegs,
+ warn: warn,
+ CalendarApiImpl: CalendarApiImpl,
+ CalendarDataManager: CalendarDataManager,
+ CalendarInner: CalendarInner,
+ CalendarMediaRoot: CalendarMediaRoot,
+ computeRootClassName: computeRootClassName,
+ parseBusinessHours: parseBusinessHours,
+ BaseComponent: BaseComponent,
+ ContentContainer: ContentContainer,
+ RenderId: RenderId,
+ generateClassName: generateClassName,
+ getFooterScrollbarSticky: getFooterScrollbarSticky,
+ getIsHeightAuto: getIsHeightAuto,
+ getTableHeaderSticky: getTableHeaderSticky,
+ memoize: memoize,
+ memoizeObjArg: memoizeObjArg,
+ setRef: setRef,
+ computeEdges: computeEdges,
+ computeInnerRect: computeInnerRect,
+ getRectCenter: getRectCenter,
+ joinFuncishClassNames: joinFuncishClassNames,
+ mergeCalendarOptions: mergeCalendarOptions,
+ mergeContentInjectors: mergeContentInjectors,
+ mergeLifecycleCallbacks: mergeLifecycleCallbacks,
+ mergeViewOptionsMap: mergeViewOptionsMap,
+ applyStyleProp: applyStyleProp,
+ computeElIsRtl: computeElIsRtl,
+ AllDaySplitter: AllDaySplitter,
+ DayTimeColsSlicer: DayTimeColsSlicer,
+ NowIndicatorDot: NowIndicatorDot,
+ NowIndicatorHeaderContainer: NowIndicatorHeaderContainer,
+ NowIndicatorLineContainer: NowIndicatorLineContainer,
+ Splitter: Splitter,
+ TimeGridLayout: TimeGridLayout,
+ buildDayRanges: buildDayRanges,
+ buildTimeColsModel: buildTimeColsModel,
+ organizeSegsByCol: organizeSegsByCol,
+ splitInteractionByCol: splitInteractionByCol,
+ DateComponent: DateComponent,
+ DelayedRunner: DelayedRunner,
+ NowTimer: NowTimer,
+ Scroller: Scroller,
+ StandardEvent: StandardEvent,
+ ViewContainer: ViewContainer,
+ afterSize: afterSize,
+ buildNavLinkAttrs: buildNavLinkAttrs,
+ getDateMeta: getDateMeta,
+ watchHeight: watchHeight,
+ watchSize: watchSize,
+ watchWidth: watchWidth,
+ requestJson: requestJson,
+ unpromisify: unpromisify,
+ Emitter: Emitter,
+ DateProfileGenerator: DateProfileGenerator,
+ computeMajorUnit: computeMajorUnit,
+ isMajorUnit: isMajorUnit,
+ BgEvent: BgEvent,
+ DayTableModel: DayTableModel,
+ DayTableSlicer: DayTableSlicer,
+ MoreLinkContainer: MoreLinkContainer,
+ RefMap: RefMap,
+ Ruler: Ruler,
+ SegHierarchy: SegHierarchy,
+ Slicer: Slicer,
+ buildDateDataConfigs: buildDateDataConfigs,
+ buildDateRenderConfig: buildDateRenderConfig,
+ buildDateRowConfig: buildDateRowConfig,
+ buildDayTableModel: buildDayTableModel,
+ createDayHeaderFormatter: createDayHeaderFormatter,
+ groupIntersectingSegs: groupIntersectingSegs,
+ renderFill: renderFill,
+ ElementDragging: ElementDragging,
+ config: config,
+ isPropsValid: isPropsValid,
+ DayGridLayout: DayGridLayout,
+ FooterScrollbar: FooterScrollbar,
+ DateEnv: DateEnv,
+ addDays: addDays,
+ addMs: addMs,
+ asCleanDays: asCleanDays,
+ asRoughMinutes: asRoughMinutes,
+ asRoughMs: asRoughMs,
+ asRoughSeconds: asRoughSeconds,
+ createDuration: createDuration,
+ diffDayAndTime: diffDayAndTime,
+ diffWholeDays: diffWholeDays,
+ diffWholeWeeks: diffWholeWeeks,
+ formatDayString: formatDayString,
+ greatestDurationDenominator: greatestDurationDenominator,
+ intersectRanges: intersectRanges,
+ isInt: isInt,
+ isValidDate: isValidDate,
+ multiplyDuration: multiplyDuration,
+ padStart: padStart,
+ parseMarker: parse,
+ rangeContainsMarker: rangeContainsMarker,
+ rangesEqual: rangesEqual,
+ rangesIntersect: rangesIntersect,
+ startOfDay: startOfDay,
+ wholeDivideDurations: wholeDivideDurations
+ });
+
+ config.touchMouseIgnoreWait = 500;
+ let ignoreMouseDepth = 0;
+ let listenerCnt = 0;
+ let isWindowTouchMoveCancelled = false;
+ /*
+ Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
+ Tracks when the pointer "drags" on a certain element, meaning down+move+up.
+
+ Also, tracks if there was touch-scrolling.
+ Also, can prevent touch-scrolling from happening.
+ Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
+
+ emits:
+ - pointerdown
+ - pointermove
+ - pointerup
+ */
+ class PointerDragging {
+ constructor(containerEl) {
+ this.subjectEl = null;
+ // options that can be directly assigned by caller
+ this.selector = ''; // will cause subjectEl in all emitted events to be this element
+ this.handleSelector = '';
+ this.shouldIgnoreMove = false;
+ this.shouldWatchScroll = true; // for simulating pointermove on scroll
+ // internal states
+ this.isDragging = false;
+ this.isTouchDragging = false;
+ this.wasTouchScroll = false; // HACK public
+ // Mouse
+ // ----------------------------------------------------------------------------------------------------
+ this.handleMouseDown = (ev) => {
+ if (!this.shouldIgnoreMouse() &&
+ isPrimaryMouseButton(ev) &&
+ this.tryStart(ev)) {
+ let pev = this.createEventFromMouse(ev, true);
+ this.emitter.trigger('pointerdown', pev);
+ this.initScrollWatch(pev);
+ if (!this.shouldIgnoreMove) {
+ document.addEventListener('mousemove', this.handleMouseMove);
+ }
+ document.addEventListener('mouseup', this.handleMouseUp);
+ }
+ };
+ this.handleMouseMove = (ev) => {
+ let pev = this.createEventFromMouse(ev);
+ this.recordCoords(pev);
+ this.emitter.trigger('pointermove', pev);
+ };
+ this.handleMouseUp = (ev) => {
+ document.removeEventListener('mousemove', this.handleMouseMove);
+ document.removeEventListener('mouseup', this.handleMouseUp);
+ this.emitter.trigger('pointerup', this.createEventFromMouse(ev));
+ this.cleanup(); // call last so that pointerup has access to props
+ };
+ // Touch
+ // ----------------------------------------------------------------------------------------------------
+ this.handleTouchStart = (ev) => {
+ if (this.tryStart(ev)) {
+ this.isTouchDragging = true;
+ let pev = this.createEventFromTouch(ev, true);
+ this.emitter.trigger('pointerdown', pev);
+ this.initScrollWatch(pev);
+ // unlike mouse, need to attach to target, not document
+ // https://stackoverflow.com/a/45760014
+ let targetEl = ev.target;
+ if (!this.shouldIgnoreMove) {
+ targetEl.addEventListener('touchmove', this.handleTouchMove);
+ }
+ targetEl.addEventListener('touchend', this.handleTouchEnd);
+ targetEl.addEventListener('touchcancel', this.handleTouchEnd); // treat it as a touch end
+ // attach a handler to get called when ANY scroll action happens on the page.
+ // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
+ // http://stackoverflow.com/a/32954565/96342
+ window.addEventListener('scroll', this.handleTouchScroll, true);
+ }
+ };
+ this.handleTouchMove = (ev) => {
+ if (this.isDragging) {
+ let pev = this.createEventFromTouch(ev);
+ this.recordCoords(pev);
+ this.emitter.trigger('pointermove', pev);
+ }
+ };
+ this.handleTouchEnd = (ev) => {
+ if (this.isDragging) { // done to guard against touchend followed by touchcancel
+ let targetEl = ev.target;
+ targetEl.removeEventListener('touchmove', this.handleTouchMove);
+ targetEl.removeEventListener('touchend', this.handleTouchEnd);
+ targetEl.removeEventListener('touchcancel', this.handleTouchEnd);
+ window.removeEventListener('scroll', this.handleTouchScroll, true); // useCaptured=true
+ this.emitter.trigger('pointerup', this.createEventFromTouch(ev));
+ this.cleanup(); // call last so that pointerup has access to props
+ this.isTouchDragging = false;
+ startIgnoringMouse();
+ }
+ };
+ this.handleTouchScroll = () => {
+ this.wasTouchScroll = true;
+ };
+ this.handleScroll = (ev) => {
+ if (!this.shouldIgnoreMove) {
+ let pageX = (window.scrollX - this.prevScrollX) + this.prevPageX;
+ let pageY = (window.scrollY - this.prevScrollY) + this.prevPageY;
+ this.emitter.trigger('pointermove', {
+ origEvent: ev,
+ isTouch: this.isTouchDragging,
+ subjectEl: this.subjectEl,
+ pageX,
+ pageY,
+ deltaX: pageX - this.origPageX,
+ deltaY: pageY - this.origPageY,
+ });
+ }
+ };
+ this.containerEl = containerEl;
+ this.emitter = new Emitter();
+ containerEl.addEventListener('mousedown', this.handleMouseDown);
+ containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
+ listenerCreated();
+ }
+ destroy() {
+ this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
+ this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
+ listenerDestroyed();
+ }
+ cancel() {
+ if (this.isDragging) {
+ this.cleanup();
+ }
+ }
+ tryStart(ev) {
+ let subjectEl = this.querySubjectEl(ev);
+ let downEl = ev.target;
+ if (subjectEl &&
+ (!this.handleSelector || downEl.closest(this.handleSelector))) {
+ this.subjectEl = subjectEl;
+ this.isDragging = true; // do this first so cancelTouchScroll will work
+ this.wasTouchScroll = false;
+ return true;
+ }
+ return false;
+ }
+ cleanup() {
+ isWindowTouchMoveCancelled = false;
+ this.isDragging = false;
+ this.subjectEl = null;
+ // keep wasTouchScroll around for later access
+ this.destroyScrollWatch();
+ }
+ querySubjectEl(ev) {
+ if (this.selector) {
+ return ev.target.closest(this.selector);
+ }
+ return this.containerEl;
+ }
+ shouldIgnoreMouse() {
+ return ignoreMouseDepth || this.isTouchDragging;
+ }
+ // can be called by user of this class, to cancel touch-based scrolling for the current drag
+ cancelTouchScroll() {
+ if (this.isDragging) {
+ isWindowTouchMoveCancelled = true;
+ }
+ }
+ // Scrolling that simulates pointermoves
+ // ----------------------------------------------------------------------------------------------------
+ initScrollWatch(ev) {
+ if (this.shouldWatchScroll) {
+ this.recordCoords(ev);
+ window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
+ }
+ }
+ recordCoords(ev) {
+ if (this.shouldWatchScroll) {
+ this.prevPageX = ev.pageX;
+ this.prevPageY = ev.pageY;
+ this.prevScrollX = window.scrollX;
+ this.prevScrollY = window.scrollY;
+ }
+ }
+ destroyScrollWatch() {
+ if (this.shouldWatchScroll) {
+ window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
+ }
+ }
+ // Event Normalization
+ // ----------------------------------------------------------------------------------------------------
+ createEventFromMouse(ev, isFirst) {
+ let deltaX = 0;
+ let deltaY = 0;
+ // TODO: repeat code
+ if (isFirst) {
+ this.origPageX = ev.pageX;
+ this.origPageY = ev.pageY;
+ }
+ else {
+ deltaX = ev.pageX - this.origPageX;
+ deltaY = ev.pageY - this.origPageY;
+ }
+ return {
+ origEvent: ev,
+ isTouch: false,
+ subjectEl: this.subjectEl,
+ pageX: ev.pageX,
+ pageY: ev.pageY,
+ deltaX,
+ deltaY,
+ };
+ }
+ createEventFromTouch(ev, isFirst) {
+ let touches = ev.touches;
+ let pageX;
+ let pageY;
+ let deltaX = 0;
+ let deltaY = 0;
+ // if touch coords available, prefer,
+ // because FF would give bad ev.pageX ev.pageY
+ if (touches && touches.length) {
+ pageX = touches[0].pageX;
+ pageY = touches[0].pageY;
+ }
+ else {
+ pageX = ev.pageX;
+ pageY = ev.pageY;
+ }
+ // TODO: repeat code
+ if (isFirst) {
+ this.origPageX = pageX;
+ this.origPageY = pageY;
+ }
+ else {
+ deltaX = pageX - this.origPageX;
+ deltaY = pageY - this.origPageY;
+ }
+ return {
+ origEvent: ev,
+ isTouch: true,
+ subjectEl: this.subjectEl,
+ pageX,
+ pageY,
+ deltaX,
+ deltaY,
+ };
+ }
+ }
+ // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
+ function isPrimaryMouseButton(ev) {
+ return ev.button === 0 && !ev.ctrlKey;
+ }
+ // Ignoring fake mouse events generated by touch
+ // ----------------------------------------------------------------------------------------------------
+ function startIgnoringMouse() {
+ ignoreMouseDepth += 1;
+ setTimeout(() => {
+ ignoreMouseDepth -= 1;
+ }, config.touchMouseIgnoreWait);
+ }
+ // We want to attach touchmove as early as possible for Safari
+ // ----------------------------------------------------------------------------------------------------
+ function listenerCreated() {
+ listenerCnt += 1;
+ if (listenerCnt === 1) {
+ window.addEventListener('touchmove', onWindowTouchMove, { passive: false });
+ }
+ }
+ function listenerDestroyed() {
+ listenerCnt -= 1;
+ if (!listenerCnt) {
+ window.removeEventListener('touchmove', onWindowTouchMove, { passive: false });
+ }
+ }
+ function onWindowTouchMove(ev) {
+ if (isWindowTouchMoveCancelled) {
+ ev.preventDefault();
+ }
+ }
+
+ /*
+ An effect in which an element follows the movement of a pointer across the screen.
+ The moving element is a clone of some other element.
+ Must call start + handleMove + stop.
+ */
+ class ElementMirror {
+ constructor() {
+ this.isVisible = false; // must be explicitly enabled
+ this.sourceEl = null;
+ this.mirrorEl = null;
+ this.sourceElRect = null; // screen coords relative to viewport
+ // options that can be set directly by caller
+ this.parentNode = document.body; // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues
+ this.zIndex = 9999;
+ this.revertDuration = 0;
+ this.colorScheme = '';
+ }
+ start(sourceEl, pageX, pageY) {
+ this.sourceEl = sourceEl;
+ this.sourceElRect = this.sourceEl.getBoundingClientRect();
+ this.origScreenX = pageX - window.scrollX;
+ this.origScreenY = pageY - window.scrollY;
+ this.deltaX = 0;
+ this.deltaY = 0;
+ this.updateElPosition();
+ }
+ handleMove(pageX, pageY) {
+ this.deltaX = (pageX - window.scrollX) - this.origScreenX;
+ this.deltaY = (pageY - window.scrollY) - this.origScreenY;
+ this.updateElPosition();
+ }
+ // can be called before start
+ setIsVisible(bool) {
+ if (bool) {
+ if (!this.isVisible) {
+ if (this.mirrorEl) {
+ // important because competes with util.module.css classNames, which are all important
+ // TODO: attach a util className here instead?
+ this.mirrorEl.style.setProperty('display', '', 'important');
+ }
+ this.isVisible = bool; // needs to happen before updateElPosition
+ this.updateElPosition(); // because was not updating the position while invisible
+ }
+ }
+ else if (this.isVisible) {
+ if (this.mirrorEl) {
+ // important because competes with util.module.css classNames, which are all important
+ // TODO: attach a util className here instead?
+ this.mirrorEl.style.setProperty('display', 'none', 'important');
+ }
+ this.isVisible = bool;
+ }
+ }
+ // always async
+ stop(needsRevertAnimation, callback) {
+ let done = () => {
+ this.cleanup();
+ callback();
+ };
+ if (needsRevertAnimation &&
+ this.mirrorEl &&
+ this.isVisible &&
+ this.revertDuration && // if 0, transition won't work
+ (this.deltaX || this.deltaY) // if same coords, transition won't work
+ ) {
+ this.doRevertAnimation(done, this.revertDuration);
+ }
+ else {
+ setTimeout(done, 0);
+ }
+ }
+ doRevertAnimation(callback, revertDuration) {
+ let mirrorEl = this.mirrorEl;
+ let finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened
+ mirrorEl.style.transition =
+ 'top ' + revertDuration + 'ms,' +
+ 'left ' + revertDuration + 'ms';
+ applyStyle(mirrorEl, {
+ left: finalSourceElRect.left,
+ top: finalSourceElRect.top,
+ });
+ whenTransitionDone(mirrorEl, () => {
+ mirrorEl.style.transition = '';
+ callback();
+ });
+ }
+ cleanup() {
+ if (this.mirrorEl) {
+ this.mirrorEl.remove();
+ this.mirrorEl = null;
+ }
+ this.sourceEl = null;
+ }
+ updateElPosition() {
+ if (this.sourceEl && this.isVisible) {
+ applyStyle(this.getMirrorEl(), {
+ left: this.sourceElRect.left + this.deltaX,
+ top: this.sourceElRect.top + this.deltaY,
+ });
+ }
+ }
+ getMirrorEl() {
+ let sourceElRect = this.sourceElRect;
+ let mirrorEl = this.mirrorEl;
+ if (!mirrorEl) {
+ mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true
+ // we don't want long taps or any mouse interaction causing selection/menus.
+ // would use preventSelection(), but that prevents selectstart, causing problems.
+ // TODO: make className for this?
+ mirrorEl.style.userSelect = 'none';
+ mirrorEl.style.webkitUserSelect = 'none';
+ mirrorEl.style.pointerEvents = 'none';
+ if (this.colorScheme) {
+ mirrorEl.setAttribute('data-color-scheme', this.colorScheme);
+ }
+ mirrorEl.classList.add(classNames.borderBoxRoot);
+ applyStyle(mirrorEl, {
+ position: 'fixed',
+ zIndex: this.zIndex,
+ visibility: '', // in case original element was hidden by the drag effect
+ width: sourceElRect.right - sourceElRect.left, // explicit height in case there was a 'right' value
+ height: sourceElRect.bottom - sourceElRect.top, // explicit width in case there was a 'bottom' value
+ right: 'auto', // erase and set width instead
+ bottom: 'auto', // erase and set height instead
+ margin: 0,
+ });
+ this.parentNode.appendChild(mirrorEl);
+ }
+ return mirrorEl;
+ }
+ }
+
+ /* eslint max-classes-per-file: "off" */
+ /*
+ An object for getting/setting scroll-related information for an element.
+ Internally, this is done very differently for window versus DOM element,
+ so this object serves as a common interface.
+ */
+ class ScrollController {
+ getMaxScrollTop() {
+ return this.getScrollHeight() - this.getClientHeight();
+ }
+ getMaxScrollLeft() {
+ return this.getScrollWidth() - this.getClientWidth();
+ }
+ canScrollVertically() {
+ return this.getMaxScrollTop() > 0;
+ }
+ canScrollHorizontally() {
+ return this.getMaxScrollLeft() > 0;
+ }
+ canScrollUp() {
+ return this.getScrollTop() > 0;
+ }
+ canScrollDown() {
+ return this.getScrollTop() < this.getMaxScrollTop();
+ }
+ canScrollLeft() {
+ return this.getScrollLeft() > 0;
+ }
+ canScrollRight() {
+ return this.getScrollLeft() < this.getMaxScrollLeft();
+ }
+ }
+ class ElementScrollController extends ScrollController {
+ constructor(el) {
+ super();
+ this.el = el;
+ }
+ getScrollTop() {
+ return this.el.scrollTop;
+ }
+ getScrollLeft() {
+ return this.el.scrollLeft;
+ }
+ setScrollTop(top) {
+ this.el.scrollTop = top;
+ }
+ setScrollLeft(left) {
+ this.el.scrollLeft = left;
+ }
+ getScrollWidth() {
+ return this.el.scrollWidth;
+ }
+ getScrollHeight() {
+ return this.el.scrollHeight;
+ }
+ getClientHeight() {
+ return this.el.clientHeight;
+ }
+ getClientWidth() {
+ return this.el.clientWidth;
+ }
+ }
+ class WindowScrollController extends ScrollController {
+ getScrollTop() {
+ return window.scrollY;
+ }
+ getScrollLeft() {
+ return window.scrollX;
+ }
+ setScrollTop(n) {
+ window.scroll(window.scrollX, n);
+ }
+ setScrollLeft(n) {
+ window.scroll(n, window.scrollY);
+ }
+ getScrollWidth() {
+ return document.documentElement.scrollWidth;
+ }
+ getScrollHeight() {
+ return document.documentElement.scrollHeight;
+ }
+ getClientHeight() {
+ return document.documentElement.clientHeight;
+ }
+ getClientWidth() {
+ return document.documentElement.clientWidth;
+ }
+ }
+
+ /*
+ Is a cache for a given element's scroll information (all the info that ScrollController stores)
+ in addition the "client rectangle" of the element.. the area within the scrollbars.
+
+ The cache can be in one of two modes:
+ - doesListening:false - ignores when the container is scrolled by someone else
+ - doesListening:true - watch for scrolling and update the cache
+ */
+ class ScrollGeomCache extends ScrollController {
+ constructor(scrollController, doesListening) {
+ super();
+ this.handleScroll = () => {
+ this.scrollTop = this.scrollController.getScrollTop();
+ this.scrollLeft = this.scrollController.getScrollLeft();
+ this.handleScrollChange();
+ };
+ this.scrollController = scrollController;
+ this.doesListening = doesListening;
+ this.scrollTop = this.origScrollTop = scrollController.getScrollTop();
+ this.scrollLeft = this.origScrollLeft = scrollController.getScrollLeft();
+ this.scrollWidth = scrollController.getScrollWidth();
+ this.scrollHeight = scrollController.getScrollHeight();
+ this.clientWidth = scrollController.getClientWidth();
+ this.clientHeight = scrollController.getClientHeight();
+ this.clientRect = this.computeClientRect(); // do last in case it needs cached values
+ if (this.doesListening) {
+ this.getEventTarget().addEventListener('scroll', this.handleScroll);
+ }
+ }
+ destroy() {
+ if (this.doesListening) {
+ this.getEventTarget().removeEventListener('scroll', this.handleScroll);
+ }
+ }
+ getScrollTop() {
+ return this.scrollTop;
+ }
+ getScrollLeft() {
+ return this.scrollLeft;
+ }
+ setScrollTop(top) {
+ this.scrollController.setScrollTop(top);
+ if (!this.doesListening) {
+ // we are not relying on the element to normalize out-of-bounds scroll values
+ // so we need to sanitize ourselves
+ this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0);
+ this.handleScrollChange();
+ }
+ }
+ setScrollLeft(top) {
+ this.scrollController.setScrollLeft(top);
+ if (!this.doesListening) {
+ // we are not relying on the element to normalize out-of-bounds scroll values
+ // so we need to sanitize ourselves
+ this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0);
+ this.handleScrollChange();
+ }
+ }
+ getClientWidth() {
+ return this.clientWidth;
+ }
+ getClientHeight() {
+ return this.clientHeight;
+ }
+ getScrollWidth() {
+ return this.scrollWidth;
+ }
+ getScrollHeight() {
+ return this.scrollHeight;
+ }
+ handleScrollChange() {
+ }
+ }
+
+ class ElementScrollGeomCache extends ScrollGeomCache {
+ constructor(el, doesListening) {
+ super(new ElementScrollController(el), doesListening);
+ }
+ getEventTarget() {
+ return this.scrollController.el;
+ }
+ computeClientRect() {
+ return computeInnerRect(this.scrollController.el);
+ }
+ }
+
+ class WindowScrollGeomCache extends ScrollGeomCache {
+ constructor(doesListening) {
+ super(new WindowScrollController(), doesListening);
+ }
+ getEventTarget() {
+ return window;
+ }
+ computeClientRect() {
+ return {
+ left: this.scrollLeft,
+ right: this.scrollLeft + this.clientWidth,
+ top: this.scrollTop,
+ bottom: this.scrollTop + this.clientHeight,
+ };
+ }
+ // the window is the only scroll object that changes it's rectangle relative
+ // to the document's topleft as it scrolls
+ handleScrollChange() {
+ this.clientRect = this.computeClientRect();
+ }
+ }
+
+ // If available we are using native "performance" API instead of "Date"
+ // Read more about it on MDN:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Performance
+ const getTime = typeof performance === 'function' ? performance.now : Date.now;
+ /*
+ For a pointer interaction, automatically scrolls certain scroll containers when the pointer
+ approaches the edge.
+
+ The caller must call start + handleMove + stop.
+ */
+ class AutoScroller {
+ constructor() {
+ // options that can be set by caller
+ this.isEnabled = true;
+ this.scrollQuery = [window, `.${classNames.internalScroller}`];
+ this.edgeThreshold = 50; // pixels
+ this.maxVelocity = 300; // pixels per second
+ // internal state
+ this.pointerScreenX = null;
+ this.pointerScreenY = null;
+ this.isAnimating = false;
+ this.scrollCaches = null;
+ // protect against the initial pointerdown being too close to an edge and starting the scroll
+ this.everMovedUp = false;
+ this.everMovedDown = false;
+ this.everMovedLeft = false;
+ this.everMovedRight = false;
+ this.animate = () => {
+ if (this.isAnimating) { // wasn't cancelled between animation calls
+ let edge = this.computeBestEdge(this.pointerScreenX + window.scrollX, this.pointerScreenY + window.scrollY);
+ if (edge) {
+ let now = getTime();
+ this.handleSide(edge, (now - this.msSinceRequest) / 1000);
+ this.requestAnimation(now);
+ }
+ else {
+ this.isAnimating = false; // will stop animation
+ }
+ }
+ };
+ }
+ start(pageX, pageY, scrollStartEl) {
+ if (this.isEnabled) {
+ this.scrollCaches = this.buildCaches(scrollStartEl);
+ this.pointerScreenX = null;
+ this.pointerScreenY = null;
+ this.everMovedUp = false;
+ this.everMovedDown = false;
+ this.everMovedLeft = false;
+ this.everMovedRight = false;
+ this.handleMove(pageX, pageY);
+ }
+ }
+ handleMove(pageX, pageY) {
+ if (this.isEnabled) {
+ let pointerScreenX = pageX - window.scrollX;
+ let pointerScreenY = pageY - window.scrollY;
+ let yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY;
+ let xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX;
+ if (yDelta < 0) {
+ this.everMovedUp = true;
+ }
+ else if (yDelta > 0) {
+ this.everMovedDown = true;
+ }
+ if (xDelta < 0) {
+ this.everMovedLeft = true;
+ }
+ else if (xDelta > 0) {
+ this.everMovedRight = true;
+ }
+ this.pointerScreenX = pointerScreenX;
+ this.pointerScreenY = pointerScreenY;
+ if (!this.isAnimating) {
+ this.isAnimating = true;
+ this.requestAnimation(getTime());
+ }
+ }
+ }
+ stop() {
+ if (this.isEnabled) {
+ this.isAnimating = false; // will stop animation
+ for (let scrollCache of this.scrollCaches) {
+ scrollCache.destroy();
+ }
+ this.scrollCaches = null;
+ }
+ }
+ requestAnimation(now) {
+ this.msSinceRequest = now;
+ requestAnimationFrame(this.animate);
+ }
+ handleSide(edge, seconds) {
+ let { scrollCache } = edge;
+ let { edgeThreshold } = this;
+ let invDistance = edgeThreshold - edge.distance;
+ let velocity = // the closer to the edge, the faster we scroll
+ ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic
+ this.maxVelocity * seconds;
+ let sign = 1;
+ switch (edge.name) {
+ case 'left':
+ sign = -1;
+ // falls through
+ case 'right':
+ scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign);
+ break;
+ case 'top':
+ sign = -1;
+ // falls through
+ case 'bottom':
+ scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign);
+ break;
+ }
+ }
+ // left/top are relative to document topleft
+ computeBestEdge(left, top) {
+ let { edgeThreshold } = this;
+ let bestSide = null;
+ let scrollCaches = this.scrollCaches || [];
+ for (let scrollCache of scrollCaches) {
+ let rect = scrollCache.clientRect;
+ let leftDist = left - rect.left;
+ let rightDist = rect.right - left;
+ let topDist = top - rect.top;
+ let bottomDist = rect.bottom - top;
+ // completely within the rect?
+ if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
+ if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
+ (!bestSide || bestSide.distance > topDist)) {
+ bestSide = { scrollCache, name: 'top', distance: topDist };
+ }
+ if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
+ (!bestSide || bestSide.distance > bottomDist)) {
+ bestSide = { scrollCache, name: 'bottom', distance: bottomDist };
+ }
+ /*
+ TODO: fix broken RTL scrolling. canScrollLeft always returning false
+ https://github.com/fullcalendar/fullcalendar/issues/4837
+ */
+ if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
+ (!bestSide || bestSide.distance > leftDist)) {
+ bestSide = { scrollCache, name: 'left', distance: leftDist };
+ }
+ if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
+ (!bestSide || bestSide.distance > rightDist)) {
+ bestSide = { scrollCache, name: 'right', distance: rightDist };
+ }
+ }
+ }
+ return bestSide;
+ }
+ buildCaches(scrollStartEl) {
+ return this.queryScrollEls(scrollStartEl).map((el) => {
+ if (el === window) {
+ return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls
+ }
+ return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls
+ });
+ }
+ queryScrollEls(scrollStartEl) {
+ let els = [];
+ for (let query of this.scrollQuery) {
+ if (typeof query === 'object') {
+ els.push(query);
+ }
+ else {
+ /*
+ TODO: in the future, always have auto-scroll happen on element where current Hit came from
+ Ticket: https://github.com/fullcalendar/fullcalendar/issues/4593
+ */
+ els.push(...Array.prototype.slice.call(scrollStartEl.getRootNode().querySelectorAll(query)));
+ }
+ }
+ return els;
+ }
+ }
+
+ /*
+ Monitors dragging on an element. Has a number of high-level features:
+ - minimum distance required before dragging
+ - minimum wait time ("delay") before dragging
+ - a mirror element that follows the pointer
+ */
+ class FeaturefulElementDragging extends ElementDragging {
+ constructor(containerEl, selector) {
+ super(containerEl);
+ this.containerEl = containerEl;
+ // options that can be directly set by caller
+ // the caller can also set the PointerDragging's options as well
+ this.delay = null;
+ this.minDistance = 0;
+ this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
+ this.mirrorNeedsRevert = false;
+ this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
+ this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
+ this.isDelayEnded = false;
+ this.isDistanceSurpassed = false;
+ this.delayTimeoutId = null;
+ this.onPointerDown = (ev) => {
+ if (!this.isDragging) { // so new drag doesn't happen while revert animation is going
+ this.isInteracting = true;
+ this.isDelayEnded = false;
+ this.isDistanceSurpassed = false;
+ this.emitter.trigger('pointerdown', ev);
+ if (this.isInteracting) { // not cancelled?
+ preventSelection(document.body);
+ preventContextMenu(document.body);
+ // prevent links from being visited if there's an eventual drag.
+ // also prevents selection in older browsers (maybe?).
+ // not necessary for touch, besides, browser would complain about passiveness.
+ if (!ev.isTouch) {
+ ev.origEvent.preventDefault();
+ }
+ // actions related to initiating dragstart+dragmove+dragend...
+ this.mirror.setIsVisible(false); // reset. caller must set-visible
+ this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down
+ this.startDelay(ev);
+ if (!this.minDistance) {
+ this.handleDistanceSurpassed(ev);
+ }
+ }
+ }
+ };
+ this.onPointerMove = (ev) => {
+ if (this.isInteracting) {
+ this.emitter.trigger('pointermove', ev);
+ if (!this.isDistanceSurpassed) {
+ let minDistance = this.minDistance;
+ let distanceSq; // current distance from the origin, squared
+ let { deltaX, deltaY } = ev;
+ distanceSq = deltaX * deltaX + deltaY * deltaY;
+ if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
+ this.handleDistanceSurpassed(ev);
+ }
+ }
+ if (this.isDragging) {
+ // a real pointer move? (not one simulated by scrolling)
+ if (ev.origEvent.type !== 'scroll') {
+ this.mirror.handleMove(ev.pageX, ev.pageY);
+ this.autoScroller.handleMove(ev.pageX, ev.pageY);
+ }
+ this.emitter.trigger('dragmove', ev);
+ }
+ }
+ };
+ this.onPointerUp = (ev) => {
+ if (this.isInteracting) {
+ this.isInteracting = false;
+ allowSelection(document.body);
+ allowContextMenu(document.body);
+ this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert
+ if (this.isDragging) {
+ this.autoScroller.stop();
+ this.tryStopDrag(ev); // which will stop the mirror
+ }
+ if (this.delayTimeoutId) {
+ clearTimeout(this.delayTimeoutId);
+ this.delayTimeoutId = null;
+ }
+ }
+ };
+ let pointer = this.pointer = new PointerDragging(containerEl);
+ pointer.emitter.on('pointerdown', this.onPointerDown);
+ pointer.emitter.on('pointermove', this.onPointerMove);
+ pointer.emitter.on('pointerup', this.onPointerUp);
+ if (selector) {
+ pointer.selector = selector;
+ }
+ this.mirror = new ElementMirror();
+ this.autoScroller = new AutoScroller();
+ }
+ destroy() {
+ this.pointer.destroy();
+ // HACK: simulate a pointer-up to end the current drag
+ // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire)
+ this.onPointerUp({});
+ }
+ startDelay(ev) {
+ if (typeof this.delay === 'number') {
+ this.delayTimeoutId = setTimeout(() => {
+ this.delayTimeoutId = null;
+ this.handleDelayEnd(ev);
+ }, this.delay); // not assignable to number!
+ }
+ else {
+ this.handleDelayEnd(ev);
+ }
+ }
+ handleDelayEnd(ev) {
+ this.isDelayEnded = true;
+ this.tryStartDrag(ev);
+ }
+ handleDistanceSurpassed(ev) {
+ this.isDistanceSurpassed = true;
+ this.tryStartDrag(ev);
+ }
+ tryStartDrag(ev) {
+ if (this.isDelayEnded && this.isDistanceSurpassed) {
+ if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
+ this.isDragging = true;
+ this.mirrorNeedsRevert = false;
+ this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl);
+ this.emitter.trigger('dragstart', ev);
+ if (this.touchScrollAllowed === false) {
+ this.pointer.cancelTouchScroll();
+ }
+ }
+ }
+ }
+ tryStopDrag(ev) {
+ // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
+ // that come from the document to fire beforehand. much more convenient this way.
+ this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev));
+ }
+ stopDrag(ev) {
+ this.isDragging = false;
+ this.emitter.trigger('dragend', ev);
+ }
+ // fill in the implementations...
+ /*
+ Can only be called by pointerdown to prevent drag
+ */
+ cancel() {
+ if (this.isInteracting) {
+ this.isInteracting = false;
+ this.pointer.cancel();
+ }
+ }
+ setMirrorIsVisible(bool) {
+ this.mirror.setIsVisible(bool);
+ }
+ setMirrorNeedsRevert(bool) {
+ this.mirrorNeedsRevert = bool;
+ }
+ setAutoScrollEnabled(bool) {
+ this.autoScroller.isEnabled = bool;
+ }
+ }
+
+ /*
+ When this class is instantiated, it records the offset of an element (relative to the document topleft),
+ and continues to monitor scrolling, updating the cached coordinates if it needs to.
+ Does not access the DOM after instantiation, so highly performant.
+
+ Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
+ and an determine if a given point is inside the combined clipping rectangle.
+ */
+ class OffsetTracker {
+ constructor(el) {
+ this.el = el;
+ this.origRect = computeRect(el);
+ this.isRtl = computeElIsRtl(el);
+ // will work fine for divs that have overflow:hidden
+ this.scrollCaches = getClippingParents(el).map((scrollEl) => new ElementScrollGeomCache(scrollEl, true));
+ }
+ destroy() {
+ for (let scrollCache of this.scrollCaches) {
+ scrollCache.destroy();
+ }
+ }
+ computeLeft() {
+ let left = this.origRect.left;
+ for (let scrollCache of this.scrollCaches) {
+ left += scrollCache.origScrollLeft - scrollCache.getScrollLeft();
+ }
+ return left;
+ }
+ computeTop() {
+ let top = this.origRect.top;
+ for (let scrollCache of this.scrollCaches) {
+ top += scrollCache.origScrollTop - scrollCache.getScrollTop();
+ }
+ return top;
+ }
+ isWithinClipping(pageX, pageY) {
+ let point = { left: pageX, top: pageY };
+ for (let scrollCache of this.scrollCaches) {
+ if (!isIgnoredClipping(scrollCache.getEventTarget()) &&
+ !pointInsideRect(point, scrollCache.clientRect)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ // certain clipping containers should never constrain interactions, like and
+ // https://github.com/fullcalendar/fullcalendar/issues/3615
+ function isIgnoredClipping(node) {
+ let tagName = node.tagName;
+ return tagName === 'HTML' || tagName === 'BODY';
+ }
+
+ /*
+ Tracks movement over multiple droppable areas (aka "hits")
+ that exist in one or more DateComponents.
+ Relies on an existing draggable.
+
+ emits:
+ - pointerdown
+ - dragstart
+ - hitchange - fires initially, even if not over a hit
+ - pointerup
+ - (hitchange - again, to null, if ended over a hit)
+ - dragend
+ */
+ class HitDragging {
+ constructor(dragging, droppableStore) {
+ // options that can be set by caller
+ this.useSubjectCenter = false;
+ this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
+ this.disablePointCheck = false;
+ this.initialHit = null;
+ this.movingHit = null;
+ this.finalHit = null; // won't ever be populated if shouldIgnoreMove
+ this.handlePointerDown = (ev) => {
+ let { dragging } = this;
+ this.initialHit = null;
+ this.movingHit = null;
+ this.finalHit = null;
+ this.prepareHits();
+ this.processFirstCoord(ev);
+ if (this.initialHit || !this.requireInitial) {
+ // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
+ this.emitter.trigger('pointerdown', ev);
+ }
+ else {
+ dragging.cancel();
+ }
+ };
+ this.handleDragStart = (ev) => {
+ this.emitter.trigger('dragstart', ev);
+ this.handleMove(ev, true); // force = fire even if initially null
+ };
+ this.handleDragMove = (ev) => {
+ this.emitter.trigger('dragmove', ev);
+ this.handleMove(ev);
+ };
+ this.handlePointerUp = (ev) => {
+ this.releaseHits();
+ this.emitter.trigger('pointerup', ev);
+ };
+ this.handleDragEnd = (ev) => {
+ if (this.movingHit) {
+ this.emitter.trigger('hitupdate', null, true, ev);
+ }
+ this.finalHit = this.movingHit;
+ this.movingHit = null;
+ this.emitter.trigger('dragend', ev);
+ };
+ this.droppableStore = droppableStore;
+ dragging.emitter.on('pointerdown', this.handlePointerDown);
+ dragging.emitter.on('dragstart', this.handleDragStart);
+ dragging.emitter.on('dragmove', this.handleDragMove);
+ dragging.emitter.on('pointerup', this.handlePointerUp);
+ dragging.emitter.on('dragend', this.handleDragEnd);
+ this.dragging = dragging;
+ this.emitter = new Emitter();
+ }
+ // sets initialHit
+ // sets coordAdjust
+ processFirstCoord(ev) {
+ let origPoint = { left: ev.pageX, top: ev.pageY };
+ let adjustedPoint = origPoint;
+ let subjectEl = ev.subjectEl;
+ let subjectRect;
+ if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot
+ subjectRect = computeRect(subjectEl);
+ adjustedPoint = constrainPoint(adjustedPoint, subjectRect);
+ }
+ let initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top);
+ if (initialHit) {
+ if (this.useSubjectCenter && subjectRect) {
+ let slicedSubjectRect = intersectRects(subjectRect, initialHit.rect);
+ if (slicedSubjectRect) {
+ adjustedPoint = getRectCenter(slicedSubjectRect);
+ }
+ }
+ this.coordAdjust = diffPoints(adjustedPoint, origPoint);
+ }
+ else {
+ this.coordAdjust = { left: 0, top: 0 };
+ }
+ }
+ handleMove(ev, forceHandle) {
+ let hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top);
+ if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
+ this.movingHit = hit;
+ this.emitter.trigger('hitupdate', hit, false, ev);
+ }
+ }
+ prepareHits() {
+ this.offsetTrackers = mapHash(this.droppableStore, (interactionSettings) => {
+ interactionSettings.component.prepareHits();
+ return new OffsetTracker(interactionSettings.el);
+ });
+ }
+ releaseHits() {
+ let { offsetTrackers } = this;
+ for (let id in offsetTrackers) {
+ offsetTrackers[id].destroy();
+ }
+ this.offsetTrackers = {};
+ }
+ queryHitForOffset(offsetLeft, offsetTop) {
+ let { droppableStore, offsetTrackers } = this;
+ let bestHit = null;
+ for (let id in droppableStore) {
+ let component = droppableStore[id].component;
+ let offsetTracker = offsetTrackers[id];
+ if (offsetTracker && // wasn't destroyed mid-drag
+ offsetTracker.isWithinClipping(offsetLeft, offsetTop)) {
+ let originLeft = offsetTracker.computeLeft();
+ let originTop = offsetTracker.computeTop();
+ let positionLeft = offsetLeft - originLeft;
+ let positionTop = offsetTop - originTop;
+ let { origRect } = offsetTracker;
+ let width = origRect.right - origRect.left;
+ let height = origRect.bottom - origRect.top;
+ if (
+ // must be within the element's bounds
+ positionLeft >= 0 && positionLeft < width &&
+ positionTop >= 0 && positionTop < height) {
+ let hit = component.queryHit(offsetTracker.isRtl, positionLeft, positionTop, width, height);
+ if (hit && (
+ // make sure the hit is within activeRange, meaning it's not a dead cell
+ rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range)) &&
+ // Ensure the component we are querying for the hit is accessibly my the pointer
+ // Prevents obscured calendars (ex: under a modal dialog) from accepting hit
+ // https://github.com/fullcalendar/fullcalendar/issues/5026
+ (this.disablePointCheck ||
+ offsetTracker.el.contains(offsetTracker.el.getRootNode().elementFromPoint(
+ // add-back origins to get coordinate relative to top-left of window viewport
+ positionLeft + originLeft - window.scrollX, positionTop + originTop - window.scrollY))) &&
+ (!bestHit || hit.layer > bestHit.layer)) {
+ hit.componentId = id;
+ hit.context = component.context;
+ // TODO: better way to re-orient rectangle
+ hit.rect.left += originLeft;
+ hit.rect.right += originLeft;
+ hit.rect.top += originTop;
+ hit.rect.bottom += originTop;
+ bestHit = hit;
+ }
+ }
+ }
+ }
+ return bestHit;
+ }
+ }
+ function isHitsEqual(hit0, hit1) {
+ if (!hit0 && !hit1) {
+ return true;
+ }
+ if (Boolean(hit0) !== Boolean(hit1)) {
+ return false;
+ }
+ return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
+ }
+
+ function buildDatePointApiWithContext(dateSpan, context) {
+ let props = {};
+ for (let transform of context.pluginHooks.datePointTransforms) {
+ Object.assign(props, transform(dateSpan, context));
+ }
+ Object.assign(props, buildDatePointApi(dateSpan, context.dateEnv));
+ return props;
+ }
+ function buildDatePointApi(span, dateEnv) {
+ return {
+ date: dateEnv.toDate(span.range.start),
+ dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
+ allDay: span.allDay,
+ };
+ }
+
+ /*
+ Monitors when the user clicks on a specific date/time of a component.
+ A pointerdown+pointerup on the same "hit" constitutes a click.
+ */
+ class DateClicking extends Interaction {
+ constructor(settings) {
+ super(settings);
+ this.handlePointerDown = (pev) => {
+ let { dragging } = this;
+ let downEl = pev.origEvent.target;
+ /*
+ If no dateClick, allow text on dates to be text-selectable
+ */
+ const canDateClick = this.component.context.emitter.hasHandlers('dateClick') &&
+ this.component.isValidDateDownEl(downEl);
+ if (!canDateClick) {
+ dragging.cancel();
+ }
+ };
+ // won't even fire if moving was ignored
+ this.handleDragEnd = (ev) => {
+ let { component } = this;
+ let { pointer } = this.dragging;
+ if (!pointer.wasTouchScroll) {
+ let { initialHit, finalHit } = this.hitDragging;
+ if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
+ let { context } = component;
+ let data = {
+ ...buildDatePointApiWithContext(initialHit.dateSpan, context),
+ dayEl: initialHit.getDayEl(),
+ jsEvent: ev.origEvent,
+ view: context.viewApi || context.calendarApi.view,
+ };
+ context.emitter.trigger('dateClick', data);
+ }
+ }
+ };
+ // we DO want to watch pointer moves because otherwise finalHit won't get populated
+ this.dragging = new FeaturefulElementDragging(settings.el);
+ this.dragging.autoScroller.isEnabled = false;
+ let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', this.handlePointerDown);
+ hitDragging.emitter.on('dragend', this.handleDragEnd);
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ }
+
+ /*
+ Tracks when the user selects a portion of time of a component,
+ constituted by a drag over date cells, with a possible delay at the beginning of the drag.
+ */
+ class DateSelecting extends Interaction {
+ constructor(settings) {
+ super(settings);
+ this.dragSelection = null;
+ this.handlePointerDown = (ev) => {
+ let { component, dragging } = this;
+ let { options } = component.context;
+ let canDateSelect = options.selectable &&
+ component.isValidDateDownEl(ev.origEvent.target);
+ if (!canDateSelect) {
+ dragging.cancel();
+ }
+ else {
+ // if touch, require user to hold down
+ dragging.delay = ev.isTouch ? getComponentTouchDelay$1(component) : null;
+ }
+ };
+ this.handleDragStart = (ev) => {
+ this.component.context.calendarApi.unselect(ev); // unselect previous selections
+ };
+ this.handleHitUpdate = (hit, isFinal) => {
+ let { context } = this.component;
+ let dragSelection = null;
+ let isInvalid = false;
+ if (hit) {
+ let initialHit = this.hitDragging.initialHit;
+ let disallowed = hit.componentId === initialHit.componentId
+ && this.isHitComboAllowed
+ && !this.isHitComboAllowed(initialHit, hit);
+ if (!disallowed) {
+ dragSelection = joinHitsIntoSelection(initialHit, hit, context.pluginHooks.dateSelectionTransformers);
+ }
+ if (!dragSelection || !isDateSelectionValid(dragSelection, hit.dateProfile, context)) {
+ isInvalid = true;
+ dragSelection = null;
+ }
+ }
+ if (dragSelection) {
+ context.dispatch({ type: 'SELECT_DATES', selection: dragSelection });
+ }
+ else if (!isFinal) { // only unselect if moved away while dragging
+ context.dispatch({ type: 'UNSELECT_DATES' });
+ }
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging
+ }
+ };
+ this.handlePointerUp = (pev) => {
+ if (this.dragSelection) {
+ // selection is already rendered, so just need to report selection
+ triggerDateSelect(this.dragSelection, pev, this.component.context);
+ this.dragSelection = null;
+ }
+ else {
+ this.component.context.emitter.trigger('_noDateSelect');
+ }
+ };
+ let { component } = settings;
+ let { options } = component.context;
+ let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.touchScrollAllowed = false;
+ dragging.minDistance = options.selectMinDistance || 0;
+ dragging.autoScroller.isEnabled = options.dragScroll;
+ let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
+ hitDragging.emitter.on('pointerup', this.handlePointerUp);
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ }
+ function getComponentTouchDelay$1(component) {
+ let { options } = component.context;
+ let delay = options.selectLongPressDelay;
+ if (delay == null) {
+ delay = options.longPressDelay;
+ }
+ return delay;
+ }
+ function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
+ let dateSpan0 = hit0.dateSpan;
+ let dateSpan1 = hit1.dateSpan;
+ let ms = [
+ dateSpan0.range.start,
+ dateSpan0.range.end,
+ dateSpan1.range.start,
+ dateSpan1.range.end,
+ ];
+ ms.sort(compareNumbers);
+ let props = {};
+ for (let transformer of dateSelectionTransformers) {
+ let res = transformer(hit0, hit1);
+ if (res === false) {
+ return null;
+ }
+ if (res) {
+ Object.assign(props, res);
+ }
+ }
+ props.range = { start: ms[0], end: ms[3] };
+ props.allDay = dateSpan0.allDay;
+ return props;
+ }
+
+ class EventDragging extends Interaction {
+ constructor(settings) {
+ super(settings);
+ // internal state
+ this.subjectEl = null;
+ this.isDragging = false;
+ this.eventRange = null;
+ this.relevantEvents = null; // the events being dragged
+ this.receivingContext = null;
+ this.validMutation = null;
+ this.mutatedRelevantEvents = null;
+ this.handlePointerDown = (ev) => {
+ let origTarget = ev.origEvent.target;
+ let { component, dragging } = this;
+ let { mirror } = dragging;
+ let { options } = component.context;
+ let initialContext = component.context;
+ this.subjectEl = ev.subjectEl;
+ let eventRange = this.eventRange = getElEventRange(ev.subjectEl);
+ let eventInstanceId = eventRange.instance.instanceId;
+ this.relevantEvents = getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId);
+ dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance;
+ dragging.delay =
+ // only do a touch delay if touch and this event hasn't been selected yet
+ (ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
+ getComponentTouchDelay(component) :
+ null;
+ mirror.parentNode = getAppendableRoot(origTarget);
+ mirror.revertDuration = options.dragRevertDuration;
+ mirror.colorScheme = options.colorScheme || '';
+ let isValid = component.isValidSegDownEl(origTarget) &&
+ !origTarget.closest(`.${classNames.internalEventResizer}`); // NOT on a resizer
+ if (!isValid) {
+ dragging.cancel();
+ }
+ else {
+ // disable dragging for elements that are resizable (ie, selectable)
+ // but are not draggable
+ // TODO: merge this with .cancel() ?
+ this.isDragging = ev.subjectEl
+ .classList.contains(classNames.internalEventDraggable);
+ }
+ };
+ this.handleDragStart = (ev) => {
+ let initialContext = this.component.context;
+ let eventRange = this.eventRange;
+ let eventInstanceId = eventRange.instance.instanceId;
+ if (ev.isTouch) {
+ // need to select a different event?
+ if (eventInstanceId !== this.component.props.eventSelection) {
+ initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId });
+ }
+ }
+ else {
+ // if now using mouse, but was previous touch interaction, clear selected event
+ initialContext.dispatch({ type: 'UNSELECT_EVENT' });
+ }
+ if (this.isDragging) {
+ initialContext.calendarApi.unselect(ev); // unselect *date* selection
+ initialContext.emitter.trigger('eventDragStart', {
+ el: this.subjectEl,
+ event: new EventImpl(initialContext, eventRange.def, eventRange.instance),
+ jsEvent: ev.origEvent, // Is this always a mouse event? See #4655
+ view: initialContext.viewApi,
+ });
+ }
+ };
+ this.handleHitUpdate = (hit, isFinal) => {
+ if (!this.isDragging) {
+ return;
+ }
+ let relevantEvents = this.relevantEvents;
+ let initialHit = this.hitDragging.initialHit;
+ let initialContext = this.component.context;
+ // states based on new hit
+ let receivingContext = null;
+ let mutation = null;
+ let mutatedRelevantEvents = null;
+ let isInvalid = false;
+ let interaction = {
+ affectedEvents: relevantEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ };
+ if (hit) {
+ receivingContext = hit.context;
+ let receivingOptions = receivingContext.options;
+ if (initialContext === receivingContext ||
+ (receivingOptions.editable && receivingOptions.droppable)) {
+ mutation = computeEventMutation(initialHit, hit, this.eventRange.instance.range.start, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers);
+ if (mutation) {
+ mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext);
+ interaction.mutatedEvents = mutatedRelevantEvents;
+ if (!isInteractionValid(interaction, hit.dateProfile, receivingContext)) {
+ isInvalid = true;
+ mutation = null;
+ mutatedRelevantEvents = null;
+ interaction.mutatedEvents = createEmptyEventStore();
+ }
+ }
+ }
+ else {
+ receivingContext = null;
+ }
+ }
+ this.displayDrag(receivingContext, interaction);
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ if (initialContext === receivingContext && // TODO: write test for this
+ isHitsEqual(initialHit, hit)) {
+ mutation = null;
+ }
+ this.dragging.setMirrorNeedsRevert(!mutation);
+ // render the mirror if no already-rendered mirror
+ // TODO: wish we could somehow wait for dispatch to guarantee render
+ this.dragging.setMirrorIsVisible(!hit || !this.subjectEl.getRootNode().querySelector(`.${classNames.internalEventMirror}`));
+ // assign states based on new hit
+ this.receivingContext = receivingContext;
+ this.validMutation = mutation;
+ this.mutatedRelevantEvents = mutatedRelevantEvents;
+ }
+ };
+ this.handlePointerUp = () => {
+ if (!this.isDragging) {
+ this.cleanup(); // because handleDragEnd won't fire
+ }
+ };
+ this.handleDragEnd = (ev) => {
+ if (this.isDragging) {
+ let initialContext = this.component.context;
+ let initialView = initialContext.viewApi;
+ let { receivingContext, validMutation } = this;
+ let eventDef = this.eventRange.def;
+ let eventInstance = this.eventRange.instance;
+ let eventApi = new EventImpl(initialContext, eventDef, eventInstance);
+ let relevantEvents = this.relevantEvents;
+ let mutatedRelevantEvents = this.mutatedRelevantEvents;
+ let { finalHit } = this.hitDragging;
+ this.clearDrag(); // must happen after revert animation
+ initialContext.emitter.trigger('eventDragStop', {
+ el: this.subjectEl,
+ event: eventApi,
+ jsEvent: ev.origEvent, // Is this always a mouse event? See #4655
+ view: initialView,
+ });
+ if (validMutation) {
+ // dropped within same calendar
+ if (receivingContext === initialContext) {
+ let updatedEventApi = new EventImpl(initialContext, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
+ initialContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents,
+ });
+ let eventChangeData = {
+ oldEvent: eventApi,
+ event: updatedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents, initialContext, eventInstance),
+ revert() {
+ initialContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents, // the pre-change data
+ });
+ },
+ };
+ let transformed = {};
+ for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) {
+ Object.assign(transformed, transformer(validMutation, initialContext));
+ }
+ initialContext.emitter.trigger('eventDrop', {
+ ...eventChangeData,
+ ...transformed,
+ el: ev.subjectEl,
+ delta: validMutation.datesDelta,
+ jsEvent: ev.origEvent, // bad
+ view: initialView,
+ });
+ initialContext.emitter.trigger('eventChange', eventChangeData);
+ // dropped in different calendar
+ }
+ else if (receivingContext) {
+ let eventRemoveData = {
+ event: eventApi,
+ relatedEvents: buildEventApis(relevantEvents, initialContext, eventInstance),
+ revert() {
+ initialContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents,
+ });
+ },
+ };
+ initialContext.emitter.trigger('eventLeave', {
+ ...eventRemoveData,
+ draggedEl: ev.subjectEl,
+ view: initialView,
+ });
+ initialContext.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: relevantEvents,
+ });
+ initialContext.emitter.trigger('eventRemove', eventRemoveData);
+ let addedEventDef = mutatedRelevantEvents.defs[eventDef.defId];
+ let addedEventInstance = mutatedRelevantEvents.instances[eventInstance.instanceId];
+ let addedEventApi = new EventImpl(receivingContext, addedEventDef, addedEventInstance);
+ receivingContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents,
+ });
+ let eventAddData = {
+ event: addedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents, receivingContext, addedEventInstance),
+ revert() {
+ receivingContext.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: mutatedRelevantEvents,
+ });
+ },
+ };
+ receivingContext.emitter.trigger('eventAdd', eventAddData);
+ if (ev.isTouch) {
+ receivingContext.dispatch({
+ type: 'SELECT_EVENT',
+ eventInstanceId: eventInstance.instanceId,
+ });
+ }
+ receivingContext.emitter.trigger('drop', {
+ ...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext),
+ draggedEl: ev.subjectEl,
+ jsEvent: ev.origEvent, // Is this always a mouse event? See #4655
+ view: finalHit.context.viewApi,
+ });
+ receivingContext.emitter.trigger('eventReceive', {
+ ...eventAddData,
+ draggedEl: ev.subjectEl,
+ view: finalHit.context.viewApi,
+ });
+ }
+ }
+ else {
+ initialContext.emitter.trigger('_noEventDrop');
+ }
+ }
+ this.cleanup();
+ };
+ let { component } = this;
+ let { options } = component.context;
+ let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.pointer.selector = EventDragging.SELECTOR;
+ dragging.touchScrollAllowed = false;
+ dragging.autoScroller.isEnabled = options.dragScroll;
+ let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsStore);
+ hitDragging.useSubjectCenter = settings.useEventCenter;
+ hitDragging.emitter.on('pointerdown', this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
+ hitDragging.emitter.on('pointerup', this.handlePointerUp);
+ hitDragging.emitter.on('dragend', this.handleDragEnd);
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ // render a drag state on the next receivingCalendar
+ displayDrag(nextContext, state) {
+ let initialContext = this.component.context;
+ let prevContext = this.receivingContext;
+ // does the previous calendar need to be cleared?
+ if (prevContext && prevContext !== nextContext) {
+ // does the initial calendar need to be cleared?
+ // if so, don't clear all the way. we still need to to hide the affectedEvents
+ if (prevContext === initialContext) {
+ prevContext.dispatch({
+ type: 'SET_EVENT_DRAG',
+ state: {
+ affectedEvents: state.affectedEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ },
+ });
+ // completely clear the old calendar if it wasn't the initial
+ }
+ else {
+ prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ }
+ if (nextContext) {
+ nextContext.dispatch({ type: 'SET_EVENT_DRAG', state });
+ }
+ }
+ clearDrag() {
+ let initialCalendar = this.component.context;
+ let { receivingContext } = this;
+ if (receivingContext) {
+ receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ // the initial calendar might have an dummy drag state from displayDrag
+ if (initialCalendar !== receivingContext) {
+ initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ }
+ cleanup() {
+ this.isDragging = false;
+ this.eventRange = null;
+ this.relevantEvents = null;
+ this.receivingContext = null;
+ this.validMutation = null;
+ this.mutatedRelevantEvents = null;
+ }
+ }
+ // TODO: test this in IE11
+ // QUESTION: why do we need it on the resizable???
+ EventDragging.SELECTOR = `.${classNames.internalEventDraggable}, .${classNames.internalEventResizable}`;
+ function computeEventMutation(hit0, hit1, eventInstanceStart, massagers) {
+ let dateSpan0 = hit0.dateSpan;
+ let dateSpan1 = hit1.dateSpan;
+ let date0 = dateSpan0.range.start;
+ let date1 = dateSpan1.range.start;
+ let standardProps = {};
+ if (dateSpan0.allDay !== dateSpan1.allDay) {
+ standardProps.allDay = dateSpan1.allDay;
+ standardProps.hasEnd = hit1.context.options.allDayMaintainDuration;
+ if (dateSpan1.allDay) {
+ // means date1 is already start-of-day,
+ // but date0 needs to be converted
+ date0 = startOfDay(eventInstanceStart);
+ }
+ else {
+ // Moving from allDate->timed
+ // Doesn't matter where on the event the drag began, mutate the event's start-date to date1
+ date0 = eventInstanceStart;
+ }
+ }
+ let delta = diffDates(date0, date1, hit0.context.dateEnv, hit0.componentId === hit1.componentId ?
+ hit0.largeUnit :
+ null);
+ if (delta.milliseconds) { // has hours/minutes/seconds
+ standardProps.allDay = false;
+ }
+ let mutation = {
+ datesDelta: delta,
+ standardProps,
+ };
+ for (let massager of massagers) {
+ massager(mutation, hit0, hit1);
+ }
+ return mutation;
+ }
+ function getComponentTouchDelay(component) {
+ let { options } = component.context;
+ let delay = options.eventLongPressDelay;
+ if (delay == null) {
+ delay = options.longPressDelay;
+ }
+ return delay;
+ }
+
+ class EventResizing extends Interaction {
+ constructor(settings) {
+ super(settings);
+ // internal state
+ this.draggingSegEl = null;
+ this.draggingEventRange = null; // TODO: rename to resizingSeg? subjectSeg?
+ this.eventRange = null;
+ this.relevantEvents = null;
+ this.validMutation = null;
+ this.mutatedRelevantEvents = null;
+ this.handlePointerDown = (ev) => {
+ let { component } = this;
+ let segEl = this.querySegEl(ev);
+ let eventRange = this.eventRange = getElEventRange(segEl);
+ this.dragging.minDistance = component.context.options.eventDragMinDistance;
+ const isValid = this.component.isValidSegDownEl(ev.origEvent.target) &&
+ !(ev.isTouch && this.component.props.eventSelection !== eventRange.instance.instanceId);
+ if (!isValid) {
+ this.dragging.cancel();
+ }
+ };
+ this.handleDragStart = (ev) => {
+ let { context } = this.component;
+ let eventRange = this.eventRange;
+ this.relevantEvents = getRelevantEvents(context.getCurrentData().eventStore, this.eventRange.instance.instanceId);
+ let segEl = this.querySegEl(ev);
+ this.draggingSegEl = segEl;
+ this.draggingEventRange = getElEventRange(segEl);
+ context.calendarApi.unselect();
+ context.emitter.trigger('eventResizeStart', {
+ el: segEl,
+ event: new EventImpl(context, eventRange.def, eventRange.instance),
+ jsEvent: ev.origEvent, // Is this always a mouse event? See #4655
+ view: context.viewApi,
+ });
+ };
+ this.handleHitUpdate = (hit, isFinal, ev) => {
+ let { context } = this.component;
+ let relevantEvents = this.relevantEvents;
+ let initialHit = this.hitDragging.initialHit;
+ let eventInstance = this.eventRange.instance;
+ let mutation = null;
+ let mutatedRelevantEvents = null;
+ let isInvalid = false;
+ let interaction = {
+ affectedEvents: relevantEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ };
+ if (hit) {
+ let disallowed = hit.componentId === initialHit.componentId
+ && this.isHitComboAllowed
+ && !this.isHitComboAllowed(initialHit, hit);
+ if (!disallowed) {
+ mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains(classNames.internalEventResizerStart), eventInstance.range);
+ }
+ }
+ if (mutation) {
+ mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context);
+ interaction.mutatedEvents = mutatedRelevantEvents;
+ if (!isInteractionValid(interaction, hit.dateProfile, context)) {
+ isInvalid = true;
+ mutation = null;
+ mutatedRelevantEvents = null;
+ interaction.mutatedEvents = null;
+ }
+ }
+ if (mutatedRelevantEvents) {
+ context.dispatch({
+ type: 'SET_EVENT_RESIZE',
+ state: interaction,
+ });
+ }
+ else {
+ context.dispatch({ type: 'UNSET_EVENT_RESIZE' });
+ }
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ if (mutation && isHitsEqual(initialHit, hit)) {
+ mutation = null;
+ }
+ this.validMutation = mutation;
+ this.mutatedRelevantEvents = mutatedRelevantEvents;
+ }
+ };
+ this.handleDragEnd = (ev) => {
+ let { context } = this.component;
+ let eventDef = this.eventRange.def;
+ let eventInstance = this.eventRange.instance;
+ let eventApi = new EventImpl(context, eventDef, eventInstance);
+ let relevantEvents = this.relevantEvents;
+ let mutatedRelevantEvents = this.mutatedRelevantEvents;
+ context.emitter.trigger('eventResizeStop', {
+ el: this.draggingSegEl,
+ event: eventApi,
+ jsEvent: ev.origEvent, // Is this always a mouse event? See #4655
+ view: context.viewApi,
+ });
+ if (this.validMutation) {
+ let updatedEventApi = new EventImpl(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
+ context.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents,
+ });
+ let eventChangeData = {
+ oldEvent: eventApi,
+ event: updatedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance),
+ revert() {
+ context.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents, // the pre-change events
+ });
+ },
+ };
+ context.emitter.trigger('eventResize', {
+ ...eventChangeData,
+ el: this.draggingSegEl,
+ startDelta: this.validMutation.startDelta || createDuration(0),
+ endDelta: this.validMutation.endDelta || createDuration(0),
+ jsEvent: ev.origEvent,
+ view: context.viewApi,
+ });
+ context.emitter.trigger('eventChange', eventChangeData);
+ }
+ else {
+ context.emitter.trigger('_noEventResize');
+ }
+ // reset all internal state
+ this.draggingEventRange = null;
+ this.relevantEvents = null;
+ this.validMutation = null;
+ // okay to keep eventInstance around. useful to set it in handlePointerDown
+ };
+ let { component } = settings;
+ let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.pointer.selector = `.${classNames.internalEventResizer}`;
+ dragging.touchScrollAllowed = false;
+ dragging.autoScroller.isEnabled = component.context.options.dragScroll;
+ let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
+ hitDragging.emitter.on('dragend', this.handleDragEnd);
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ querySegEl(ev) {
+ return ev.subjectEl.closest(`.${classNames.internalEvent}`);
+ }
+ }
+ function computeMutation(hit0, hit1, isFromStart, instanceRange) {
+ let dateEnv = hit0.context.dateEnv;
+ let date0 = hit0.dateSpan.range.start;
+ let date1 = hit1.dateSpan.range.start;
+ let delta = diffDates(date0, date1, dateEnv, hit0.largeUnit);
+ if (isFromStart) {
+ if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
+ return { startDelta: delta };
+ }
+ }
+ else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
+ return { endDelta: delta };
+ }
+ return null;
+ }
+
+ class UnselectAuto {
+ constructor(context) {
+ this.context = context;
+ this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
+ this.matchesCancel = false;
+ this.matchesEvent = false;
+ this.onSelect = (selectInfo) => {
+ if (selectInfo.jsEvent) {
+ this.isRecentPointerDateSelect = true;
+ }
+ };
+ this.onDocumentPointerDown = (pev) => {
+ let unselectCancel = this.context.options.unselectCancel;
+ let downEl = getEventTargetViaRoot(pev.origEvent);
+ this.matchesCancel = !!downEl.closest(unselectCancel);
+ this.matchesEvent = !!downEl.closest(EventDragging.SELECTOR); // interaction started on an event?
+ };
+ this.onDocumentPointerUp = (pev) => {
+ let { context } = this;
+ let { documentPointer } = this;
+ let calendarState = context.getCurrentData();
+ // touch-scrolling should never unfocus any type of selection
+ if (!documentPointer.wasTouchScroll) {
+ if (calendarState.dateSelection && // an existing date selection?
+ !this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
+ ) {
+ let unselectAuto = context.options.unselectAuto;
+ if (unselectAuto && (!unselectAuto || !this.matchesCancel)) {
+ context.calendarApi.unselect(pev);
+ }
+ }
+ if (calendarState.eventSelection && // an existing event selected?
+ !this.matchesEvent // interaction DIDN'T start on an event
+ ) {
+ context.dispatch({ type: 'UNSELECT_EVENT' });
+ }
+ }
+ this.isRecentPointerDateSelect = false;
+ };
+ let documentPointer = this.documentPointer = new PointerDragging(document);
+ documentPointer.shouldIgnoreMove = true;
+ documentPointer.shouldWatchScroll = false;
+ documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown);
+ documentPointer.emitter.on('pointerup', this.onDocumentPointerUp);
+ /*
+ TODO: better way to know about whether there was a selection with the pointer
+ */
+ context.emitter.on('select', this.onSelect);
+ }
+ destroy() {
+ this.context.emitter.off('select', this.onSelect);
+ this.documentPointer.destroy();
+ }
+ }
+
+ var interactionPlugin = {
+ name: 'interaction',
+ componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing],
+ calendarInteractions: [UnselectAuto],
+ elementDraggingImpl: FeaturefulElementDragging,
+ };
+
+ /*
+ Information about what will happen when an external element is dragged-and-dropped
+ onto a calendar. Contains information for creating an event.
+ */
+ const DRAG_META_REFINERS = {
+ startTime: createDuration,
+ duration: createDuration,
+ create: Boolean,
+ sourceId: String,
+ };
+ function parseDragMeta(raw) {
+ let { refined, extra } = refineProps(raw, DRAG_META_REFINERS);
+ return {
+ startTime: refined.startTime || null,
+ duration: refined.duration || null,
+ create: refined.create != null ? refined.create : true,
+ sourceId: refined.sourceId,
+ leftoverProps: extra,
+ };
+ }
+
+ /*
+ Given an already instantiated draggable object for one-or-more elements,
+ Interprets any dragging as an attempt to drag an events that lives outside
+ of a calendar onto a calendar.
+ */
+ class ExternalElementDragging {
+ constructor(dragging, suppliedDragMeta) {
+ this.receivingContext = null;
+ this.droppableEvent = null; // will exist for all drags, even if create:false
+ this.suppliedDragMeta = null;
+ this.dragMeta = null;
+ this.handleDragStart = (ev) => {
+ this.dragMeta = this.buildDragMeta(ev.subjectEl);
+ };
+ this.handleHitUpdate = (hit, isFinal, ev) => {
+ let { dragging } = this.hitDragging;
+ let receivingContext = null;
+ let droppableEvent = null;
+ let isInvalid = false;
+ let interaction = {
+ affectedEvents: createEmptyEventStore(),
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: this.dragMeta.create,
+ };
+ if (hit) {
+ receivingContext = hit.context;
+ if (this.canDropElOnCalendar(ev.subjectEl, receivingContext)) {
+ droppableEvent = computeEventForDateSpan(hit.dateSpan, this.dragMeta, receivingContext);
+ interaction.mutatedEvents = eventTupleToStore(droppableEvent);
+ isInvalid = !isInteractionValid(interaction, hit.dateProfile, receivingContext);
+ if (isInvalid) {
+ interaction.mutatedEvents = createEmptyEventStore();
+ droppableEvent = null;
+ }
+ }
+ }
+ this.displayDrag(receivingContext, interaction);
+ // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
+ // TODO: wish we could somehow wait for dispatch to guarantee render
+ dragging.setMirrorIsVisible(isFinal ||
+ !droppableEvent ||
+ !document.querySelector(`.${classNames.internalEventMirror}`));
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ dragging.setMirrorNeedsRevert(!droppableEvent);
+ this.receivingContext = receivingContext;
+ this.droppableEvent = droppableEvent;
+ }
+ };
+ this.handleDragEnd = (pev) => {
+ let { receivingContext, droppableEvent } = this;
+ this.clearDrag();
+ if (receivingContext && droppableEvent) {
+ let finalHit = this.hitDragging.finalHit;
+ let finalView = finalHit.context.viewApi;
+ let dragMeta = this.dragMeta;
+ receivingContext.emitter.trigger('drop', {
+ ...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext),
+ draggedEl: pev.subjectEl,
+ jsEvent: pev.origEvent, // Is this always a mouse event? See #4655
+ view: finalView,
+ });
+ if (dragMeta.create) {
+ let addingEvents = eventTupleToStore(droppableEvent);
+ receivingContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: addingEvents,
+ });
+ if (pev.isTouch) {
+ receivingContext.dispatch({
+ type: 'SELECT_EVENT',
+ eventInstanceId: droppableEvent.instance.instanceId,
+ });
+ }
+ // signal that an external event landed
+ receivingContext.emitter.trigger('eventReceive', {
+ event: new EventImpl(receivingContext, droppableEvent.def, droppableEvent.instance),
+ relatedEvents: [],
+ revert() {
+ receivingContext.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: addingEvents,
+ });
+ },
+ draggedEl: pev.subjectEl,
+ view: finalView,
+ });
+ }
+ }
+ this.receivingContext = null;
+ this.droppableEvent = null;
+ };
+ let hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore);
+ hitDragging.requireInitial = false; // will start outside of a component
+ hitDragging.emitter.on('dragstart', this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
+ hitDragging.emitter.on('dragend', this.handleDragEnd);
+ this.suppliedDragMeta = suppliedDragMeta;
+ }
+ buildDragMeta(subjectEl) {
+ if (typeof this.suppliedDragMeta === 'object') {
+ return parseDragMeta(this.suppliedDragMeta);
+ }
+ if (typeof this.suppliedDragMeta === 'function') {
+ return parseDragMeta(this.suppliedDragMeta(subjectEl));
+ }
+ return getDragMetaFromEl(subjectEl);
+ }
+ displayDrag(nextContext, state) {
+ let prevContext = this.receivingContext;
+ if (prevContext && prevContext !== nextContext) {
+ prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ if (nextContext) {
+ nextContext.dispatch({ type: 'SET_EVENT_DRAG', state });
+ }
+ }
+ clearDrag() {
+ if (this.receivingContext) {
+ this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ }
+ canDropElOnCalendar(el, receivingContext) {
+ let dropAccept = receivingContext.options.dropAccept;
+ if (typeof dropAccept === 'function') {
+ return dropAccept.call(receivingContext.calendarApi, el);
+ }
+ if (typeof dropAccept === 'string' && dropAccept) {
+ return el.matches(dropAccept);
+ }
+ return true;
+ }
+ }
+ // Utils for computing event store from the DragMeta
+ // ----------------------------------------------------------------------------------------------------
+ function computeEventForDateSpan(dateSpan, dragMeta, context) {
+ let defProps = { ...dragMeta.leftoverProps };
+ for (let transform of context.pluginHooks.externalDefTransforms) {
+ Object.assign(defProps, transform(dateSpan, dragMeta));
+ }
+ let { refined, extra } = refineEventDef(defProps, context);
+ let def = parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd
+ context);
+ let start = dateSpan.range.start;
+ // only rely on time info if drop zone is all-day,
+ // otherwise, we already know the time
+ if (dateSpan.allDay && dragMeta.startTime) {
+ start = context.dateEnv.add(start, dragMeta.startTime);
+ }
+ let end = dragMeta.duration ?
+ context.dateEnv.add(start, dragMeta.duration) :
+ getDefaultEventEnd(dateSpan.allDay, start, context);
+ let instance = createEventInstance(def.defId, { start, end });
+ return { def, instance };
+ }
+ // Utils for extracting data from element
+ // ----------------------------------------------------------------------------------------------------
+ function getDragMetaFromEl(el) {
+ let str = getEmbeddedElData(el, 'event');
+ let obj = str ?
+ JSON.parse(str) :
+ { create: false }; // if no embedded data, assume no event creation
+ return parseDragMeta(obj);
+ }
+ config.dataAttrPrefix = '';
+ function getEmbeddedElData(el, name) {
+ let prefix = config.dataAttrPrefix;
+ let prefixedName = (prefix ? prefix + '-' : '') + name;
+ return el.getAttribute('data-' + prefixedName) || '';
+ }
+
+ /*
+ Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
+ The third-party system is responsible for drawing the visuals effects of the drag.
+ This class simply monitors for pointer movements and fires events.
+ It also has the ability to hide the moving element (the "mirror") during the drag.
+ */
+ class InferredElementDragging extends ElementDragging {
+ constructor(containerEl) {
+ super(containerEl);
+ this.shouldIgnoreMove = false;
+ this.mirrorSelector = '';
+ this.currentMirrorEl = null;
+ this.handlePointerDown = (ev) => {
+ this.emitter.trigger('pointerdown', ev);
+ if (!this.shouldIgnoreMove) {
+ // fire dragstart right away. does not support delay or min-distance
+ this.emitter.trigger('dragstart', ev);
+ }
+ };
+ this.handlePointerMove = (ev) => {
+ if (!this.shouldIgnoreMove) {
+ this.emitter.trigger('dragmove', ev);
+ }
+ };
+ this.handlePointerUp = (ev) => {
+ this.emitter.trigger('pointerup', ev);
+ if (!this.shouldIgnoreMove) {
+ // fire dragend right away. does not support a revert animation
+ this.emitter.trigger('dragend', ev);
+ }
+ };
+ let pointer = this.pointer = new PointerDragging(containerEl);
+ pointer.emitter.on('pointerdown', this.handlePointerDown);
+ pointer.emitter.on('pointermove', this.handlePointerMove);
+ pointer.emitter.on('pointerup', this.handlePointerUp);
+ }
+ destroy() {
+ this.pointer.destroy();
+ }
+ cancel() {
+ this.shouldIgnoreMove = true;
+ }
+ setMirrorIsVisible(bool) {
+ if (bool) {
+ // restore a previously hidden element.
+ // use the reference in case the selector class has already been removed.
+ if (this.currentMirrorEl) {
+ this.currentMirrorEl.style.visibility = '';
+ this.currentMirrorEl = null;
+ }
+ }
+ else {
+ let mirrorEl = this.mirrorSelector
+ // TODO: somehow query FullCalendars WITHIN shadow-roots
+ ? document.querySelector(this.mirrorSelector)
+ : null;
+ if (mirrorEl) {
+ this.currentMirrorEl = mirrorEl;
+ mirrorEl.style.visibility = 'hidden';
+ }
+ }
+ }
+ }
+
+ /*
+ Bridges third-party drag-n-drop systems with FullCalendar.
+ Must be instantiated and destroyed by caller.
+ */
+ class ThirdPartyDraggable {
+ constructor(containerOrSettings, settings) {
+ let containerEl = document;
+ if (
+ // wish we could just test instanceof EventTarget, but doesn't work in IE11
+ containerOrSettings === document ||
+ containerOrSettings instanceof Element) {
+ containerEl = containerOrSettings;
+ settings = settings || {};
+ }
+ else {
+ settings = (containerOrSettings || {});
+ }
+ let dragging = this.dragging = new InferredElementDragging(containerEl);
+ if (typeof settings.itemSelector === 'string') {
+ dragging.pointer.selector = settings.itemSelector;
+ }
+ else if (containerEl === document) {
+ dragging.pointer.selector = '[data-event]';
+ }
+ if (typeof settings.mirrorSelector === 'string') {
+ dragging.mirrorSelector = settings.mirrorSelector;
+ }
+ let externalDragging = new ExternalElementDragging(dragging, settings.eventData);
+ // The hit-detection system requires that the dnd-mirror-element be pointer-events:none,
+ // but this can't be guaranteed for third-party draggables, so disable
+ externalDragging.hitDragging.disablePointCheck = true;
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ }
+
+ /*
+ Makes an element (that is *external* to any calendar) draggable.
+ Can pass in data that determines how an event will be created when dropped onto a calendar.
+ Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
+ */
+ class ExternalDraggable {
+ constructor(el, settings = {}) {
+ this.handlePointerDown = (ev) => {
+ let { dragging } = this;
+ let { minDistance, longPressDelay } = this.settings;
+ dragging.minDistance =
+ minDistance != null ?
+ minDistance :
+ (ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance);
+ dragging.delay =
+ ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
+ (longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) :
+ 0;
+ };
+ this.handleDragStart = (ev) => {
+ if (ev.isTouch &&
+ this.dragging.delay &&
+ ev.subjectEl.classList.contains(classNames.internalEvent)) {
+ this.dragging.mirror.getMirrorEl().classList.add(classNames.internalEventSelected);
+ }
+ };
+ this.settings = settings;
+ let dragging = this.dragging = new FeaturefulElementDragging(el);
+ dragging.touchScrollAllowed = false;
+ if (settings.itemSelector != null) {
+ dragging.pointer.selector = settings.itemSelector;
+ }
+ if (settings.appendTo != null) {
+ dragging.mirror.parentNode = settings.appendTo; // TODO: write tests
+ }
+ dragging.emitter.on('pointerdown', this.handlePointerDown);
+ dragging.emitter.on('dragstart', this.handleDragStart);
+ new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new
+ }
+ destroy() {
+ this.dragging.destroy();
+ }
+ }
+
+ var interaction = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ Draggable: ExternalDraggable,
+ ThirdPartyDraggable: ThirdPartyDraggable,
+ 'default': interactionPlugin
+ });
+
+ class TableDateProfileGenerator extends DateProfileGenerator {
+ // Computes the date range that will be rendered
+ buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) {
+ let renderRange = super.buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay);
+ let { props } = this;
+ return buildDayTableRenderRange({
+ currentRange: renderRange, // ???
+ snapToWeek: /^(year|month)$/.test(currentRangeUnit),
+ fixedWeekCount: props.fixedWeekCount,
+ dateEnv: props.dateEnv,
+ });
+ }
+ }
+ function buildDayTableRenderRange(props) {
+ let { dateEnv, currentRange } = props;
+ let { start, end } = currentRange;
+ let endOfWeek;
+ // year and month views should be aligned with weeks. this is already done for week
+ if (props.snapToWeek) {
+ start = dateEnv.startOfWeek(start);
+ // make end-of-week if not already
+ endOfWeek = dateEnv.startOfWeek(end);
+ if (endOfWeek.valueOf() !== end.valueOf()) {
+ end = addWeeks(endOfWeek, 1);
+ }
+ }
+ // ensure 6 weeks
+ if (props.fixedWeekCount) {
+ // TODO: instead of these date-math gymnastics (for multimonth view),
+ // compute dateprofiles of all months, then use start of first and end of last.
+ let lastMonthRenderStart = dateEnv.startOfWeek(dateEnv.startOfMonth(addDays(currentRange.end, -1)));
+ let rowCount = Math.ceil(// could be partial weeks due to hiddenDays
+ diffWeeks(lastMonthRenderStart, end));
+ end = addWeeks(end, 6 - rowCount);
+ }
+ return { start, end };
+ }
+
+ class DayGridView extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.buildDayTableModel = memoize(buildDayTableModel);
+ this.buildDateRowConfigs = memoize(buildDateRowConfigs);
+ this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
+ // internal
+ this.slicer = new DayTableSlicer();
+ }
+ render() {
+ const { props, context } = this;
+ const { dateProfile } = props;
+ const { options, dateEnv } = context;
+ const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv);
+ const datesRepDistinctDays = dayTableModel.rowCount === 1;
+ const dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, dayTableModel.colCount);
+ const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel);
+ return (u$1(NowTimer, { unit: "day", children: (nowDate, todayRange) => {
+ const headerTiers = this.buildDateRowConfigs(dayTableModel.headerDates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context);
+ return (u$1(DayGridLayout, { labelId: props.labelId, labelStr: props.labelStr, dateProfile: dateProfile, todayRange: todayRange, cellRows: dayTableModel.cellRows, forPrint: props.forPrint, className: props.className,
+ // header content
+ headerTiers: headerTiers,
+ // body content
+ fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection }));
+ } }));
+ }
+ }
+
+ var dayGridPlugin = {
+ name: 'daygrid',
+ initialView: 'dayGridMonth',
+ views: {
+ dayGrid: {
+ component: DayGridView,
+ dateProfileGeneratorClass: TableDateProfileGenerator,
+ },
+ dayGridDay: {
+ type: 'dayGrid',
+ duration: { days: 1 },
+ },
+ dayGridWeek: {
+ type: 'dayGrid',
+ duration: { weeks: 1 },
+ },
+ dayGridMonth: {
+ type: 'dayGrid',
+ duration: { months: 1 },
+ fixedWeekCount: true,
+ },
+ dayGridYear: {
+ type: 'dayGrid',
+ duration: { years: 1 },
+ },
+ },
+ };
+
+ var daygrid = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ 'default': dayGridPlugin
+ });
+
+ class TimeGridView extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
+ this.buildTimeColsModel = memoize(buildTimeColsModel);
+ this.buildDayRanges = memoize(buildDayRanges);
+ this.buildDateRowConfigs = memoize(buildDateRowConfigs);
+ this.splitFgEventSegs = memoize((organizeSegsByCol));
+ this.splitBgEventSegs = memoize((organizeSegsByCol));
+ this.splitBusinessHourSegs = memoize((organizeSegsByCol));
+ this.splitNowIndicatorSegs = memoize((organizeSegsByCol));
+ this.splitDateSelectionSegs = memoize((organizeSegsByCol));
+ this.splitEventDrag = memoize(splitInteractionByCol);
+ this.splitEventResize = memoize(splitInteractionByCol);
+ // internal
+ this.allDaySplitter = new AllDaySplitter();
+ this.dayTableSlicer = new DayTableSlicer();
+ this.dayTimeColsSlicer = new DayTimeColsSlicer();
+ }
+ render() {
+ const { props, context } = this;
+ const { dateProfile } = props;
+ const { options, dateProfileGenerator } = context;
+ const dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator, context.dateEnv);
+ const dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv);
+ const splitProps = this.allDaySplitter.splitProps(props);
+ const allDayProps = this.dayTableSlicer.sliceProps(splitProps.allDay, dateProfile, options.nextDayThreshold, context, dayTableModel);
+ const timedProps = this.dayTimeColsSlicer.sliceProps(splitProps.timed, dateProfile, null, context, dayRanges);
+ const dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, true, // datesRepDistinctDays
+ dayTableModel.colCount);
+ return (u$1(NowTimer, { unit: options.nowIndicator ? 'minute' : 'day' /* hacky */, children: (nowDate, todayRange) => {
+ const colCount = dayTableModel.cellRows[0].length;
+ const nowIndicatorSeg = !props.forPrint && options.nowIndicator &&
+ this.dayTimeColsSlicer.sliceNowDate(nowDate, dateProfile, options.nextDayThreshold, context, dayRanges);
+ const fgEventSegsByCol = this.splitFgEventSegs(timedProps.fgEventSegs, colCount);
+ const bgEventSegsByCol = this.splitBgEventSegs(timedProps.bgEventSegs, colCount);
+ const businessHourSegsByCol = this.splitBusinessHourSegs(timedProps.businessHourSegs, colCount);
+ const nowIndicatorSegsByCol = this.splitNowIndicatorSegs(nowIndicatorSeg, colCount);
+ const dateSelectionSegsByCol = this.splitDateSelectionSegs(timedProps.dateSelectionSegs, colCount);
+ const eventDragByCol = this.splitEventDrag(timedProps.eventDrag, colCount);
+ const eventResizeByCol = this.splitEventResize(timedProps.eventResize, colCount);
+ const headerTiers = this.buildDateRowConfigs(dayTableModel.headerDates, true, // datesRepDistinctDays
+ props.dateProfile, todayRange, dayHeaderFormat, context);
+ return (u$1(TimeGridLayout, { labelId: props.labelId, labelStr: props.labelStr, dateProfile: dateProfile, nowDate: nowDate, todayRange: todayRange, cells: dayTableModel.cellRows[0], forPrint: props.forPrint, className: props.className,
+ // header content
+ headerTiers: headerTiers,
+ // all-day content
+ fgEventSegs: allDayProps.fgEventSegs, bgEventSegs: allDayProps.bgEventSegs, businessHourSegs: allDayProps.businessHourSegs, dateSelectionSegs: allDayProps.dateSelectionSegs, eventDrag: allDayProps.eventDrag, eventResize: allDayProps.eventResize,
+ // timed content
+ fgEventSegsByCol: fgEventSegsByCol, bgEventSegsByCol: bgEventSegsByCol, businessHourSegsByCol: businessHourSegsByCol, nowIndicatorSegsByCol: nowIndicatorSegsByCol, dateSelectionSegsByCol: dateSelectionSegsByCol, eventDragByCol: eventDragByCol, eventResizeByCol: eventResizeByCol,
+ // universal content
+ eventSelection: props.eventSelection }));
+ } }));
+ }
+ }
+
+ var timeGridPlugin = {
+ name: 'timegrid',
+ initialView: 'timeGridWeek',
+ deps: [dayGridPlugin],
+ views: {
+ timeGrid: {
+ component: TimeGridView,
+ usesMinMaxTime: true, // indicates that slotMinTime/slotMaxTime affects rendering
+ allDaySlot: true,
+ slotDuration: '00:30:00',
+ slotEventOverlap: true, // a bad name. confused with overlap/constraint system
+ },
+ timeGridDay: {
+ type: 'timeGrid',
+ duration: { days: 1 },
+ },
+ timeGridWeek: {
+ type: 'timeGrid',
+ duration: { weeks: 1 },
+ },
+ },
+ };
+
+ var timegrid = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ 'default': timeGridPlugin
+ });
+
+ class ListDayHeaderInner extends BaseComponent {
+ render() {
+ const { props, context } = this;
+ const { options } = context;
+ const textParts = context.dateEnv.formatToParts(props.dayDate, props.dayFormat);
+ const text = joinDateTimeFormatParts(textParts);
+ const hasNavLink = options.navLinks;
+ const renderProps = {
+ ...props.dateMeta,
+ view: context.viewApi,
+ text,
+ textParts,
+ get weekdayText() { return findWeekdayText(textParts); },
+ get dayNumberText() { return findDayNumberText(textParts); },
+ hasNavLink,
+ level: props.level,
+ };
+ const navLinkAttrs = hasNavLink
+ ? buildNavLinkAttrs(this.context, props.dayDate, undefined, text, this.props.isTabbable)
+ : {};
+ return (u$1(ContentContainer, { tag: "div", attrs: navLinkAttrs, renderProps: renderProps, generatorName: "listDayHeaderContent", customGenerator: options.listDayHeaderContent, defaultGenerator: renderText$1, classNameGenerator: options.listDayHeaderInnerClass }));
+ }
+ }
+
+ class ListDayHeader extends BaseComponent {
+ render() {
+ let { options, viewApi, viewSpec } = this.context;
+ let { dayDate, dateMeta } = this.props;
+ let stickyHeaderDates = !this.props.forPrint;
+ const listDayFormat = options.listDayFormat ?? createDefaultListDayFormat(viewSpec);
+ const listDayAltFormat = options.listDayAltFormat ?? createDefaultListDaySideFormat(viewSpec);
+ let renderProps = {
+ ...dateMeta,
+ view: viewApi,
+ };
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ 'data-date': formatDayString(dayDate),
+ ...(dateMeta.isToday ? { 'aria-current': 'date' } : {}),
+ }, className: stickyHeaderDates ? classNames.stickyT : '', renderProps: renderProps, generatorName: undefined, classNameGenerator: options.listDayHeaderClass, didMount: options.listDayHeaderDidMount, willUnmount: options.listDayHeaderWillUnmount, children: () => (u$1(S, { children: [Boolean(listDayFormat) && (u$1(ListDayHeaderInner, { dayDate: dayDate, dayFormat: listDayFormat, isTabbable: true, dateMeta: dateMeta, level: 0 })), Boolean(listDayAltFormat) && (u$1(ListDayHeaderInner, { dayDate: dayDate, dayFormat: listDayAltFormat, isTabbable: false, dateMeta: dateMeta, level: 1 }))] })) }));
+ }
+ }
+ function createDefaultListDayFormat({ durationUnit, singleUnit }) {
+ if (singleUnit === 'day') {
+ return WEEKDAY_ONLY_FORMAT;
+ }
+ else if (durationUnit === 'day' || singleUnit === 'week') {
+ return WEEKDAY_ONLY_FORMAT;
+ }
+ else {
+ return FULL_DATE_FORMAT;
+ }
+ }
+ function createDefaultListDaySideFormat({ durationUnit, singleUnit }) {
+ if (singleUnit === 'day') ;
+ else if (durationUnit === 'day' || singleUnit === 'week') {
+ return FULL_DATE_FORMAT;
+ }
+ else {
+ return WEEKDAY_ONLY_FORMAT;
+ }
+ }
+
+ const DEFAULT_TIME_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ meridiem: 'short',
+ });
+ class ListEvent extends BaseComponent {
+ render() {
+ let { props, context } = this;
+ let { eventRange } = props;
+ const { displayEventTime } = context.options;
+ let forcedTimeText = (displayEventTime !== false) && (eventRange.def.allDay || (!props.isStart && !props.isEnd))
+ ? context.options.allDayText
+ : undefined;
+ return (u$1(StandardEvent, { ...props, attrs: {
+ role: 'listitem',
+ }, forcedTimeText: forcedTimeText, defaultTimeFormat: DEFAULT_TIME_FORMAT, disableDragging: true, disableResizing: true, disableZindexes // because conflicts with sticky list headers
+ : true, display: 'list-item' }));
+ }
+ }
+
+ class ListDay extends BaseComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.getDateMeta = memoize(getDateMeta);
+ this.sortEventSegs = memoize(sortEventSegs);
+ }
+ render() {
+ const { props, context } = this;
+ const { nowDate, todayRange } = props;
+ const { options } = context;
+ const dateMeta = this.getDateMeta(props.dayDate, context.dateEnv, undefined, todayRange);
+ const segs = this.sortEventSegs(props.segs, options.eventOrder);
+ const fullDateStr = buildDateStr(this.context, props.dayDate);
+ const listDayData = {
+ ...dateMeta,
+ isFirst: props.isFirst,
+ isLast: props.isLast,
+ view: context.viewApi,
+ };
+ const listDayEventsData = {
+ ...dateMeta,
+ view: context.viewApi,
+ };
+ return (u$1("div", { role: 'listitem', "aria-label": fullDateStr, className: generateClassName(options.listDayClass, listDayData), children: [u$1(ListDayHeader, { dayDate: props.dayDate, dateMeta: dateMeta, forPrint: props.forPrint }), u$1("div", { role: 'list', "aria-label": options.eventsHint, className: joinClassNames(generateClassName(options.listDayBodyClass, listDayEventsData), classNames.flexCol), children: segs.map((seg, index) => {
+ const key = getEventKey(seg);
+ const isFirst = index === 0;
+ const isLast = index === segs.length - 1;
+ return (u$1(ListEvent, { eventRange: seg.eventRange, slicedStart: seg.slicedStart, slicedEnd: seg.slicedEnd, isStart: seg.isStart, isEnd: seg.isEnd, isFirst: isFirst, isLast: isLast, isDragging: false, isResizing: false, isMirror: false, isSelected: false, ...getEventRangeMeta(seg.eventRange, todayRange, nowDate) }, key));
+ }) })] }));
+ }
+ }
+
+ /*
+ Responsible for the scroller, and forwarding event-related actions into the "grid".
+ */
+ class ListView extends DateComponent {
+ constructor() {
+ super(...arguments);
+ // memo
+ this.computeDateVars = memoize(computeDateVars);
+ this.eventStoreToSegs = memoize(this._eventStoreToSegs);
+ this.setRootEl = (rootEl) => {
+ if (rootEl) {
+ this.context.registerInteractiveComponent(this, {
+ el: rootEl,
+ disableHits: true, // HACK to not do date-clicking/selecting
+ });
+ }
+ else {
+ this.context.unregisterInteractiveComponent(this);
+ }
+ };
+ }
+ render() {
+ let { props, context } = this;
+ let { options } = context;
+ let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile);
+ let eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges);
+ let verticalScrolling = !props.forPrint && !getIsHeightAuto(options);
+ return (u$1(ViewContainer, { viewSpec: context.viewSpec, className: joinClassNames(props.className, classNames.flexCol), elRef: this.setRootEl, children: eventSegs.length ? (u$1(Scroller // TODO: don't need heavyweight component
+ , { vertical: verticalScrolling, className: joinClassNames(classNames.flexCol, verticalScrolling ? classNames.liquid : ''), children: this.renderSegList(eventSegs, dayDates) })) : this.renderEmptyMessage() }));
+ }
+ renderEmptyMessage() {
+ let { options, viewApi } = this.context;
+ let renderProps = {
+ text: options.noEventsText,
+ view: viewApi,
+ };
+ return (u$1(ContentContainer, { tag: "div", attrs: {
+ role: 'status', // does a polite announcement
+ }, renderProps: renderProps, generatorName: "noEventsContent", customGenerator: options.noEventsContent, defaultGenerator: renderNoEventsInner, classNameGenerator: options.noEventsClass, className: classNames.grow, didMount: options.noEventsDidMount, willUnmount: options.noEventsWillUnmount, children: (InnerContent) => (u$1(InnerContent, { tag: "div", className: generateClassName(options.noEventsInnerClass, renderProps) })) }));
+ }
+ renderSegList(allSegs, dayDates) {
+ let { options } = this.context;
+ let segsByDay = groupSegsByDay(allSegs); // sparse array
+ return (u$1("div", { role: "list", "aria-labelledby": this.props.labelId, "aria-label": this.props.labelStr, className: joinClassNames(classNames.flexCol, joinClassNames(options.listDaysClass)), children: u$1(NowTimer, { unit: "day", children: (nowDate, todayRange) => {
+ const dayNodes = [];
+ const populatedDayCount = segsByDay.reduce((count, daySegs) => count + (daySegs ? 1 : 0), 0);
+ let populatedDayIndex = 0;
+ for (let dayIndex = 0; dayIndex < segsByDay.length; dayIndex += 1) {
+ let daySegs = segsByDay[dayIndex];
+ if (daySegs) { // sparse array, so might be undefined
+ const dayDate = dayDates[dayIndex];
+ const key = formatDayString(dayDate);
+ const isFirst = populatedDayIndex === 0;
+ const isLast = populatedDayIndex === populatedDayCount - 1;
+ dayNodes.push(u$1(ListDay, { dayDate: dayDate, nowDate: nowDate, todayRange: todayRange, segs: daySegs, isFirst: isFirst, isLast: isLast, forPrint: this.props.forPrint }, key));
+ populatedDayIndex += 1;
+ }
+ }
+ return (u$1(S, { children: dayNodes }));
+ } }) }));
+ }
+ _eventStoreToSegs(eventStore, eventUiBases, dayRanges) {
+ return this.eventRangesToSegs(sliceEventStore(eventStore, eventUiBases,
+ // HACKY to reference internal state...
+ this.props.dateProfile.activeRange, this.context.options.nextDayThreshold).fg, dayRanges);
+ }
+ eventRangesToSegs(fullDayEventRanges, dayRanges) {
+ let segs = [];
+ for (let fullDayEventRange of fullDayEventRanges) {
+ segs.push(...this.eventRangeToSegs(fullDayEventRange, dayRanges));
+ }
+ return segs;
+ }
+ eventRangeToSegs(fullDayEventRange, dayRanges) {
+ let fullDayRange = fullDayEventRange.range;
+ let dayIndex;
+ let segs = [];
+ for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex += 1) {
+ const slicedFullDayRange = intersectRanges(fullDayRange, dayRanges[dayIndex]);
+ if (slicedFullDayRange) {
+ segs.push({
+ eventRange: fullDayEventRange,
+ slicedStart: slicedFullDayRange.start,
+ slicedEnd: slicedFullDayRange.end,
+ isStart: fullDayEventRange.isStart && fullDayRange.start.valueOf() === slicedFullDayRange.start.valueOf(),
+ isEnd: fullDayEventRange.isEnd && fullDayRange.end.valueOf() === slicedFullDayRange.end.valueOf(),
+ dayIndex,
+ });
+ }
+ }
+ return segs;
+ }
+ }
+ function renderNoEventsInner(renderProps) {
+ return renderProps.text;
+ }
+ function computeDateVars(dateProfile) {
+ let dayStart = startOfDay(dateProfile.renderRange.start);
+ let viewEnd = dateProfile.renderRange.end;
+ let dayDates = [];
+ let dayRanges = [];
+ while (dayStart < viewEnd) {
+ dayDates.push(dayStart);
+ dayRanges.push({
+ start: dayStart,
+ end: addDays(dayStart, 1),
+ });
+ dayStart = addDays(dayStart, 1);
+ }
+ return { dayDates, dayRanges };
+ }
+ // Returns a sparse array of arrays, segs grouped by their dayIndex
+ function groupSegsByDay(segs) {
+ let segsByDay = []; // sparse array
+ let i;
+ let seg;
+ for (i = 0; i < segs.length; i += 1) {
+ seg = segs[i];
+ (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
+ .push(seg);
+ }
+ return segsByDay;
+ }
+
+ var listPlugin = {
+ name: 'list',
+ views: {
+ list: {
+ component: ListView,
+ buttonTextKey: 'listText', // what to lookup in locale files
+ disallowAmbigTitle: true,
+ },
+ listDay: {
+ type: 'list',
+ duration: { days: 1 },
+ },
+ listWeek: {
+ type: 'list',
+ duration: { weeks: 1 },
+ },
+ listMonth: {
+ type: 'list',
+ duration: { month: 1 },
+ },
+ listYear: {
+ type: 'list',
+ duration: { year: 1 },
+ },
+ },
+ };
+
+ var list = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ 'default': listPlugin
+ });
+
+ class SingleMonth extends DateComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ // memo
+ this.buildDayTableModel = memoize(buildDayTableModel);
+ this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
+ this.buildDateRowConfig = memoize(buildDateRowConfig);
+ // ref
+ this.titleElRef = M$1();
+ this.tableHeaderElRef = M$1();
+ this.rowHeightRefMap = new RefMap(() => {
+ afterSize(this.handleHeights);
+ });
+ this.slicer = new DayTableSlicer();
+ this.handleEl = (el) => {
+ const { options } = this.context;
+ if (el) {
+ this.rootEl = el;
+ options.singleMonthDidMount?.({
+ el: this.rootEl,
+ ...this.renderProps,
+ });
+ }
+ };
+ this.handleGridWidth = (gridWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ gridWidth });
+ };
+ this.handleHeights = () => {
+ if (this._isUnmounting)
+ return;
+ setRef(this.props.heightsRef, {
+ titleHeight: this.titleHeight,
+ tableHeaderHeight: this.tableHeaderHeight,
+ rowHeightMap: this.rowHeightRefMap.current,
+ cellRows: this.cellRows,
+ });
+ };
+ }
+ get titleId() {
+ return this.context.baseId + 'month-' + this.props.isoDateStr;
+ }
+ render() {
+ const { props, state, context } = this;
+ const { dateProfile, forPrint } = props;
+ const { options, dateEnv } = context;
+ const { borderlessX, borderlessTop, borderlessBottom } = computeViewBorderless(options);
+ const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator, dateEnv);
+ const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel);
+ const dayHeaderFormat = this.createDayHeaderFormatter(options.dayHeaderFormat, false, // datesRepDistinctDays
+ dayTableModel.colCount);
+ const rowConfig = this.buildDateRowConfig(dayTableModel.headerDates, false, // datesRepDistinctDays
+ dateProfile, props.todayRange, dayHeaderFormat, context);
+ this.cellRows = dayTableModel.cellRows;
+ const isTitleAndHeaderSticky = !forPrint && props.colCount === 1;
+ const isAspectRatio = !forPrint || props.hasLateralSiblings;
+ const cellColCnt = dayTableModel.cellRows[0].length;
+ const colWidth = state.gridWidth != null ? state.gridWidth / cellColCnt : undefined;
+ const cellIsMicro = colWidth != null && colWidth <= dayMicroWidth;
+ const cellIsNarrow = cellIsMicro || (colWidth != null && colWidth <= options.dayNarrowWidth);
+ const rowHeightGuess = state.gridWidth != null
+ ? (1 / options.aspectRatio) * state.gridWidth / 6
+ : undefined;
+ const headerStickyBottom = isTitleAndHeaderSticky
+ ? rowHeightGuess
+ : undefined;
+ const titleStickyBottom = isTitleAndHeaderSticky && rowHeightGuess != null && state.tableHeaderHeight != null
+ ? rowHeightGuess + state.tableHeaderHeight + 1
+ : undefined;
+ const businessHourSegs = forPrint ? [] : slicedProps.businessHourSegs;
+ const dateSelectionSegs = forPrint ? [] : slicedProps.dateSelectionSegs;
+ const eventDrag = forPrint ? null : slicedProps.eventDrag;
+ const eventResize = forPrint ? null : slicedProps.eventResize;
+ const hasNavLink = options.navLinks && props.colCount > 1;
+ const headerRenderProps = {
+ multiMonthColumns: props.colCount || 0,
+ isSticky: isTitleAndHeaderSticky,
+ isNarrow: cellIsNarrow,
+ hasNavLink,
+ };
+ const monthStartDate = props.dateProfile.currentRange.start;
+ const navLinkAttrs = hasNavLink
+ ? buildNavLinkAttrs(context, monthStartDate, 'month', props.isoDateStr)
+ : {};
+ return (u$1("div", { role: 'listitem', style: { width: props.width }, children: u$1("div", { role: 'grid', "aria-labelledby": this.titleId, "data-date": props.isoDateStr, className: joinClassNames(generateClassName(options.singleMonthClass, {
+ isFirst: props.isFirst,
+ isLast: props.isLast,
+ multiMonthColumns: props.colCount || 0,
+ }), classNames.flexCol, props.hasLateralSiblings && classNames.breakInsideAvoid), children: [u$1(Ruler, { widthRef: this.handleGridWidth }), u$1("div", { id: this.titleId, ref: this.titleElRef, className: joinClassNames(generateClassName(options.singleMonthHeaderClass, headerRenderProps), isTitleAndHeaderSticky && classNames.stickyT, classNames.flexCol), style: {
+ // HACK to keep zIndex above table-header,
+ // because in Chrome, something about position:sticky on this title div
+ // causes its bottom border to no be considered part of its mass,
+ // and would get overlapped and hidden by the table-header div
+ zIndex: isTitleAndHeaderSticky ? 3 : undefined, // TODO: className?
+ marginBottom: titleStickyBottom,
+ }, children: u$1("div", { ...navLinkAttrs, className: joinClassNames(generateClassName(options.singleMonthHeaderInnerClass, headerRenderProps), navLinkAttrs.className), children: joinDateTimeFormatParts(dateEnv.formatToParts(monthStartDate, props.titleFormat)) }) }), u$1("div", { className: joinClassNames(generateClassName(options.tableClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: props.colCount || 0,
+ }), classNames.flexCol), style: {
+ marginTop: titleStickyBottom != null ? -titleStickyBottom : undefined,
+ }, children: [u$1("div", { ref: this.tableHeaderElRef, className: joinClassNames(generateClassName(options.tableHeaderClass, {
+ isSticky: isTitleAndHeaderSticky,
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: props.colCount || 0,
+ }), classNames.flexCol, isTitleAndHeaderSticky && classNames.sticky), style: {
+ zIndex: isTitleAndHeaderSticky ? 2 : undefined, // TODO: className?
+ top: isTitleAndHeaderSticky ? state.titleHeight : 0,
+ marginBottom: headerStickyBottom,
+ }, children: [u$1(DayGridHeaderRow, { ...rowConfig, role: 'row', borderBottom: false, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowLevel: 0 }), u$1("div", { className: generateClassName(options.dayHeaderDividerClass, {
+ isSticky: isTitleAndHeaderSticky,
+ multiMonthColumns: props.colCount || 0,
+ options: { allDaySlot: Boolean(options.allDaySlot) },
+ }) })] }), u$1("div", { className: joinClassNames(generateClassName(options.tableBodyClass, {
+ borderlessX,
+ borderlessTop,
+ borderlessBottom,
+ multiMonthColumns: props.colCount || 0,
+ }), classNames.flexCol, isAspectRatio && classNames.rel), style: {
+ zIndex: isTitleAndHeaderSticky ? 1 : undefined, // TODO: className?
+ marginTop: headerStickyBottom != null ? -headerStickyBottom : undefined,
+ aspectRatio: isAspectRatio ? String(options.aspectRatio) : undefined,
+ }, children: u$1(DayGridRows, { dateProfile: props.dateProfile, todayRange: props.todayRange, cellRows: dayTableModel.cellRows, className: isAspectRatio ? classNames.fill : '', forPrint: forPrint && !props.hasLateralSiblings, dayMaxEventRows: (forPrint && props.hasLateralSiblings)
+ ? 1 // for side-by-side multimonths, limit to one row
+ : true // otherwise, always do +more link, never expand rows
+ ,
+ // content
+ fgEventSegs: slicedProps.fgEventSegs, bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: businessHourSegs, dateSelectionSegs: dateSelectionSegs, eventDrag: eventDrag, eventResize: eventResize, eventSelection: slicedProps.eventSelection,
+ // dimensions
+ visibleWidth: state.gridWidth, cellIsNarrow: cellIsNarrow, cellIsMicro: cellIsMicro, rowHeightRefMap: this.rowHeightRefMap }) })] })] }) }));
+ }
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.disconnectTitleHeight = watchHeight(this.titleElRef.current, (height) => {
+ this.setState({ titleHeight: this.titleHeight = height });
+ afterSize(this.handleHeights);
+ });
+ this.disconnectTableHeaderHeight = watchHeight(this.tableHeaderElRef.current, (height) => {
+ this.setState({ tableHeaderHeight: this.tableHeaderHeight = height });
+ afterSize(this.handleHeights);
+ });
+ }
+ componentWillUnmount() {
+ const { options } = this.context;
+ this._isUnmounting = true;
+ this.disconnectTitleHeight();
+ this.disconnectTableHeaderHeight();
+ options.singleMonthWillUnmount?.({
+ el: this.rootEl,
+ ...this.renderProps,
+ });
+ }
+ }
+
+ class MultiMonthView extends DateComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {};
+ // memo
+ this.splitDateProfileByMonth = memoize(splitDateProfileByMonth);
+ this.buildMonthFormat = memoize(buildMonthFormat);
+ // ref
+ this.scrollerRef = M$1();
+ this.tilesElRef = M$1();
+ this.scrollState = {};
+ // Scrolling
+ // -----------------------------------------------------------------------------------------------
+ this.handleInnerWidth = (innerWidth) => {
+ if (this._isUnmounting)
+ return;
+ this.setState({ innerWidth });
+ };
+ this.handleScrollStart = () => {
+ this.scrollState.date = undefined;
+ this.scrollState.top = undefined;
+ };
+ this.handleScrollEnd = (isDevice) => {
+ const scroller = this.scrollerRef.current;
+ if (isDevice && scroller) {
+ this.scrollState.top = scroller.y;
+ this.scrollState.date = undefined;
+ }
+ };
+ }
+ render() {
+ const { context, props, state } = this;
+ const { options } = context;
+ const verticalScrolling = !props.forPrint && !getIsHeightAuto(options);
+ const monthDateProfiles = this.splitDateProfileByMonth(context.dateProfileGenerator, props.dateProfile, context.dateEnv, options.fixedWeekCount, options.showNonCurrentDates);
+ const monthTitleFormat = this.buildMonthFormat(options.singleMonthTitleFormat, monthDateProfiles);
+ const { multiMonthMaxColumns, singleMonthMinWidth } = options;
+ const { innerWidth } = state;
+ let cols;
+ let cssMonthWidth;
+ let hasLateralSiblings = false;
+ if (innerWidth != null) {
+ cols = Math.max(1, Math.min(multiMonthMaxColumns, Math.floor(innerWidth / singleMonthMinWidth)));
+ if (props.forPrint) {
+ cols = Math.min(cols, 2);
+ }
+ cssMonthWidth = fracToCssDim(1 / cols);
+ hasLateralSiblings = cols > 1;
+ }
+ return (u$1(NowTimer, { unit: "day", children: (nowDate, todayRange) => (u$1(ViewContainer, { viewSpec: context.viewSpec, className: joinClassNames(
+ // HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
+ // https://stackoverflow.com/a/60256345
+ !props.forPrint && classNames.flexCol, props.className), children: [u$1(Scroller, { vertical: verticalScrolling, className: verticalScrolling ? classNames.liquid : '', ref: this.scrollerRef, children: u$1("div", { role: 'list', ref: this.tilesElRef, "aria-labelledby": props.labelId, "aria-label": props.labelStr, className: classNames.safeTiles, children: monthDateProfiles.map((monthDateProfile, i) => {
+ const monthStr = formatIsoMonthStr(monthDateProfile.currentRange.start);
+ return (k$1(SingleMonth, { ...props, key: monthStr, todayRange: todayRange, isoDateStr: monthStr, titleFormat: monthTitleFormat, dateProfile: monthDateProfile, width: cssMonthWidth, colCount: cols, isFirst: !i, isLast: i === monthDateProfiles.length - 1, hasLateralSiblings: hasLateralSiblings }));
+ }) }) }), u$1(Ruler, { widthRef: this.handleInnerWidth })] })) }));
+ }
+ // Lifecycle
+ // -----------------------------------------------------------------------------------------------
+ componentDidMount() {
+ this._isUnmounting = false;
+ this.scrollState.date = this.props.dateProfile.currentDate;
+ this.scrollerRef.current.addScrollStartListener(this.handleScrollStart);
+ this.scrollerRef.current.addScrollEndListener(this.handleScrollEnd);
+ // this.applyScroll() // definitely not ready yet b/c doesn't have state.innerWidth
+ // workaround for off-by-a-few-pixels on first time when multiMonthMaxColumns=1, not sure why
+ setTimeout(() => {
+ this.applyScroll();
+ }, 0);
+ }
+ componentDidUpdate(prevProps, prevState) {
+ if (prevProps.dateProfile !== this.props.dateProfile) {
+ if (this.context.options.scrollTimeReset) {
+ this.resetScroll();
+ }
+ else {
+ this.applyScroll();
+ }
+ }
+ else if (prevState.innerWidth !== this.state.innerWidth) {
+ this.applyScroll();
+ }
+ }
+ componentWillUnmount() {
+ this._isUnmounting = true;
+ this.scrollerRef.current.removeScrollStartListener(this.handleScrollStart);
+ this.scrollerRef.current.removeScrollEndListener(this.handleScrollEnd);
+ }
+ resetScroll() {
+ this.scrollState.date = this.props.dateProfile.currentDate;
+ this.scrollState.top = undefined;
+ this.applyScroll();
+ }
+ applyScroll() {
+ const scroller = this.scrollerRef.current;
+ const top = this.computeScrollTop();
+ if (scroller && top != null) {
+ scroller.scrollTo({ y: top });
+ }
+ }
+ computeScrollTop() {
+ const { scrollState } = this;
+ if (scrollState.top != null) {
+ return scrollState.top;
+ }
+ if (scrollState.date != null) {
+ const tilesEl = this.tilesElRef.current;
+ const monthEl = tilesEl?.querySelector(`[data-date="${formatIsoMonthStr(scrollState.date)}"]`);
+ const monthWrapEl = monthEl?.parentElement;
+ if (tilesEl && monthWrapEl) {
+ // rounding required for proper alignment
+ const monthTop = Math.round(monthWrapEl.getBoundingClientRect().top);
+ const originTop = Math.round(tilesEl.getBoundingClientRect().top);
+ return monthTop - originTop;
+ }
+ }
+ }
+ }
+ // date profile
+ // -------------------------------------------------------------------------------------------------
+ const oneMonthDuration = createDuration(1, 'month');
+ function splitDateProfileByMonth(dateProfileGenerator, dateProfile, dateEnv, fixedWeekCount, showNonCurrentDates) {
+ const { start, end } = dateProfile.currentRange;
+ let monthStart = start;
+ const monthDateProfiles = [];
+ while (monthStart.valueOf() < end.valueOf()) {
+ const monthEnd = dateEnv.add(monthStart, oneMonthDuration);
+ const currentRange = {
+ // yuck
+ start: dateProfileGenerator.skipHiddenDays(monthStart),
+ end: dateProfileGenerator.skipHiddenDays(monthEnd, -1, true),
+ };
+ let renderRange = buildDayTableRenderRange({
+ currentRange,
+ snapToWeek: true,
+ fixedWeekCount,
+ dateEnv,
+ });
+ renderRange = {
+ // yuck
+ start: dateProfileGenerator.skipHiddenDays(renderRange.start),
+ end: dateProfileGenerator.skipHiddenDays(renderRange.end, -1, true),
+ };
+ const activeRange = dateProfile.activeRange ?
+ intersectRanges(dateProfile.activeRange, showNonCurrentDates ? renderRange : currentRange) :
+ null;
+ monthDateProfiles.push({
+ currentDate: dateProfile.currentDate,
+ isValid: dateProfile.isValid,
+ validRange: dateProfile.validRange,
+ renderRange,
+ activeRange,
+ currentRange,
+ currentRangeUnit: 'month',
+ isRangeAllDay: true,
+ dateIncrement: dateProfile.dateIncrement,
+ slotMinTime: dateProfile.slotMaxTime,
+ slotMaxTime: dateProfile.slotMinTime,
+ });
+ monthStart = monthEnd;
+ }
+ return monthDateProfiles;
+ }
+ // date formatting
+ // -------------------------------------------------------------------------------------------------
+ const YEAR_MONTH_FORMATTER = createFormatter({ year: 'numeric', month: 'long' });
+ const YEAR_FORMATTER = createFormatter({ month: 'long' });
+ function buildMonthFormat(formatOverride, monthDateProfiles) {
+ return formatOverride ||
+ ((monthDateProfiles[0].currentRange.start.getUTCFullYear() !==
+ monthDateProfiles[monthDateProfiles.length - 1].currentRange.start.getUTCFullYear())
+ ? YEAR_MONTH_FORMATTER
+ : YEAR_FORMATTER);
+ }
+
+ var multiMonthPlugin = {
+ name: 'multimonth',
+ initialView: 'multiMonthYear',
+ views: {
+ multiMonth: {
+ component: MultiMonthView,
+ dateProfileGeneratorClass: TableDateProfileGenerator,
+ multiMonthMaxColumns: 3,
+ singleMonthMinWidth: 350,
+ },
+ multiMonthYear: {
+ type: 'multiMonth',
+ duration: { years: 1 },
+ fixedWeekCount: true, // TODO: apply to all multi-col layouts?
+ showNonCurrentDates: false, // TODO: looks bad when single-col layout
+ },
+ },
+ };
+
+ var multimonth = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ 'default': multiMonthPlugin
+ });
+
+ const blankButtonState = {
+ text: '', hint: '', isDisabled: false,
+ };
+ class CalendarController {
+ constructor(handleDateChange) {
+ this.handleDateChange = handleDateChange;
+ }
+ today() {
+ this.calendarApi?.today();
+ }
+ prev() {
+ this.calendarApi?.prev();
+ }
+ next() {
+ this.calendarApi?.next();
+ }
+ prevYear() {
+ this.calendarApi?.prevYear();
+ }
+ nextYear() {
+ this.calendarApi?.nextYear();
+ }
+ gotoDate(zonedDateInput) {
+ this.calendarApi?.gotoDate(zonedDateInput);
+ }
+ incrementDate(duration) {
+ this.calendarApi?.incrementDate(duration);
+ }
+ changeView(viewType) {
+ this.calendarApi?.changeView(viewType);
+ }
+ get view() {
+ return this.calendarApi?.view;
+ }
+ getDate() {
+ return this.calendarApi?.getDate();
+ }
+ getButtonState() {
+ const { calendarApi } = this;
+ return (calendarApi && calendarApi.getButtonState()) || {
+ today: blankButtonState,
+ prev: blankButtonState,
+ next: blankButtonState,
+ prevYear: blankButtonState,
+ nextYear: blankButtonState,
+ };
+ }
+ _setApi(calendarApi) {
+ if (this.calendarApi !== calendarApi) {
+ if (this.calendarApi) {
+ this.calendarApi.off('datesSet', this.handleDateChange);
+ this.calendarApi = undefined;
+ }
+ if (calendarApi) {
+ this.calendarApi = calendarApi;
+ calendarApi.on('datesSet', this.handleDateChange);
+ }
+ }
+ }
+ }
+
+ function formatDate(dateInput, options = {}) {
+ let dateEnv = buildDateEnv(options);
+ let formatter = createFormatter(options);
+ let dateMeta = dateEnv.createMarkerMeta(dateInput);
+ if (!dateMeta) { // TODO: warning?
+ return '';
+ }
+ return joinDateTimeFormatParts(dateEnv.formatToParts(dateMeta.marker, formatter));
+ }
+ 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 joinDateTimeFormatParts(dateEnv.formatRangeToParts(startMeta.marker, endMeta.marker, formatter, {
+ isEndExclusive: options.isEndExclusive,
+ }));
+ }
+ // 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({
+ timeZone: BASE_OPTION_DEFAULTS.timeZone,
+ calendarSystem: 'gregory',
+ ...settings,
+ locale,
+ });
+ }
+
+ /*
+ 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 = '7.0.0';
+
+ var protectedStyles = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ 'default': classNames
+ });
+
+ function createRoot(container) {
+ return {
+ // eslint-disable-next-line
+ render: function (children) {
+ nn(children, container);
+ },
+ // eslint-disable-next-line
+ unmount: function () {
+ pn(container);
+ }
+ };
+ }
+
+ /*
+ Vanilla JS API
+ */
+ class Calendar$1 extends CalendarApiImpl {
+ constructor(el, optionOverrides = {}) {
+ super();
+ this.baseId = `fc:${guid()}:`;
+ this.isRendering = false;
+ this.isRendered = false;
+ this.customContentRenderId = 0;
+ this.currentClassName = '';
+ this.currentColorScheme = '';
+ this.handleDataChange = (data, actions) => {
+ this.currentData = data;
+ let renderImmediate = false;
+ for (const action of actions) {
+ if (action.type === 'SET_EVENT_DRAG' ||
+ action.type === 'UNSET_EVENT_DRAG' ||
+ action.type === 'SET_EVENT_RESIZE' ||
+ action.type === 'UNSET_EVENT_RESIZE' ||
+ // could happen as a result of a drag or resize and must be part of same sync pipeline
+ action.type === 'MERGE_EVENTS') {
+ renderImmediate = true;
+ break;
+ }
+ }
+ this.renderRunner.request(renderImmediate ? undefined : data.calendarOptions.rerenderDelay);
+ };
+ this.handleRenderRequest = () => {
+ if (this.isRendering) {
+ let { currentData } = this;
+ this.isRendered = true;
+ bn(() => {
+ this.vdomRoot.render(u$1(S, { children: u$1(RenderId.Provider, { value: this.customContentRenderId, children: u$1(CalendarMediaRoot, { emitter: currentData.emitter, children: (forPrint) => {
+ const options = currentData.calendarOptions;
+ const isRtl = options.direction === 'rtl';
+ const className = computeRootClassName(options, forPrint);
+ this.setIsRtl(isRtl);
+ this.setClassName(className);
+ this.setHeight(options.height);
+ this.setColorScheme(options.colorScheme || '');
+ return (u$1(CalendarInner, { ...currentData, forPrint: forPrint, baseId: this.baseId }));
+ } }) }) }));
+ });
+ }
+ else if (this.isRendered) {
+ this.isRendered = false;
+ this.vdomRoot.unmount();
+ this.setIsRtl(false);
+ this.setClassName('');
+ this.setHeight('');
+ this.setColorScheme('');
+ }
+ };
+ this.el = el;
+ this.vdomRoot = createRoot(el);
+ this.renderRunner = new DelayedRunner(this.handleRenderRequest);
+ this.dataManager = new CalendarDataManager({
+ calendarApi: this,
+ onDataChange: this.handleDataChange,
+ });
+ this.currentData = this.dataManager.update(optionOverrides);
+ }
+ render() {
+ let wasRendering = this.isRendering;
+ if (!wasRendering) {
+ this.isRendering = true;
+ }
+ else {
+ this.customContentRenderId += 1;
+ }
+ this.renderRunner.request();
+ }
+ destroy() {
+ if (this.isRendering) {
+ this.isRendering = false;
+ this.renderRunner.request();
+ }
+ this.dataManager.destroy();
+ }
+ 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);
+ }
+ setClassName(className) {
+ if (className !== this.currentClassName) {
+ let { classList } = this.el;
+ for (let singleClassName of this.currentClassName.split(' ')) {
+ if (singleClassName) {
+ classList.remove(singleClassName);
+ }
+ }
+ for (let singleClassName of className.split(' ')) {
+ if (singleClassName) {
+ classList.add(singleClassName);
+ }
+ }
+ this.currentClassName = className;
+ }
+ }
+ setHeight(height) {
+ applyStyleProp(this.el, 'height', height);
+ }
+ setColorScheme(colorScheme) {
+ if (colorScheme !== this.currentColorScheme) {
+ if (colorScheme) {
+ this.el.dataset.colorScheme = colorScheme;
+ }
+ else {
+ delete this.el.dataset.colorScheme;
+ }
+ this.currentColorScheme = colorScheme;
+ }
+ }
+ setIsRtl(isRtl) {
+ if (isRtl) {
+ this.el.dir = 'rtl';
+ }
+ else {
+ this.el.removeAttribute('dir');
+ }
+ }
+ }
+
+ const plugins = [
+ interactionPlugin,
+ dayGridPlugin,
+ timeGridPlugin,
+ listPlugin,
+ multiMonthPlugin,
+ ];
+ class Calendar extends Calendar$1 {
+ constructor(el, optionOverrides = {}) {
+ super(el, {
+ ...optionOverrides,
+ plugins: [
+ ...plugins,
+ ...(optionOverrides.plugins || []),
+ ]
+ });
+ }
+ }
+
+ const Shared = { F: globalLocales, G: globalPlugins, H: joinClassNames, S, u: u$1 };
+
+ exports.Calendar = Calendar;
+ exports.CalendarController = CalendarController;
+ exports.DayGrid = daygrid;
+ exports.Interaction = interaction;
+ exports.JsonRequestError = JsonRequestError;
+ exports.List = list;
+ exports.MultiMonth = multimonth;
+ exports.Preact = preact;
+ exports.PreactJSXRuntime = jsxRuntime;
+ exports.ProtectedApi = protectedApi;
+ exports.ProtectedStyles = protectedStyles;
+ exports.Shared = Shared;
+ exports.TimeGrid = timegrid;
+ exports.formatDate = formatDate;
+ exports.formatRange = formatRange;
+ exports.globalLocales = globalLocales;
+ exports.globalPlugins = globalPlugins;
+ exports.joinClassNames = joinClassNames;
+ exports.sliceEvents = sliceEvents;
+ exports.version = version;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+ return exports;
+
+})({});
diff --git a/pub/src/js/sentri/companiesMap.js b/pub/src/js/sentri/companiesMap.js
new file mode 100644
index 0000000..a5a7240
--- /dev/null
+++ b/pub/src/js/sentri/companiesMap.js
@@ -0,0 +1,34 @@
+async function loadCompanies() {
+
+ // Use cached data if available
+ const cached = localStorage.getItem('companies');
+
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ const response = await fetch(
+ '/api/v1/customers/companies/?' +
+ 'builder[0][where][0]=company_state&' +
+ 'builder[0][where][1]=active',
+ {
+ credentials: 'include'
+ }
+ );
+
+ const companies = await response.json();
+
+ localStorage.setItem('companies', JSON.stringify(companies));
+
+ return companies;
+}
+
+function getCompanyMap() {
+
+ const companies = JSON.parse(localStorage.getItem('companies') || '[]');
+
+ return companies.reduce((map, company) => {
+ map[company.company_uuid] = company.company_name;
+ return map;
+ }, {});
+}
\ No newline at end of file
diff --git a/pub/src/js/sentri/travelReimburse.js b/pub/src/js/sentri/travelReimburse.js
new file mode 100644
index 0000000..e416ba4
--- /dev/null
+++ b/pub/src/js/sentri/travelReimburse.js
@@ -0,0 +1,357 @@
+let calendar;
+let travelEventSource;
+const rootStyles = getComputedStyle(document.documentElement);
+const background = rootStyles.getPropertyValue('--swal-bg')?.trim();
+const color = rootStyles.getPropertyValue('--swal-text-color')?.trim();
+document.addEventListener('DOMContentLoaded', async function () {
+ await loadCompanies();
+
+ const companyMap = getCompanyMap();
+
+ const calendarEl = document.getElementById('calendar');
+
+ // Build calendar
+ calendar = new FullCalendar.Calendar(calendarEl, {
+ initialDate: new Date().toISOString().split('T')[0],
+ contentHeight: 'auto',
+ initialView: 'dayGridMonth',
+ aspectRatio: 3,
+ fixedWeekCount: false,
+
+ selectable: true,
+ businessHours: true,
+ dayMaxEvents: true,
+ showNonCurrentDates: false,
+
+ headerToolbar: {
+ start: 'title',
+ end: 'today prev,next',
+ },
+
+
+ datesSet: function (info) {
+ const date = calendar.getDate();
+
+ document.getElementById('calender_month').value =
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
+ },
+
+ eventContent: function (arg) {
+ return {html: arg.event.title};
+ },
+
+ dateClick: function (info) {
+
+ const clicked = info.date;
+ const current = info.view.currentStart;
+
+ // Compare month + year only
+ const sameMonth =
+ clicked.getMonth() === current.getMonth() &&
+ clicked.getFullYear() === current.getFullYear();
+
+ if (!sameMonth) {
+ return; // ignore outside-month days
+ }
+
+ createTravelReimburse(info.dateStr);
+ },
+
+ // Open modal on click
+ eventClick: function (info) {
+
+ const event = info.event;
+
+ document.getElementById('reimburse_uuid').value = event.id;
+ document.getElementById('departure_postcode').value = event.extendedProps.departure;
+ document.getElementById('destination_postcode').value = event.extendedProps.destination;
+ document.getElementById('travel_distance').value = event.extendedProps.distance;
+ document.getElementById('travel_date').value = event.extendedProps.travel_date;
+ document.getElementById('homework').checked = event.extendedProps.homework;
+ document.getElementById('office_travelreimburse_company_uuid').value = event.extendedProps.company;
+ document.getElementById('travel_description').value = event.extendedProps.description ?? '';
+
+ new bootstrap.Modal(document.getElementById('travelModal')).show();
+ }
+ });
+
+ calendar.render();
+
+
+ // Event retrievement
+ travelEventSource = calendar.addEventSource({
+ events: function (fetchInfo, successCallback, failureCallback) {
+
+ const start = toYMD(fetchInfo.start);
+
+ const endDate = new Date(fetchInfo.end);
+ endDate.setDate(endDate.getDate() - 1);
+
+ const end = toYMD(endDate);
+
+ fetch(`/api/v1/office/travel-reimburse/?` +
+ `builder[1][between][0]=travel_date&` +
+ `builder[1][between][1]=${start}&` +
+ `builder[1][between][2]=${end}`, {
+ credentials: 'include'
+ })
+ .then(res => res.json())
+ .then(data => {
+ let totalDistanceHomework = 0;
+ let totalDistanceBusiness = 0;
+
+ data.forEach(item => {
+ const distance = parseFloat(item.travel_distance) || 0;
+
+ if (Number(item.homework) === 1) {
+ totalDistanceHomework += distance;
+ } else {
+ totalDistanceBusiness += distance;
+ }
+ });
+
+ const total_homeWorkIncl = document.getElementById('total_travel_distance_homework_incl');
+ const total_homeWorkExcl = document.getElementById('total_travel_distance_homework_excl');
+ const total_businesskIncl = document.getElementById('total_travel_distance_business_incl');
+ const total_businessExcl = document.getElementById('total_travel_distance_business_excl');
+
+ if (total_homeWorkExcl) {
+ total_homeWorkExcl.textContent = `${totalDistanceHomework} km`;
+ total_homeWorkIncl.textContent = `${totalDistanceHomework} km`;
+ }
+
+ if (total_businesskIncl) {
+ total_businesskIncl.textContent = `${totalDistanceBusiness} km`;
+ total_businessExcl.textContent = `${totalDistanceBusiness} km`;
+ }
+
+ const formatEuro = amount =>
+ amount.toLocaleString('nl-NL', {
+ style: 'currency',
+ currency: 'EUR'
+ });
+
+ const parseEuro = (el) => {
+ if (!el) return 0;
+
+ // "€ 0,23" -> 0.23
+ return parseFloat(
+ el.textContent
+ .replace('€', '')
+ .replace(/\s/g, '')
+ .replace('.', '')
+ .replace(',', '.')
+ ) || 0;
+ };
+
+ const homeworkInclRate = parseEuro(document.getElementById('office_travelreimburse_cents_homework_incl'));
+ const homeworkExclRate = parseEuro(document.getElementById('office_travelreimburse_cents_homework_excl'));
+ const businessInclRate = parseEuro(document.getElementById('office_travelreimburse_cents_business_incl'));
+ const businessExclRate = parseEuro(document.getElementById('office_travelreimburse_cents_business_excl'));
+
+
+ document.getElementById('office_travelreimburse_cents_homework_incl_total').textContent =
+ formatEuro(totalDistanceHomework * homeworkInclRate);
+
+ document.getElementById('office_travelreimburse_cents_homework_excl_total').textContent =
+ formatEuro(totalDistanceHomework * homeworkExclRate);
+
+ document.getElementById('office_travelreimburse_cents_business_incl_total').textContent =
+ formatEuro(totalDistanceBusiness * businessInclRate);
+
+ document.getElementById('office_travelreimburse_cents_business_excl_total').textContent =
+ formatEuro(totalDistanceBusiness * businessExclRate);
+
+ document.getElementById('total_travel_incl').textContent =
+ formatEuro((totalDistanceBusiness * businessInclRate) + (totalDistanceHomework * homeworkInclRate));
+
+ document.getElementById('total_travel_excl').textContent =
+ formatEuro((totalDistanceBusiness * businessExclRate) + (totalDistanceHomework * homeworkExclRate));
+
+ const events = data.map(item => ({
+ id: item.reimburse_uuid,
+ title: `${item.travel_distance}km | ${companyMap[item.office_travelreimburse_company_uuid] ?? item.office_travelreimburse_company_uuid}`,
+ start: item.travel_date,
+
+ ...(item.homework === 1 && {
+ color: 'var(--bs-success)'
+ }),
+
+ extendedProps: {
+ departure: item.departure_postcode,
+ destination: item.destination_postcode,
+ distance: item.travel_distance,
+ travel_date: item.travel_date,
+ description: item.travel_description,
+ company: item.office_travelreimburse_company_uuid,
+ homework: item.homework
+ }
+ }));
+
+ successCallback(events);
+ })
+ .catch(failureCallback);
+ }
+ });
+});
+
+function toYMD(dateStr) {
+ const d = new Date(dateStr);
+
+ return d.getFullYear() + '-' +
+ String(d.getMonth() + 1).padStart(2, '0') + '-' +
+ String(d.getDate()).padStart(2, '0');
+}
+
+// Update from modal form
+document.getElementById('saveTravelBtn').addEventListener('click', function () {
+
+ const payload = {
+ reimburse_uuid: document.getElementById('reimburse_uuid').value,
+ departure_postcode: document.getElementById('departure_postcode').value,
+ destination_postcode: document.getElementById('destination_postcode').value,
+ travel_distance: document.getElementById('travel_distance').value,
+ travel_description: document.getElementById('travel_description').value,
+ travel_date: document.getElementById('travel_date').value,
+ homework: document.getElementById('homework').checked,
+ office_travelreimburse_company_uuid: document.getElementById('office_travelreimburse_company_uuid').value,
+ "X-HTTP-Method-Override": "PUT"
+ };
+
+ $.post('/api/v1/office/travel-reimburse/', payload)
+ .done(function (response) {
+
+ if (response.error) {
+ alert(response.error);
+ return;
+ }
+
+ calendar.refetchEvents();
+
+ // close modal
+ bootstrap.Modal.getInstance(
+ document.getElementById('travelModal')
+ ).hide();
+ })
+ .fail(function (xhr) {
+ console.error(xhr.responseText);
+ alert('Something went wrong');
+ });
+});
+
+document.getElementById('deleteTravelBtn').addEventListener('click', function () {
+
+ const payload = {
+ reimburse_uuid: document.getElementById('reimburse_uuid').value,
+ "X-HTTP-Method-Override": "DELETE"
+ };
+
+ $.post('/api/v1/office/travel-reimburse/', payload)
+ .done(function (response) {
+
+ if (response.error) {
+ alert(response.error);
+ return;
+ }
+
+ calendar.refetchEvents();
+
+ // close modal
+ bootstrap.Modal.getInstance(
+ document.getElementById('travelModal')
+ ).hide();
+ })
+ .fail(function (xhr) {
+ console.error(xhr.responseText);
+ alert('Something went wrong');
+ });
+});
+
+function createTravelReimburse(date) {
+
+ const payload = {
+ departure_postcode: document.getElementById('new_departure_postcode').value,
+ destination_postcode: document.getElementById('new_destination_postcode').value,
+ travel_distance: document.getElementById('new_travel_distance').value,
+ homework: Number(document.getElementById('new_homework').checked),
+ office_travelreimburse_company_uuid: document.getElementById('new_office_travelreimburse_company_uuid').value,
+ travel_date: date,
+ "X-HTTP-Method-Override": "POST"
+ };
+
+ $.post('/api/v1/office/travel-reimburse/', payload)
+ .done(function (response) {
+
+ if (response.error) {
+ alert(response.error);
+ return;
+ }
+
+ // refresh calendar
+ calendar.refetchEvents();
+ })
+ .fail(function (xhr) {
+ console.error(xhr.responseText);
+ alert('Something went wrong');
+ });
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+
+ const form = document.getElementById('defaultForNew');
+
+ const fields = [
+ 'new_departure_postcode',
+ 'new_destination_postcode',
+ 'new_travel_distance',
+ 'new_homework',
+ 'new_office_travelreimburse_company_uuid'
+ ];
+
+ // Load defaults
+ const defaults = JSON.parse(localStorage.getItem('travelDefaults') || '{}');
+
+ fields.forEach(id => {
+ const element = document.getElementById(id);
+
+ if (!element || defaults[id] === undefined) return;
+
+ if (element.type === 'checkbox') {
+ element.checked = !!defaults[id];
+ } else {
+ element.value = defaults[id];
+ }
+ });
+
+ // Save defaults
+ form.addEventListener('submit', function (e) {
+ e.preventDefault();
+
+ const defaults = {};
+
+ fields.forEach(id => {
+ const el = document.getElementById(id);
+
+ if (!el) return;
+
+ if (el.type === 'checkbox') {
+ defaults[id] = el.checked ? 1 : 0;
+ } else {
+ defaults[id] = el.value;
+ }
+ });
+
+ localStorage.setItem('travelDefaults', JSON.stringify(defaults));
+
+ Swal.fire({
+ background: background,
+ color: color,
+ icon: 'success',
+ title: __('default_saved'),
+ text: __('default_have_been_saved'),
+ timer: 700,
+ showConfirmButton: false
+ });
+ });
+
+});