UNPKG

@fullcalendar/timeline

Version:

Display events on a horizontal time axis (without resources)

1,068 lines (1,052 loc) 62.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var internal_cjs = require('@fullcalendar/core/internal.cjs'); var preact_cjs = require('@fullcalendar/core/preact.cjs'); var internal_cjs$1 = require('@fullcalendar/scrollgrid/internal.cjs'); const MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours const MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour const MAX_AUTO_CELLS = 200; // allows 4-days to have a :30 slot duration internal_cjs.config.MAX_TIMELINE_SLOTS = 1000; // potential nice values for slot-duration and interval-duration const STOCK_SUB_DURATIONS = [ { years: 1 }, { months: 1 }, { days: 1 }, { hours: 1 }, { minutes: 30 }, { minutes: 15 }, { minutes: 10 }, { minutes: 5 }, { minutes: 1 }, { seconds: 30 }, { seconds: 15 }, { seconds: 10 }, { seconds: 5 }, { seconds: 1 }, { milliseconds: 500 }, { milliseconds: 100 }, { milliseconds: 10 }, { milliseconds: 1 }, ]; function buildTimelineDateProfile(dateProfile, dateEnv, allOptions, dateProfileGenerator) { let tDateProfile = { labelInterval: allOptions.slotLabelInterval, slotDuration: allOptions.slotDuration, }; validateLabelAndSlot(tDateProfile, dateProfile, dateEnv); // validate after computed grid duration ensureLabelInterval(tDateProfile, dateProfile, dateEnv); ensureSlotDuration(tDateProfile, dateProfile, dateEnv); let input = allOptions.slotLabelFormat; let rawFormats = Array.isArray(input) ? input : (input != null) ? [input] : computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions); tDateProfile.headerFormats = rawFormats.map((rawFormat) => internal_cjs.createFormatter(rawFormat)); tDateProfile.isTimeScale = Boolean(tDateProfile.slotDuration.milliseconds); let largeUnit = null; if (!tDateProfile.isTimeScale) { const slotUnit = internal_cjs.greatestDurationDenominator(tDateProfile.slotDuration).unit; if (/year|month|week/.test(slotUnit)) { largeUnit = slotUnit; } } tDateProfile.largeUnit = largeUnit; tDateProfile.emphasizeWeeks = internal_cjs.asCleanDays(tDateProfile.slotDuration) === 1 && currentRangeAs('weeks', dateProfile, dateEnv) >= 2 && !allOptions.businessHours; /* console.log('label interval =', timelineView.labelInterval.humanize()) console.log('slot duration =', timelineView.slotDuration.humanize()) console.log('header formats =', timelineView.headerFormats) console.log('isTimeScale', timelineView.isTimeScale) console.log('largeUnit', timelineView.largeUnit) */ let rawSnapDuration = allOptions.snapDuration; let snapDuration; let snapsPerSlot; if (rawSnapDuration) { snapDuration = internal_cjs.createDuration(rawSnapDuration); snapsPerSlot = internal_cjs.wholeDivideDurations(tDateProfile.slotDuration, snapDuration); // ^ TODO: warning if not whole? } if (snapsPerSlot == null) { snapDuration = tDateProfile.slotDuration; snapsPerSlot = 1; } tDateProfile.snapDuration = snapDuration; tDateProfile.snapsPerSlot = snapsPerSlot; // more... let timeWindowMs = internal_cjs.asRoughMs(dateProfile.slotMaxTime) - internal_cjs.asRoughMs(dateProfile.slotMinTime); // TODO: why not use normalizeRange!? let normalizedStart = normalizeDate(dateProfile.renderRange.start, tDateProfile, dateEnv); let normalizedEnd = normalizeDate(dateProfile.renderRange.end, tDateProfile, dateEnv); // apply slotMinTime/slotMaxTime // TODO: View should be responsible. if (tDateProfile.isTimeScale) { normalizedStart = dateEnv.add(normalizedStart, dateProfile.slotMinTime); normalizedEnd = dateEnv.add(internal_cjs.addDays(normalizedEnd, -1), dateProfile.slotMaxTime); } tDateProfile.timeWindowMs = timeWindowMs; tDateProfile.normalizedRange = { start: normalizedStart, end: normalizedEnd }; let slotDates = []; let date = normalizedStart; while (date < normalizedEnd) { if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) { slotDates.push(date); } date = dateEnv.add(date, tDateProfile.slotDuration); } tDateProfile.slotDates = slotDates; // more... let snapIndex = -1; let snapDiff = 0; // index of the diff :( const snapDiffToIndex = []; const snapIndexToDiff = []; date = normalizedStart; while (date < normalizedEnd) { if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) { snapIndex += 1; snapDiffToIndex.push(snapIndex); snapIndexToDiff.push(snapDiff); } else { snapDiffToIndex.push(snapIndex + 0.5); } date = dateEnv.add(date, tDateProfile.snapDuration); snapDiff += 1; } tDateProfile.snapDiffToIndex = snapDiffToIndex; tDateProfile.snapIndexToDiff = snapIndexToDiff; tDateProfile.snapCnt = snapIndex + 1; // is always one behind tDateProfile.slotCnt = tDateProfile.snapCnt / tDateProfile.snapsPerSlot; // more... tDateProfile.isWeekStarts = buildIsWeekStarts(tDateProfile, dateEnv); tDateProfile.cellRows = buildCellRows(tDateProfile, dateEnv); tDateProfile.slotsPerLabel = internal_cjs.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration); return tDateProfile; } /* snaps to appropriate unit */ function normalizeDate(date, tDateProfile, dateEnv) { let normalDate = date; if (!tDateProfile.isTimeScale) { normalDate = internal_cjs.startOfDay(normalDate); if (tDateProfile.largeUnit) { normalDate = dateEnv.startOf(normalDate, tDateProfile.largeUnit); } } return normalDate; } /* snaps to appropriate unit */ function normalizeRange(range, tDateProfile, dateEnv) { if (!tDateProfile.isTimeScale) { range = internal_cjs.computeVisibleDayRange(range); if (tDateProfile.largeUnit) { let dayRange = range; // preserve original result range = { start: dateEnv.startOf(range.start, tDateProfile.largeUnit), end: dateEnv.startOf(range.end, tDateProfile.largeUnit), }; // if date is partially through the interval, or is in the same interval as the start, // make the exclusive end be the *next* interval if (range.end.valueOf() !== dayRange.end.valueOf() || range.end <= range.start) { range = { start: range.start, end: dateEnv.add(range.end, tDateProfile.slotDuration), }; } } } return range; } function isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator) { if (dateProfileGenerator.isHiddenDay(date)) { return false; } if (tDateProfile.isTimeScale) { // determine if the time is within slotMinTime/slotMaxTime, which may have wacky values let day = internal_cjs.startOfDay(date); let timeMs = date.valueOf() - day.valueOf(); let ms = timeMs - internal_cjs.asRoughMs(dateProfile.slotMinTime); // milliseconds since slotMinTime ms = ((ms % 86400000) + 86400000) % 86400000; // make negative values wrap to 24hr clock return ms < tDateProfile.timeWindowMs; // before the slotMaxTime? } return true; } function validateLabelAndSlot(tDateProfile, dateProfile, dateEnv) { const { currentRange } = dateProfile; // make sure labelInterval doesn't exceed the max number of cells if (tDateProfile.labelInterval) { const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.labelInterval); if (labelCnt > internal_cjs.config.MAX_TIMELINE_SLOTS) { console.warn('slotLabelInterval results in too many cells'); tDateProfile.labelInterval = null; } } // make sure slotDuration doesn't exceed the maximum number of cells if (tDateProfile.slotDuration) { const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.slotDuration); if (slotCnt > internal_cjs.config.MAX_TIMELINE_SLOTS) { console.warn('slotDuration results in too many cells'); tDateProfile.slotDuration = null; } } // make sure labelInterval is a multiple of slotDuration if (tDateProfile.labelInterval && tDateProfile.slotDuration) { const slotsPerLabel = internal_cjs.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration); if (slotsPerLabel === null || slotsPerLabel < 1) { console.warn('slotLabelInterval must be a multiple of slotDuration'); tDateProfile.slotDuration = null; } } } function ensureLabelInterval(tDateProfile, dateProfile, dateEnv) { const { currentRange } = dateProfile; let { labelInterval } = tDateProfile; if (!labelInterval) { // compute based off the slot duration // find the largest label interval with an acceptable slots-per-label let input; if (tDateProfile.slotDuration) { for (input of STOCK_SUB_DURATIONS) { const tryLabelInterval = internal_cjs.createDuration(input); const slotsPerLabel = internal_cjs.wholeDivideDurations(tryLabelInterval, tDateProfile.slotDuration); if (slotsPerLabel !== null && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) { labelInterval = tryLabelInterval; break; } } // use the slot duration as a last resort if (!labelInterval) { labelInterval = tDateProfile.slotDuration; } // compute based off the view's duration // find the largest label interval that yields the minimum number of labels } else { for (input of STOCK_SUB_DURATIONS) { labelInterval = internal_cjs.createDuration(input); const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, labelInterval); if (labelCnt >= MIN_AUTO_LABELS) { break; } } } tDateProfile.labelInterval = labelInterval; } return labelInterval; } function ensureSlotDuration(tDateProfile, dateProfile, dateEnv) { const { currentRange } = dateProfile; let { slotDuration } = tDateProfile; if (!slotDuration) { const labelInterval = ensureLabelInterval(tDateProfile, dateProfile, dateEnv); // will compute if necessary // compute based off the label interval // find the largest slot duration that is different from labelInterval, but still acceptable for (let input of STOCK_SUB_DURATIONS) { const trySlotDuration = internal_cjs.createDuration(input); const slotsPerLabel = internal_cjs.wholeDivideDurations(labelInterval, trySlotDuration); if (slotsPerLabel !== null && slotsPerLabel > 1 && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) { slotDuration = trySlotDuration; break; } } // only allow the value if it won't exceed the view's # of slots limit if (slotDuration) { const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, slotDuration); if (slotCnt > MAX_AUTO_CELLS) { slotDuration = null; } } // use the label interval as a last resort if (!slotDuration) { slotDuration = labelInterval; } tDateProfile.slotDuration = slotDuration; } return slotDuration; } function computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions) { let format1; let format2; const { labelInterval } = tDateProfile; let unit = internal_cjs.greatestDurationDenominator(labelInterval).unit; const weekNumbersVisible = allOptions.weekNumbers; let format0 = (format1 = (format2 = null)); // NOTE: weekNumber computation function wont work if ((unit === 'week') && !weekNumbersVisible) { unit = 'day'; } switch (unit) { case 'year': format0 = { year: 'numeric' }; // '2015' break; case 'month': if (currentRangeAs('years', dateProfile, dateEnv) > 1) { format0 = { year: 'numeric' }; // '2015' } format1 = { month: 'short' }; // 'Jan' break; case 'week': if (currentRangeAs('years', dateProfile, dateEnv) > 1) { format0 = { year: 'numeric' }; // '2015' } format1 = { week: 'narrow' }; // 'Wk4' break; case 'day': if (currentRangeAs('years', dateProfile, dateEnv) > 1) { format0 = { year: 'numeric', month: 'long' }; // 'January 2014' } else if (currentRangeAs('months', dateProfile, dateEnv) > 1) { format0 = { month: 'long' }; // 'January' } if (weekNumbersVisible) { format1 = { week: 'short' }; // 'Wk 4' } format2 = { weekday: 'narrow', day: 'numeric' }; // 'Su 9' break; case 'hour': if (weekNumbersVisible) { format0 = { week: 'short' }; // 'Wk 4' } if (currentRangeAs('days', dateProfile, dateEnv) > 1) { format1 = { weekday: 'short', day: 'numeric', month: 'numeric', omitCommas: true }; // Sat 4/7 } format2 = { hour: 'numeric', minute: '2-digit', omitZeroMinute: true, meridiem: 'short', }; break; case 'minute': // sufficiently large number of different minute cells? if ((internal_cjs.asRoughMinutes(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) { format0 = { hour: 'numeric', meridiem: 'short', }; format1 = (params) => (':' + internal_cjs.padStart(params.date.minute, 2) // ':30' ); } else { format0 = { hour: 'numeric', minute: 'numeric', meridiem: 'short', }; } break; case 'second': // sufficiently large number of different second cells? if ((internal_cjs.asRoughSeconds(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) { format0 = { hour: 'numeric', minute: '2-digit', meridiem: 'lowercase' }; // '8:30 PM' format1 = (params) => (':' + internal_cjs.padStart(params.date.second, 2) // ':30' ); } else { format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM' } break; case 'millisecond': format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM' format1 = (params) => ('.' + internal_cjs.padStart(params.millisecond, 3)); break; } return [].concat(format0 || [], format1 || [], format2 || []); } // Compute the number of the give units in the "current" range. // Won't go more precise than days. // Will return `0` if there's not a clean whole interval. function currentRangeAs(unit, dateProfile, dateEnv) { let range = dateProfile.currentRange; let res = null; if (unit === 'years') { res = dateEnv.diffWholeYears(range.start, range.end); } else if (unit === 'months') { res = dateEnv.diffWholeMonths(range.start, range.end); } else if (unit === 'weeks') { res = dateEnv.diffWholeMonths(range.start, range.end); } else if (unit === 'days') { res = internal_cjs.diffWholeDays(range.start, range.end); } return res || 0; } function buildIsWeekStarts(tDateProfile, dateEnv) { let { slotDates, emphasizeWeeks } = tDateProfile; let prevWeekNumber = null; let isWeekStarts = []; for (let slotDate of slotDates) { let weekNumber = dateEnv.computeWeekNumber(slotDate); let isWeekStart = emphasizeWeeks && (prevWeekNumber !== null) && (prevWeekNumber !== weekNumber); prevWeekNumber = weekNumber; isWeekStarts.push(isWeekStart); } return isWeekStarts; } function buildCellRows(tDateProfile, dateEnv) { let slotDates = tDateProfile.slotDates; let formats = tDateProfile.headerFormats; let cellRows = formats.map(() => []); // indexed by row,col let slotAsDays = internal_cjs.asCleanDays(tDateProfile.slotDuration); let guessedSlotUnit = slotAsDays === 7 ? 'week' : slotAsDays === 1 ? 'day' : null; // specifically for navclicks let rowUnitsFromFormats = formats.map((format) => (format.getSmallestUnit ? format.getSmallestUnit() : null)); // builds cellRows and slotCells for (let i = 0; i < slotDates.length; i += 1) { let date = slotDates[i]; let isWeekStart = tDateProfile.isWeekStarts[i]; for (let row = 0; row < formats.length; row += 1) { let format = formats[row]; let rowCells = cellRows[row]; let leadingCell = rowCells[rowCells.length - 1]; let isLastRow = row === formats.length - 1; let isSuperRow = formats.length > 1 && !isLastRow; // more than one row and not the last let newCell = null; let rowUnit = rowUnitsFromFormats[row] || (isLastRow ? guessedSlotUnit : null); if (isSuperRow) { let text = dateEnv.format(date, format); if (!leadingCell || (leadingCell.text !== text)) { newCell = buildCellObject(date, text, rowUnit); } else { leadingCell.colspan += 1; } } else if (!leadingCell || internal_cjs.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval))) { let text = dateEnv.format(date, format); newCell = buildCellObject(date, text, rowUnit); } else { leadingCell.colspan += 1; } if (newCell) { newCell.weekStart = isWeekStart; rowCells.push(newCell); } } } return cellRows; } function buildCellObject(date, text, rowUnit) { return { date, text, rowUnit, colspan: 1, isWeekStart: false }; } class TimelineHeaderTh extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.refineRenderProps = internal_cjs.memoizeObjArg(refineRenderProps); this.buildCellNavLinkAttrs = internal_cjs.memoize(buildCellNavLinkAttrs); } render() { let { props, context } = this; let { dateEnv, options } = context; let { cell, dateProfile, tDateProfile } = props; // the cell.rowUnit is f'd // giving 'month' for a 3-day view // workaround: to infer day, do NOT time let dateMeta = internal_cjs.getDateMeta(cell.date, props.todayRange, props.nowDate, dateProfile); let renderProps = this.refineRenderProps({ level: props.rowLevel, dateMarker: cell.date, text: cell.text, dateEnv: context.dateEnv, viewApi: context.viewApi, }); return (preact_cjs.createElement(internal_cjs.ContentContainer, { elTag: "th", elClasses: [ 'fc-timeline-slot', 'fc-timeline-slot-label', cell.isWeekStart && 'fc-timeline-slot-em', ...( // TODO: so slot classnames for week/month/bigger. see note above about rowUnit cell.rowUnit === 'time' ? internal_cjs.getSlotClassNames(dateMeta, context.theme) : internal_cjs.getDayClassNames(dateMeta, context.theme)), ], elAttrs: { colSpan: cell.colspan, 'data-date': dateEnv.formatIso(cell.date, { omitTime: !tDateProfile.isTimeScale, omitTimeZoneOffset: true, }), }, renderProps: renderProps, generatorName: "slotLabelContent", customGenerator: options.slotLabelContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotLabelClassNames, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, (InnerContent) => (preact_cjs.createElement("div", { className: "fc-timeline-slot-frame", style: { height: props.rowInnerHeight } }, preact_cjs.createElement(InnerContent, { elTag: "a", elClasses: [ 'fc-timeline-slot-cushion', 'fc-scrollgrid-sync-inner', props.isSticky && 'fc-sticky', ], elAttrs: this.buildCellNavLinkAttrs(context, cell.date, cell.rowUnit) }))))); } } function buildCellNavLinkAttrs(context, cellDate, rowUnit) { return (rowUnit && rowUnit !== 'time') ? internal_cjs.buildNavLinkAttrs(context, cellDate, rowUnit) : {}; } function renderInnerContent(renderProps) { return renderProps.text; } function refineRenderProps(input) { return { level: input.level, date: input.dateEnv.toDate(input.dateMarker), view: input.viewApi, text: input.text, }; } class TimelineHeaderRows extends internal_cjs.BaseComponent { render() { let { dateProfile, tDateProfile, rowInnerHeights, todayRange, nowDate } = this.props; let { cellRows } = tDateProfile; return (preact_cjs.createElement(preact_cjs.Fragment, null, cellRows.map((rowCells, rowLevel) => { let isLast = rowLevel === cellRows.length - 1; let isChrono = tDateProfile.isTimeScale && isLast; // the final row, with times? let classNames = [ 'fc-timeline-header-row', isChrono ? 'fc-timeline-header-row-chrono' : '', ]; return ( // eslint-disable-next-line react/no-array-index-key preact_cjs.createElement("tr", { key: rowLevel, className: classNames.join(' ') }, rowCells.map((cell) => (preact_cjs.createElement(TimelineHeaderTh, { key: cell.date.toISOString(), cell: cell, rowLevel: rowLevel, dateProfile: dateProfile, tDateProfile: tDateProfile, todayRange: todayRange, nowDate: nowDate, rowInnerHeight: rowInnerHeights && rowInnerHeights[rowLevel], isSticky: !isLast }))))); }))); } } class TimelineCoords { constructor(slatRootEl, // okay to expose? slatEls, dateProfile, tDateProfile, dateEnv, isRtl) { this.slatRootEl = slatRootEl; this.dateProfile = dateProfile; this.tDateProfile = tDateProfile; this.dateEnv = dateEnv; this.isRtl = isRtl; this.outerCoordCache = new internal_cjs.PositionCache(slatRootEl, slatEls, true, // isHorizontal false); // for the inner divs within the slats // used for event rendering and scrollTime, to disregard slat border this.innerCoordCache = new internal_cjs.PositionCache(slatRootEl, internal_cjs.findDirectChildren(slatEls, 'div'), true, // isHorizontal false); } isDateInRange(date) { return internal_cjs.rangeContainsMarker(this.dateProfile.currentRange, date); } // results range from negative width of area to 0 dateToCoord(date) { let { tDateProfile } = this; let snapCoverage = this.computeDateSnapCoverage(date); let slotCoverage = snapCoverage / tDateProfile.snapsPerSlot; let slotIndex = Math.floor(slotCoverage); slotIndex = Math.min(slotIndex, tDateProfile.slotCnt - 1); let partial = slotCoverage - slotIndex; let { innerCoordCache, outerCoordCache } = this; if (this.isRtl) { return outerCoordCache.originClientRect.width - (outerCoordCache.rights[slotIndex] - (innerCoordCache.getWidth(slotIndex) * partial)); } return (outerCoordCache.lefts[slotIndex] + (innerCoordCache.getWidth(slotIndex) * partial)); } rangeToCoords(range) { return { start: this.dateToCoord(range.start), end: this.dateToCoord(range.end), }; } durationToCoord(duration) { let { dateProfile, tDateProfile, dateEnv, isRtl } = this; let coord = 0; if (dateProfile) { let date = dateEnv.add(dateProfile.activeRange.start, duration); if (!tDateProfile.isTimeScale) { date = internal_cjs.startOfDay(date); } coord = this.dateToCoord(date); // hack to overcome the left borders of non-first slat if (!isRtl && coord) { coord += 1; } } return coord; } coordFromLeft(coord) { if (this.isRtl) { return this.outerCoordCache.originClientRect.width - coord; } return coord; } // returned value is between 0 and the number of snaps computeDateSnapCoverage(date) { return computeDateSnapCoverage(date, this.tDateProfile, this.dateEnv); } } // returned value is between 0 and the number of snaps function computeDateSnapCoverage(date, tDateProfile, dateEnv) { let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration); if (snapDiff < 0) { return 0; } if (snapDiff >= tDateProfile.snapDiffToIndex.length) { return tDateProfile.snapCnt; } let snapDiffInt = Math.floor(snapDiff); let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt]; if (internal_cjs.isInt(snapCoverage)) { // not an in-between value snapCoverage += snapDiff - snapDiffInt; // add the remainder } else { // a fractional value, meaning the date is not visible // always round up in this case. works for start AND end dates in a range. snapCoverage = Math.ceil(snapCoverage); } return snapCoverage; } function coordToCss(hcoord, isRtl) { if (hcoord === null) { return { left: '', right: '' }; } if (isRtl) { return { right: hcoord, left: '' }; } return { left: hcoord, right: '' }; } function coordsToCss(hcoords, isRtl) { if (!hcoords) { return { left: '', right: '' }; } if (isRtl) { return { right: hcoords.start, left: -hcoords.end }; } return { left: hcoords.start, right: -hcoords.end }; } class TimelineHeader extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.rootElRef = preact_cjs.createRef(); } render() { let { props, context } = this; // TODO: very repetitive // TODO: make part of tDateProfile? let { unit: timerUnit, value: timerUnitValue } = internal_cjs.greatestDurationDenominator(props.tDateProfile.slotDuration); // WORKAROUND: make ignore slatCoords when out of sync with dateProfile let slatCoords = props.slatCoords && props.slatCoords.dateProfile === props.dateProfile ? props.slatCoords : null; return (preact_cjs.createElement(internal_cjs.NowTimer, { unit: timerUnit, unitValue: timerUnitValue }, (nowDate, todayRange) => (preact_cjs.createElement("div", { className: "fc-timeline-header", ref: this.rootElRef }, preact_cjs.createElement("table", { "aria-hidden": true, className: "fc-scrollgrid-sync-table", style: { minWidth: props.tableMinWidth, width: props.clientWidth } }, props.tableColGroupNode, preact_cjs.createElement("tbody", null, preact_cjs.createElement(TimelineHeaderRows, { dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, nowDate: nowDate, todayRange: todayRange, rowInnerHeights: props.rowInnerHeights }))), context.options.nowIndicator && ( // need to have a container regardless of whether the current view has a visible now indicator // because apparently removal of the element resets the scroll for some reasons (issue #5351). // this issue doesn't happen for the timeline body however ( preact_cjs.createElement("div", { className: "fc-timeline-now-indicator-container" }, (slatCoords && slatCoords.isDateInRange(nowDate)) && (preact_cjs.createElement(internal_cjs.NowIndicatorContainer, { elClasses: ['fc-timeline-now-indicator-arrow'], elStyle: coordToCss(slatCoords.dateToCoord(nowDate), context.isRtl), isAxis: true, date: nowDate })))))))); } componentDidMount() { this.updateSize(); } componentDidUpdate() { this.updateSize(); } updateSize() { if (this.props.onMaxCushionWidth) { this.props.onMaxCushionWidth(this.computeMaxCushionWidth()); } } computeMaxCushionWidth() { return Math.max(...internal_cjs.findElements(this.rootElRef.current, '.fc-timeline-header-row:last-child .fc-timeline-slot-cushion').map((el) => el.getBoundingClientRect().width)); } } class TimelineSlatCell extends internal_cjs.BaseComponent { render() { let { props, context } = this; let { dateEnv, options, theme } = context; let { date, tDateProfile, isEm } = props; let dateMeta = internal_cjs.getDateMeta(props.date, props.todayRange, props.nowDate, props.dateProfile); let renderProps = Object.assign(Object.assign({ date: dateEnv.toDate(props.date) }, dateMeta), { view: context.viewApi }); return (preact_cjs.createElement(internal_cjs.ContentContainer, { elTag: "td", elRef: props.elRef, elClasses: [ 'fc-timeline-slot', 'fc-timeline-slot-lane', isEm && 'fc-timeline-slot-em', tDateProfile.isTimeScale ? (internal_cjs.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, props.date, tDateProfile.labelInterval)) ? 'fc-timeline-slot-major' : 'fc-timeline-slot-minor') : '', ...(props.isDay ? internal_cjs.getDayClassNames(dateMeta, theme) : internal_cjs.getSlotClassNames(dateMeta, theme)), ], elAttrs: { 'data-date': dateEnv.formatIso(date, { omitTimeZoneOffset: true, omitTime: !tDateProfile.isTimeScale, }), }, renderProps: renderProps, generatorName: "slotLaneContent", customGenerator: options.slotLaneContent, classNameGenerator: options.slotLaneClassNames, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }, (InnerContent) => (preact_cjs.createElement(InnerContent, { elTag: "div" })))); } } class TimelineSlatsBody extends internal_cjs.BaseComponent { render() { let { props } = this; let { tDateProfile, cellElRefs } = props; let { slotDates, isWeekStarts } = tDateProfile; let isDay = !tDateProfile.isTimeScale && !tDateProfile.largeUnit; return (preact_cjs.createElement("tbody", null, preact_cjs.createElement("tr", null, slotDates.map((slotDate, i) => { let key = slotDate.toISOString(); return (preact_cjs.createElement(TimelineSlatCell, { key: key, elRef: cellElRefs.createRef(key), date: slotDate, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isEm: isWeekStarts[i], isDay: isDay })); })))); } } class TimelineSlats extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.rootElRef = preact_cjs.createRef(); this.cellElRefs = new internal_cjs.RefMap(); this.handleScrollRequest = (request) => { let { onScrollLeftRequest } = this.props; let { coords } = this; if (onScrollLeftRequest && coords) { if (request.time) { let scrollLeft = coords.coordFromLeft(coords.durationToCoord(request.time)); onScrollLeftRequest(scrollLeft); } return true; } return null; // best? }; } render() { let { props, context } = this; return (preact_cjs.createElement("div", { className: "fc-timeline-slots", ref: this.rootElRef }, preact_cjs.createElement("table", { "aria-hidden": true, className: context.theme.getClass('table'), style: { minWidth: props.tableMinWidth, width: props.clientWidth, } }, props.tableColGroupNode, preact_cjs.createElement(TimelineSlatsBody, { cellElRefs: this.cellElRefs, dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange })))); } componentDidMount() { this.updateSizing(); this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest); } componentDidUpdate(prevProps) { this.updateSizing(); this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile); } componentWillUnmount() { this.scrollResponder.detach(); if (this.props.onCoords) { this.props.onCoords(null); } } updateSizing() { let { props, context } = this; if (props.clientWidth !== null && // is sizing stable? this.scrollResponder // ^it's possible to have clientWidth immediately after mount (when returning from print view), but w/o scrollResponder ) { let rootEl = this.rootElRef.current; if (rootEl.offsetWidth) { // not hidden by css this.coords = new TimelineCoords(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.tDateProfile.slotDates), props.dateProfile, props.tDateProfile, context.dateEnv, context.isRtl); if (props.onCoords) { props.onCoords(this.coords); } this.scrollResponder.update(false); // TODO: wouldn't have to do this if coords were in state } } } positionToHit(leftPosition) { let { outerCoordCache } = this.coords; let { dateEnv, isRtl } = this.context; let { tDateProfile } = this.props; let slatIndex = outerCoordCache.leftToIndex(leftPosition); if (slatIndex != null) { // somewhat similar to what TimeGrid does. consolidate? let slatWidth = outerCoordCache.getWidth(slatIndex); let partial = isRtl ? (outerCoordCache.rights[slatIndex] - leftPosition) / slatWidth : (leftPosition - outerCoordCache.lefts[slatIndex]) / slatWidth; let localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot); let start = dateEnv.add(tDateProfile.slotDates[slatIndex], internal_cjs.multiplyDuration(tDateProfile.snapDuration, localSnapIndex)); let end = dateEnv.add(start, tDateProfile.snapDuration); return { dateSpan: { range: { start, end }, allDay: !this.props.tDateProfile.isTimeScale, }, dayEl: this.cellElRefs.currentMap[slatIndex], left: outerCoordCache.lefts[slatIndex], right: outerCoordCache.rights[slatIndex], }; } return null; } } function collectCellEls(elMap, slotDates) { return slotDates.map((slotDate) => { let key = slotDate.toISOString(); return elMap[key]; }); } function computeSegHCoords(segs, minWidth, timelineCoords) { let hcoords = []; if (timelineCoords) { for (let seg of segs) { let res = timelineCoords.rangeToCoords(seg); let start = Math.round(res.start); // for barely-overlapping collisions let end = Math.round(res.end); // if (end - start < minWidth) { end = start + minWidth; } hcoords.push({ start, end }); } } return hcoords; } function computeFgSegPlacements(segs, segHCoords, // might not have for every seg eventInstanceHeights, // might not have for every seg moreLinkHeights, // might not have for every more-link strictOrder, maxStackCnt) { let segInputs = []; let crudePlacements = []; // when we don't know dims for (let i = 0; i < segs.length; i += 1) { let seg = segs[i]; let instanceId = seg.eventRange.instance.instanceId; let height = eventInstanceHeights[instanceId]; let hcoords = segHCoords[i]; if (height && hcoords) { segInputs.push({ index: i, span: hcoords, thickness: height, }); } else { crudePlacements.push({ seg, hcoords, top: null, }); } } let hierarchy = new internal_cjs.SegHierarchy(); if (strictOrder != null) { hierarchy.strictOrder = strictOrder; } if (maxStackCnt != null) { hierarchy.maxStackCnt = maxStackCnt; } let hiddenEntries = hierarchy.addSegs(segInputs); let hiddenPlacements = hiddenEntries.map((entry) => ({ seg: segs[entry.index], hcoords: entry.span, top: null, })); let hiddenGroups = internal_cjs.groupIntersectingEntries(hiddenEntries); let moreLinkInputs = []; let moreLinkCrudePlacements = []; const extractSeg = (entry) => segs[entry.index]; for (let i = 0; i < hiddenGroups.length; i += 1) { let hiddenGroup = hiddenGroups[i]; let sortedSegs = hiddenGroup.entries.map(extractSeg); let height = moreLinkHeights[internal_cjs.buildIsoString(internal_cjs.computeEarliestSegStart(sortedSegs))]; // not optimal :( if (height != null) { // NOTE: the hiddenGroup's spanStart/spanEnd are already computed by rangeToCoords. computed during input. moreLinkInputs.push({ index: segs.length + i, thickness: height, span: hiddenGroup.span, }); } else { moreLinkCrudePlacements.push({ seg: sortedSegs, hcoords: hiddenGroup.span, top: null, }); } } // add more-links into the hierarchy, but don't limit hierarchy.maxStackCnt = -1; hierarchy.addSegs(moreLinkInputs); let visibleRects = hierarchy.toRects(); let visiblePlacements = []; let maxHeight = 0; for (let rect of visibleRects) { let segIndex = rect.index; visiblePlacements.push({ seg: segIndex < segs.length ? segs[segIndex] // a real seg : hiddenGroups[segIndex - segs.length].entries.map(extractSeg), hcoords: rect.span, top: rect.levelCoord, }); maxHeight = Math.max(maxHeight, rect.levelCoord + rect.thickness); } return [ visiblePlacements.concat(crudePlacements, hiddenPlacements, moreLinkCrudePlacements), maxHeight, ]; } class TimelineLaneBg extends internal_cjs.BaseComponent { render() { let { props } = this; let highlightSeg = [].concat(props.eventResizeSegs, props.dateSelectionSegs); return props.timelineCoords && (preact_cjs.createElement("div", { className: "fc-timeline-bg" }, this.renderSegs(props.businessHourSegs || [], props.timelineCoords, 'non-business'), this.renderSegs(props.bgEventSegs || [], props.timelineCoords, 'bg-event'), this.renderSegs(highlightSeg, props.timelineCoords, 'highlight'))); } renderSegs(segs, timelineCoords, fillType) { let { todayRange, nowDate } = this.props; let { isRtl } = this.context; let segHCoords = computeSegHCoords(segs, 0, timelineCoords); let children = segs.map((seg, i) => { let hcoords = segHCoords[i]; let hStyle = coordsToCss(hcoords, isRtl); return (preact_cjs.createElement("div", { key: internal_cjs.buildEventRangeKey(seg.eventRange), className: "fc-timeline-bg-harness", style: hStyle }, fillType === 'bg-event' ? preact_cjs.createElement(internal_cjs.BgEvent, Object.assign({ seg: seg }, internal_cjs.getSegMeta(seg, todayRange, nowDate))) : internal_cjs.renderFill(fillType))); }); return preact_cjs.createElement(preact_cjs.Fragment, null, children); } } class TimelineLaneSlicer extends internal_cjs.Slicer { sliceRange(origRange, dateProfile, dateProfileGenerator, tDateProfile, dateEnv) { let normalRange = normalizeRange(origRange, tDateProfile, dateEnv); let segs = []; // protect against when the span is entirely in an invalid date region if (computeDateSnapCoverage(normalRange.start, tDateProfile, dateEnv) < computeDateSnapCoverage(normalRange.end, tDateProfile, dateEnv)) { // intersect the footprint's range with the grid's range let slicedRange = internal_cjs.intersectRanges(normalRange, tDateProfile.normalizedRange); if (slicedRange) { segs.push({ start: slicedRange.start, end: slicedRange.end, isStart: slicedRange.start.valueOf() === normalRange.start.valueOf() && isValidDate(slicedRange.start, tDateProfile, dateProfile, dateProfileGenerator), isEnd: slicedRange.end.valueOf() === normalRange.end.valueOf() && isValidDate(internal_cjs.addMs(slicedRange.end, -1), tDateProfile, dateProfile, dateProfileGenerator), }); } } return segs; } } const DEFAULT_TIME_FORMAT = internal_cjs.createFormatter({ hour: 'numeric', minute: '2-digit', omitZeroMinute: true, meridiem: 'narrow', }); class TimelineEvent extends internal_cjs.BaseComponent { render() { let { props } = this; return (preact_cjs.createElement(internal_cjs.StandardEvent, Object.assign({}, props, { elClasses: ['fc-timeline-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TIME_FORMAT, defaultDisplayEventTime: !props.isTimeScale }))); } } class TimelineLaneMoreLink extends internal_cjs.BaseComponent { render() { let { props, context } = this; let { hiddenSegs, placement, resourceId } = props; let { top, hcoords } = placement; let isVisible = hcoords && top !== null; let hStyle = coordsToCss(hcoords, context.isRtl); let extraDateSpan = resourceId ? { resourceId } : {}; return (preact_cjs.createElement(internal_cjs.MoreLinkContainer, { elRef: props.elRef, elClasses: ['fc-timeline-more-link'], elStyle: Object.assign({ visibility: isVisible ? '' : 'hidden', top: top || 0 }, hStyle), allDayDate: null, moreCnt: hiddenSegs.length, allSegs: hiddenSegs, hiddenSegs: hiddenSegs, dateProfile: props.dateProfile, todayRange: props.todayRange, extraDateSpan: extraDateSpan, popoverContent: () => (preact_cjs.createElement(preact_cjs.Fragment, null, hiddenSegs.map((seg) => { let instanceId = seg.eventRange.instance.instanceId; return (preact_cjs.createElement("div", { key: instanceId, style: { visibility: props.isForcedInvisible[instanceId] ? 'hidden' : '' } }, preact_cjs.createElement(TimelineEvent, Object.assign({ isTimeScale: props.isTimeScale, seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === props.eventSelection }, internal_cjs.getSegMeta(seg, props.todayRange, props.nowDate))))); }))) }, (InnerContent) => (preact_cjs.createElement(InnerContent, { elTag: "div", elClasses: ['fc-timeline-more-link-inner', 'fc-sticky'] })))); } } class TimelineLane extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.slicer = new TimelineLaneSlicer(); this.sortEventSegs = internal_cjs.memoize(internal_cjs.sortEventSegs); this.harnessElRefs = new internal_cjs.RefMap(); this.moreElRefs = new internal_cjs.RefMap(); this.innerElRef = preact_cjs.createRef(); // TODO: memoize event positioning this.state = { eventInstanceHeights: {}, moreLinkHeights: {}, }; this.handleResize = (isForced) => { if (isForced) { this.updateSize(); } }; } render() { let { props, state, context } = this; let { options } = context; let { dateProfile, tDateProfile } = props; let slicedProps = this.slicer.sliceProps(props, dateProfile, tDateProfile.isTimeScale ? null : props.nextDayThreshold, context, // wish we didn't have to pass in the rest of the args... dateProfile, context.dateProfileGenerator, tDateProfile, context.dateEnv); let mirrorSegs = (slicedProps.eventDrag ? slicedProps.eventDrag.segs : null) || (slicedProps.eventResize ? slicedProps.eventResize.segs : null) || []; let fgSegs = this.sortEventSegs(slicedProps.fgEventSegs, options.eventOrder); let fgSegHCoords = computeSegHCoords(fgSegs, options.eventMinWidth, props.timelineCoords); let [fgPlacements, fgHeight] = computeFgSegPlacements(fgSegs, fgSegHCoords, state.eventInstanceHeights, state.moreLinkHeights, options.eventOrderStrict, options.eventMaxStack); let isForcedInvisible = // TODO: more convenient (slicedProps.eventDrag ? slicedProps.eventDrag.affectedInstances : null) || (slicedProps.eventResize ? slicedProps.eventResize.affectedInstances : null) || {}; return (preact_cjs.createElement(preact_cjs.Fragment, null, preact_cjs.createElement(TimelineLaneBg, { businessHourSegs: slicedProps.businessHourSegs, bgEventSegs: slicedProps.bgEventSegs, timelineCoords: props.timelineCoords, eventResizeSegs: slicedProps.eventResize ? slicedProps.eventResize.segs : [] /* bad new empty array? */, dateSelectionSegs: slicedProps.dateSelectionSegs, nowDate: props.nowDate, todayRange: props.todayRange }), preact_cjs.createElement("div", { className: "fc-timeline-events fc-scrollgrid-sync-inner", ref: this.innerElRef, style: { height: fgHeight } }, this.renderFgSegs(fgPlacements, isForcedInvisible, false, false, false), this.renderFgSegs(buildMirrorPlacements(mirrorSegs, props.timelineCoords, fgPlacements), {}, Boolean(slicedProps.eventDrag), Boolean(slicedProps.eventResize), false)))); } componentDidMount() { this.updateSize(); this.context.addResizeHandler(this.handleResize); } componentDidUpdate(prevProps, prevState) { if (prevProps.eventStore !== this.props.eventStore || // external thing changed? prevProps.timelineCoords !== this.props.timelineCoords || // external thing changed? prevState.moreLinkHeights !== this.state.moreLinkHeights // HACK. see addStateEquality ) { this.updateSize(); } } componentWillUnmount() { this.context.removeResizeHandler(this.handleResize); } updateSize() { let { props } = this; let { timelineCoords } = props; const innerEl = this.innerElRef.current; if (props.onHeightChange) { props.onHeightChange(innerEl, false); } if (timelineCoords) { this.setState({ eventInstanceHeights: internal_cjs.mapHash(this.harnessElRefs.currentMap, (harnessEl) => (Math.round(harnessEl.getBoundingClientRect().height))), moreLinkHeights: internal_cjs.mapHash(this.moreElRefs.currentMap, (moreEl) => (Math.round(moreEl.getBoundingClientRect().height))), }, () => { if (props.onHeightChange) { props.onHeightChange(innerEl, true); } }); } // hack if (props.syncParentMinHeight) { innerEl.parentElement.style.minHeight = innerEl.style.height; } } renderFgSegs(segPlacements, isForcedInvisible, isDragging, isResizing, isDateSelecting) { let { harnessElRefs, moreElRefs, props, context } = this; let isMirror = isDragging || isResizing || isDateSelecting; return (preact_cjs.createElement(preact_cjs.Fragment, null, segPlacements.map((segPlacement) => { let { seg, hcoords, top } = segPlacement; if (Array.isArray(seg)) { // a more-link let isoStr = internal_cjs.buildIsoString(internal_cjs.computeEarliestSegStart(seg)); return (preact_cjs.createElement(TimelineLaneMoreLink, { key: 'm:' + isoStr /* "m" for "more" */, elRef: moreElRefs.createRef(isoStr), hiddenSegs: seg, placement: segPlacement, dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isTimeScale: props.tDateProfile.isTimeScale, eventSelection: props.eventSelection, resourceId: props.resourceId, isForcedInvisible: isForcedInvisible })); } let instanceId = seg.eventRange.instance.instanceId; let isVisible =