@fullcalendar/timeline
Version:
Display events on a horizontal time axis (without resources)
1,068 lines (1,052 loc) • 62.9 kB
JavaScript
'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 =