UNPKG

@fullcalendar/web-component

Version:
1,067 lines (1,045 loc) 101 kB
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