@fullcalendar/web-component
Version:
Custom Element for FullCalendar
1,296 lines (1,273 loc) • 56.5 kB
JavaScript
import { N as refineClassName, O as getUnequalProps, P as mergeMaybePropsDepth1, j as isPropsEqualShallow, c as createFormatter, D as formatWithOrdinals, F as classNames, Q as createAriaClickAttrs, I as computeViewBorderless, g as guid, R as computeElIsRtl, v as EventImpl, S as setElEventRange, T as buildEventRangeTimeText, U as getEventTagAndAttrs, W as computeEventRangeDraggable, G as isArraysEqual, X as isPropsEqualWithMap, k as Emitter } from './69b11357.js';
import { createContext, Component, createElement, flushSync, isValidElement } from 'preact/compat';
import { j as joinClassNames } from './423b7bc6.js';
import { joinDateTimeFormatParts, createDuration, startOfDay, addDays, rangeContainsMarker } from '@full-ui/headless-calendar';
import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
const classNamesRe = /(^c|C)lass(Name)?$/;
const contentRe = /Content$/;
const lifecycleRe = /(DidMount|WillUnmount)$/;
const handlerRe = /^on[A-Z]/;
// Somewhat tracks COMPLEX_OPTION_COMPARATORS
// Unfortunately always need 'maybe' to handle undefined inital value, because of CalendarDataManager
const customMergeFuncs = {
buttons: mergeMaybePropsDepth1,
};
function mergeViewOptionsMap(...hashes) {
const merged = {};
for (const hash of hashes) {
for (const viewName in hash) {
const viewOptions = hash[viewName];
if (!merged[viewName]) {
merged[viewName] = viewOptions;
}
else {
merged[viewName] = mergeCalendarOptions(merged[viewName], viewOptions);
}
}
}
return merged;
}
/*
Merges an array of RAW options objects into a single object.
The second argument allows for an array of property names who's object values will be merged together.
*/
function mergeCalendarOptions(...optionSets) {
let dest = {};
for (const options of optionSets) {
for (let name in options) {
if (name in dest) {
const mergeFunc = customMergeFuncs[name] || (classNamesRe.test(name) ? joinFuncishClassNames :
contentRe.test(name) ? mergeContentInjectors :
lifecycleRe.test(name) ? mergeLifecycleCallbacks : undefined);
dest[name] = mergeFunc
? mergeFunc(dest[name], options[name], name)
: options[name]; // last wins
}
else {
dest[name] = options[name]; // last wins
}
}
}
return dest;
}
/*
Called while merging raw option objects, before the normal option refinement pass.
ClassName values are validated here because merging may join raw strings, or build a
combined function that joins raw generator outputs later. Without checking each part
before joinClassNames, invalid values like objects/arrays could be stringified into
valid-looking class strings before refineClassName/refineClassNameGenerator see them.
Ideally this would be a single-pass responsibility: either merge after refinement, or
store unjoined class parts during raw merging and have one later refiner validate and
join all parts. For now, this merge helper validates just enough to avoid corrupting
invalid values before the formal refinement pass.
*/
function joinFuncishClassNames(input0, // added to string first
input1, optionName) {
const isFunc0 = typeof input0 === 'function';
const isFunc1 = typeof input1 === 'function';
if (isFunc0 || isFunc1) {
const combinedFunc = (info) => {
return joinClassNames(refineClassName(isFunc0 ? input0(info) : input0, optionName), refineClassName(isFunc1 ? input1(info) : input1, optionName));
};
combinedFunc.parts = [input0, input1]; // see CalendarDataManager::processRawCalendarOptions
return combinedFunc;
}
return joinClassNames(refineClassName(input0, optionName), refineClassName(input1, optionName));
}
function mergeContentInjectors(contentGenerator0, // fallback
contentGenerator1) {
if (typeof contentGenerator1 === 'function') {
// fabricate new function
const combinedFunc = (renderProps) => {
const res = contentGenerator1(renderProps);
if (res === true) { // `true` indicates use-fallback
if (typeof contentGenerator0 === 'function') {
return contentGenerator0(renderProps);
}
return contentGenerator0;
}
return res;
};
combinedFunc.parts = [contentGenerator0, contentGenerator1]; // see CalendarDataManager::processRawCalendarOptions
return combinedFunc;
}
if (contentGenerator1 != null) {
return contentGenerator1;
}
return contentGenerator0;
}
function mergeLifecycleCallbacks(fn0, // called first
fn1) {
if (fn0 && fn1) {
// fabricate new function
const combinedFunc = (...args) => {
fn0(...args);
fn1(...args);
};
combinedFunc.parts = [fn0, fn1]; // see CalendarDataManager::processRawCalendarOptions
return combinedFunc;
}
return fn0 || fn1;
}
function isNonHandlerPropsEqual(obj0, obj1) {
const keys = getUnequalProps(obj0, obj1);
for (let key of keys) {
if (!handlerRe.test(key)) {
return false;
}
}
return true;
}
function isMergedPropsEqual(val0, val1) {
const parts0 = val0 && val0.parts;
const parts1 = val1 && val1.parts;
if (parts0 && parts1) {
const count0 = parts0.length;
const count1 = parts1.length;
if (count0 !== count1) {
return false;
}
for (let i = 0; i < count0; i++) {
if (!(parts0[i] === parts1[i] || isMergedPropsEqual(parts0[i], parts1[i]))) {
return false;
}
}
return true;
}
return false;
}
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 = createContext({}); // 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 Component {
// 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;
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
? isValidElement(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 createElement(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,
generatorMeta,
renderProps: props.renderProps,
});
}
}
}
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 = createContext(0);
class ContentContainer extends Component {
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 createElement(props.tag, attrs, children);
}
else {
return children;
}
}
else {
return createElement((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 createElement((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(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 NowTimer extends Component {
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);
}
}
flushSync(() => {
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);
}
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 (jsx(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(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 (jsxs("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) && (jsx("div", { ref: this.handleHRuler, className: classNames.fillTop })), Boolean(props.clientHeightRef || props.bottomScrollbarWidthRef) && (jsx("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' &