@fullcalendar/web-component
Version:
Custom Element for FullCalendar
1,067 lines (1,045 loc) • 101 kB
JavaScript
import { F as FooterScrollbar, d as dayGridPlugin } from './ca2f9465.js';
import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
import { B as BaseComponent, a as memoize, f as afterSize, h as ViewContainer, g as generateClassName, o as DateComponent, d as getIsHeightAuto, j as getTableHeaderSticky, k as getFooterScrollbarSticky, S as Scroller, s as setRef, l as getScrollerSyncerClass, C as ContentContainer, p as watchWidth, q as getDateMeta, r as watchSize, t as buildDateStr, u as renderText, v as buildNavLinkAttrs, x as StandardEvent, V as ViewContextType, n as NowTimer } from './fa7e597f.js';
import { S as Slicer, j as DaySeriesModel, k as DayTableModel, l as getCellEl, R as RefMap, b as computeColWidth, d as DayGridHeaderRow, a as Ruler, m as DayGridRow, n as computeColFromPosition, B as BgEvent, r as renderFill, e as dayMicroWidth, o as SegHierarchy, p as groupIntersectingSegs, M as MoreLinkContainer, q as binarySearch, s as getCoordRangeEnd, f as DayTableSlicer, i as createDayHeaderFormatter, h as buildDateRowConfigs } from './bd0d9fb1.js';
import { ai as hasBgRendering, c as createFormatter, j as isPropsEqualShallow, G as isArraysEqual, I as computeViewBorderless, F as classNames, p as mapHash, l as createEmptyEventStore, aj as combineEventUis, ak as sortEventSegs, al as getEventRangeMeta, am as buildEventRangeKey, an as getEventKey } from './69b11357.js';
import { intersectRanges, createDuration, asRoughMs, wholeDivideDurations, formatIsoTimeString, addDurations, multiplyDuration, startOfDay, rangeContainsMarker, addDays, formatDayString, joinDateTimeFormatParts, diffDays } from '@full-ui/headless-calendar';
import { j as joinClassNames, f as fracToCssDim } from './423b7bc6.js';
import { createRef, createElement } from 'preact/compat';
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 = createRef();
}
render() {
let { props } = this;
let { options, viewApi } = this.context;
let renderProps = {
text: options.allDayText,
view: viewApi,
isNarrow: props.isNarrow,
};
return (jsx(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) => (jsx("div", { className: joinClassNames(classNames.flexRow, classNames.noShrink, classNames.whiteSpacePre), ref: this.innerElRef, children: jsx(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 = createRef();
this.handleRootEl = (rootEl) => {
this.rootEl = rootEl;
if (rootEl) {
this.context.registerInteractiveComponent(this, {
el: rootEl,
});
}
else {
this.context.unregisterInteractiveComponent(this);
}
};
}
render() {
return (jsx(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 = createFormatter({
hour: 'numeric',
minute: '2-digit',
meridiem: false,
});
class TimeGridEvent extends BaseComponent {
render() {
const { props } = this;
return (jsx(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 }));
}
}
class TimeGridMoreLink extends BaseComponent {
render() {
let { props } = this;
return (jsx("div", { className: joinClassNames(classNames.abs, classNames.flexCol), style: {
top: props.top,
height: props.height,
insetInlineEnd: 0,
zIndex: 9999, // HACK. move to className?
}, children: jsx(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) => (jsx(ViewContextType.Consumer, { children: (context) => {
let { options } = context;
return (jsx("div", { className: joinClassNames(props.className, options.nowIndicatorDotClass), style: props.style }));
} }));
const NowIndicatorLineContainer = (props) => (jsx(ViewContextType.Consumer, { children: (context) => {
let { options } = context;
let renderProps = {
date: context.dateEnv.toDate(props.date),
view: context.viewApi,
};
return (jsx(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 (jsxs("div", { className: classNames.fill, style: {
zIndex: 2, // inlined from $now-indicator-z
pointerEvents: 'none', // TODO: className
}, children: [jsx(NowIndicatorLineContainer, { className: joinClassNames(classNames.fillX, classNames.noMarginX, classNames.borderlessX), style: { top }, date: props.nowDate }), (props.showDot ?? true) && (jsx(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 (jsx("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 (jsx(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: () => (jsxs(Fragment, { children: [this.renderFillSegs(props.businessHourSegs, 'non-business'), this.renderFillSegs(props.bgEventSegs, 'bg-event'), this.renderFillSegs(props.dateSelectionSegs, 'highlight'), jsx("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
jsx("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 (jsxs(Fragment, { 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 (jsx("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: jsx(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 (jsx(Fragment, { children: hiddenGroups.map((hiddenGroup) => {
return (jsx(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 (jsx(Fragment, { children: segs.map((seg, index) => {
const { eventRange } = seg;
const segVertical = segVerticals[index] || {};
return (jsx("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' ?
jsx(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) => (jsx(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 (jsx(Fragment, { 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 (jsx("div", { className: classNames.breakInsideAvoid, style: { visibility: isInvisible ? 'hidden' : undefined }, children: jsx(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 (jsx("div", { role: props.role /* !!! */, className: joinClassNames(props.className, classNames.flexRow), ref: this.handleRootEl, children: props.cells.map((cell, col) => (jsx(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) => (jsx(ViewContextType.Consumer, { children: (context) => {
let { options } = context;
let renderProps = {
date: context.dateEnv.toDate(props.date),
view: context.viewApi,
};
return (jsx(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 (jsx("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: jsx(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 = createRef();
}
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 (jsx("div", { className: joinClassNames(generateClassName(options.slotHeaderClass, renderProps), className), style: {
height: props.height,
} }));
}
return (jsx(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) => (jsx("div", { ref: this.innerElRef, className: joinClassNames(classNames.noShrink, classNames.whiteSpaceNoWrap, classNames.flexRow), children: jsx(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 (jsx(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 = createRef();
}
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 (jsx(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, classNameGenerator: options.weekNumberHeaderClass, didMount: options.weekNumberHeaderDidMount, willUnmount: options.weekNumberHeaderWillUnmount, children: (InnerContent) => (jsx("div", { ref: this.innerElRef, className: joinClassNames(classNames.flexRow, classNames.noShrink, classNames.whiteSp