@fullcalendar/web-component
Version:
Custom Element for FullCalendar
1,066 lines (1,052 loc) • 101 kB
JavaScript
import { addDays, addMs, intersectRanges, formatDayString, joinDateTimeFormatParts, diffDays, buildIsoString, addWeeks, diffWeeks } from '@full-ui/headless-calendar';
import { g as guid, c as createFormatter, F as classNames, $ as expandRecurring, s as sliceEventStore, ao as fabricateEventRange, ak as sortEventSegs, al as getEventRangeMeta, an as getEventKey, v as EventImpl, S as setElEventRange, D as formatWithOrdinals, Q as createAriaClickAttrs, ah as getEventTargetViaRoot, R as computeElIsRtl, aa as getAppendableRoot, ad as applyStyle } from './69b11357.js';
import { B as BaseComponent, p as watchWidth, s as setRef, o as DateComponent, a as memoize, d as getIsHeightAuto, f as afterSize, q as getDateMeta, t as buildDateStr, v as buildNavLinkAttrs, W as WEEKDAY_ONLY_FORMAT, C as ContentContainer, u as renderText$1, x as StandardEvent, w as watchHeight, r as watchSize, g as generateClassName, y as findWeekdayText, z as findDayNumberText, b as memoizeObjArg, A as isDimsEqual, E as findMonthText, V as ViewContextType } from './fa7e597f.js';
import { c as computeMajorUnit, i as isMajorUnit, D as DateProfileGenerator } from './3d9e7be7.js';
import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
import { createRef, Component, createPortal } from 'preact/compat';
import { j as joinClassNames, f as fracToCssDim } from './423b7bc6.js';
import { f as computeClippedClientRect } from './cf1edde2.js';
class Slicer {
constructor() {
this.sliceBusinessHours = memoize(this._sliceBusinessHours);
this.sliceDateSelection = memoize(this._sliceDateSpan);
this.sliceEventStore = memoize(this._sliceEventStore);
this.sliceEventDrag = memoize(this._sliceInteraction);
this.sliceEventResize = memoize(this._sliceInteraction);
this.forceDayIfListItem = false; // hack
}
sliceProps(props, dateProfile, nextDayThreshold, context, ...extraArgs) {
let { eventUiBases } = props;
let eventSegs = this.sliceEventStore(props.eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs);
return {
dateSelectionSegs: this.sliceDateSelection(props.dateSelection, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs),
businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, context, ...extraArgs),
fgEventSegs: eventSegs.fg,
bgEventSegs: eventSegs.bg,
eventDrag: this.sliceEventDrag(props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
eventResize: this.sliceEventResize(props.eventResize, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
eventSelection: props.eventSelection,
}; // TODO: give interactionSegs?
}
sliceNowDate(// does not memoize
date, dateProfile, nextDayThreshold, context, ...extraArgs) {
return this._sliceDateSpan({ range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range
dateProfile, nextDayThreshold, {}, context, ...extraArgs);
}
_sliceBusinessHours(businessHours, dateProfile, nextDayThreshold, context, ...extraArgs) {
if (!businessHours) {
return [];
}
return this._sliceEventStore(expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), context), {}, dateProfile, nextDayThreshold, ...extraArgs).bg;
}
_sliceEventStore(eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
if (eventStore) {
let rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
bg: this.sliceEventRanges(rangeRes.bg, extraArgs),
fg: this.sliceEventRanges(rangeRes.fg, extraArgs),
};
}
return { bg: [], fg: [] };
}
_sliceInteraction(interaction, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
if (!interaction) {
return null;
}
let rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
segs: this.sliceEventRanges(rangeRes.fg, extraArgs),
affectedInstances: interaction.affectedEvents.instances,
isEvent: interaction.isEvent,
};
}
_sliceDateSpan(dateSpan, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs) {
if (!dateSpan) {
return [];
}
let activeRange = computeActiveRange(dateProfile, Boolean(nextDayThreshold));
let activeDateSpanRange = intersectRanges(dateSpan.range, activeRange);
if (activeDateSpanRange) {
dateSpan = { ...dateSpan, range: activeDateSpanRange };
let eventRange = fabricateEventRange(dateSpan, eventUiBases, context);
let segs = this.sliceRange(dateSpan.range, ...extraArgs);
for (let seg of segs) {
seg.eventRange = eventRange;
}
return segs;
}
return [];
}
/*
"complete" seg means it has component and eventRange
*/
sliceEventRanges(eventRanges, extraArgs) {
let segs = [];
for (let eventRange of eventRanges) {
segs.push(...this.sliceEventRange(eventRange, extraArgs));
}
return segs;
}
/*
"complete" seg means it has component and eventRange
*/
sliceEventRange(eventRange, extraArgs) {
let dateRange = eventRange.range;
// hack to make multi-day events that are being force-displayed as list-items to take up only one day
if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') {
dateRange = {
start: dateRange.start,
end: addDays(dateRange.start, 1),
};
}
let segs = this.sliceRange(dateRange, ...extraArgs); // !!!
for (let seg of segs) {
seg.eventRange = eventRange;
seg.isStart = eventRange.isStart && seg.isStart;
seg.isEnd = eventRange.isEnd && seg.isEnd;
}
return segs;
}
}
/*
for incorporating slotMinTime/slotMaxTime if appropriate
TODO: should be part of DateProfile!
TimelineDateProfile already does this btw
*/
function computeActiveRange(dateProfile, isComponentAllDay) {
let range = dateProfile.activeRange;
if (isComponentAllDay) {
return range;
}
return {
start: addMs(range.start, dateProfile.slotMinTime.milliseconds),
end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day
};
}
class DayTableSlicer extends Slicer {
constructor() {
super(...arguments);
this.forceDayIfListItem = true;
}
sliceRange(dateRange, dayTableModel) {
return dayTableModel.sliceRange(dateRange);
}
}
// TODO: converge types with DayTableCell and DayCellContainer (the component) and refineRenderProps
// the generation of DayTableCell will be distinct (for the BODY cells)
// but can share some of the same types/utils
// Date Cells
// -------------------------------------------------------------------------------------------------
const firstSunday = new Date(259200000);
function buildDateRowConfigs(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
context) {
const rowConfig = buildDateRowConfig(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context);
const majorUnit = computeMajorUnit(dateProfile, context.dateEnv);
// HACK mutate isMajor
// Skip 'day' majorUnit: when each header cell IS a day, every cell would match,
// so there's no meaningful boundary to highlight (unlike timeline slots which can be sub-day).
if (datesRepDistinctDays && majorUnit !== 'day') {
for (const dataConfig of rowConfig.dataConfigs) {
if (isMajorUnit(dataConfig.dateMarker, majorUnit, context.dateEnv)) {
dataConfig.renderProps.isMajor = true;
}
}
}
return [rowConfig];
}
/*
Should this receive resource data attributes?
Or ResourceApi object itself?
*/
function buildDateRowConfig(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
context, colSpan, isMajorMod) {
return {
isDateRow: true,
renderConfig: buildDateRenderConfig(dayHeaderFormat, datesRepDistinctDays, context),
dataConfigs: buildDateDataConfigs(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context, colSpan, undefined, undefined, undefined, undefined, isMajorMod)
};
}
/*
For header cells: how to connect w/ custom rendering
Applies to all cells in a row
*/
function buildDateRenderConfig(dayHeaderFormat, datesRepDistinctDays, context) {
const { options } = context;
return {
generatorName: 'dayHeaderContent',
customGenerator: options.dayHeaderContent,
classNameGenerator: options.dayHeaderClass,
innerClassNameGenerator: options.dayHeaderInnerClass,
didMount: options.dayHeaderDidMount,
willUnmount: options.dayHeaderWillUnmount,
align: options.dayHeaderAlign,
sticky: options._dayHeaderSticky,
dayHeaderFormat,
datesRepDistinctDays,
};
}
const dowDates = [];
for (let dow = 0; dow < 7; dow++) {
dowDates.push(addDays(new Date(259200000), dow)); // start with Sun, 04 Jan 1970 00:00:00 GMT)
}
/*
For header cells: data
*/
function buildDateDataConfigs(dateMarkers, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat?
context, colSpan = 1, keyPrefix = '', extraRenderProps = {}, // TODO
extraAttrs = {}, // TODO
className = '', isMajorMod) {
const { dateEnv, viewApi, options } = context;
return datesRepDistinctDays
? dateMarkers.map((dateMarker, i) => {
const dateMeta = getDateMeta(dateMarker, dateEnv, dateProfile, todayRange);
const isMajor = isMajorMod != null && !(i % isMajorMod);
const hasNavLink = options.navLinks && !dateMeta.isDisabled &&
dateMarkers.length > 1; // don't show navlink to day if only one day
const renderProps = {
...dateMeta,
...extraRenderProps,
isMajor,
isSticky: false, // HACK. gets overridden
inPopover: false,
hasNavLink,
view: viewApi,
};
const fullDateStr = buildDateStr(context, dateMarker);
// for DayGridHeaderCell
return {
key: keyPrefix + dateMarker.toUTCString(),
dateMarker,
renderProps,
attrs: {
'aria-label': fullDateStr,
...(dateMeta.isToday ? { 'aria-current': 'date' } : {}), // TODO: assign undefined for nonexistent
'data-date': formatDayString(dateMarker),
...extraAttrs,
},
// for navlink
innerAttrs: hasNavLink
? buildNavLinkAttrs(context, dateMarker, undefined, fullDateStr)
: { 'aria-hidden': true }, // label already on cell
colSpan,
hasNavLink,
className,
};
})
: dateMarkers.map((dateMarker, i) => {
const dow = dateMarker.getUTCDay();
const normDate = addDays(firstSunday, dow);
const dateMeta = {
date: dateEnv.toDate(dateMarker),
dow,
isDisabled: false,
isFuture: false,
isPast: false,
isToday: false,
isOther: false,
};
const isMajor = isMajorMod != null && !(i % isMajorMod);
const renderProps = {
...dateMeta,
date: dowDates[dow],
isMajor,
isSticky: false, // HACK. gets overridden
inPopover: false,
hasNavLink: false,
view: viewApi,
...extraRenderProps,
};
const fullWeekDayStr = joinDateTimeFormatParts(dateEnv.formatToParts(normDate, WEEKDAY_ONLY_FORMAT));
// for DayGridHeaderCell
return {
key: keyPrefix + String(dow),
dateMarker,
renderProps,
attrs: {
'aria-label': fullWeekDayStr,
...extraAttrs,
},
// NOT a navlink
innerAttrs: {
'aria-hidden': true, // label already on cell
},
colSpan,
className,
};
});
}
/*
TODO: make API where createRefMap() called
*/
class RefMap {
constructor(masterCallback, ignoreDeletes = false) {
this.masterCallback = masterCallback;
this.ignoreDeletes = ignoreDeletes;
this.rev = '';
this.current = new Map();
this.callbacks = new Map;
this.handleValue = (val, key) => {
let { current, callbacks } = this;
if (val === null) {
if (!this.ignoreDeletes) {
current.delete(key);
callbacks.delete(key);
}
}
else {
current.set(key, val);
}
this.rev = guid();
if (this.masterCallback) {
this.masterCallback(val, key);
}
};
}
createRef(key) {
let refCallback = this.callbacks.get(key);
if (!refCallback) {
refCallback = (val) => {
this.handleValue(val, key);
};
this.callbacks.set(key, refCallback);
}
return refCallback;
}
}
class Ruler extends BaseComponent {
constructor() {
super(...arguments);
this.elRef = createRef();
}
render() {
return (jsx("div", { ref: this.elRef }));
}
componentDidMount() {
this._isUnmounting = false;
const { props } = this;
const el = this.elRef.current;
this.disconnectWidth = watchWidth(el, (width) => {
if (this._isUnmounting)
return;
setRef(props.widthRef, width);
});
}
componentWillUnmount() {
this._isUnmounting = true;
this.disconnectWidth();
const { props } = this;
if (props.widthRef) {
setRef(props.widthRef, null);
}
}
}
/*
We need really specific keys because RefMap::createRef() which is then given to heightRef
unable to change key! As a result, we cannot reuse elements between normal/slice/standin types,
but that's okay since they render quite differently
*/
function getEventPartKey(seg) {
return getEventKey(seg) + ':' + seg.start +
(seg.standinFor ? ':standin' : seg.isSlice ? ':slice' : '');
}
// DayGridRange utils (TODO: move)
// -------------------------------------------------------------------------------------------------
function splitSegsByRow(segs, rowCount) {
const byRow = [];
for (let row = 0; row < rowCount; row++) {
byRow[row] = [];
}
for (const seg of segs) {
byRow[seg.row].push(seg);
}
return byRow;
}
function splitInteractionByRow(ui, rowCount) {
const byRow = [];
if (!ui) {
for (let row = 0; row < rowCount; row++) {
byRow[row] = null;
}
}
else {
for (let row = 0; row < rowCount; row++) {
byRow[row] = {
affectedInstances: ui.affectedInstances,
isEvent: ui.isEvent,
segs: [],
};
}
for (const seg of ui.segs) {
byRow[seg.row].segs.push(seg);
}
}
return byRow;
}
function sliceSegForCol(seg, col) {
return {
...seg,
start: col,
end: col + 1,
isStart: seg.isStart && seg.start === col,
isEnd: seg.isEnd && seg.end - 1 === col,
standinFor: seg,
};
}
class BgEvent extends BaseComponent {
constructor() {
super(...arguments);
// memo
this.buildPublicEvent = memoize((context, eventDef, eventInstance) => new EventImpl(context, eventDef, eventInstance));
this.handleEl = (el) => {
this.el = el;
if (el) {
setElEventRange(el, this.props.eventRange);
}
};
}
render() {
const { props, context } = this;
const { eventRange } = props;
const { options } = context;
const eventUi = eventRange.ui;
const eventApi = this.buildPublicEvent(context, eventRange.def, eventRange.instance);
const subcontentRenderProps = {
event: eventApi,
isNarrow: props.isNarrow || false,
isShort: props.isShort || false,
};
const renderProps = {
event: eventApi,
view: context.viewApi,
timeText: '', // never display time
color: eventUi.color || options.backgroundEventColor,
contrastColor: eventUi.contrastColor,
isDraggable: false,
isStartResizable: false,
isEndResizable: false,
isMirror: false,
isStart: props.isStart,
isEnd: props.isEnd,
isFirst: false,
isLast: false,
isPast: props.isPast,
isFuture: props.isFuture,
isToday: props.isToday,
isSelected: false,
isDragging: false,
isResizing: false,
isInteractive: false,
level: 0,
isNarrow: props.isNarrow || false,
isShort: props.isShort || false,
timeClass: '', // never display time
titleClass: generateClassName(options.backgroundEventTitleClass, subcontentRenderProps),
options: { eventOverlap: Boolean(options.eventOverlap) },
};
// does not include backgroundEventClass.. added below
const outerClassName = joinClassNames(eventUi.className, classNames.fill, classNames.internalEvent, classNames.internalBgEvent, props.isVertical ? classNames.flexCol : classNames.flexRow);
const innerClassName = joinClassNames(generateClassName(options.backgroundEventInnerClass, renderProps), classNames.liquid);
return (jsx(ContentContainer, { tag: 'div', className: outerClassName, style: {
'--fc-event-color': renderProps.color,
'--fc-event-contrast-color': renderProps.contrastColor,
}, defaultGenerator: renderInnerContent, elRef: this.handleEl, renderProps: renderProps, generatorName: "backgroundEventContent", customGenerator: options.backgroundEventContent, classNameGenerator: options.backgroundEventClass, didMount: options.backgroundEventDidMount, willUnmount: options.backgroundEventWillUnmount, children: (InnerContent) => (jsx(InnerContent, { tag: 'div', className: innerClassName })) }));
}
componentDidUpdate(prevProps) {
if (this.el && this.props.eventRange !== prevProps.eventRange) {
setElEventRange(this.el, this.props.eventRange);
}
}
}
function renderInnerContent(props) {
let { title } = props.event;
return title && (jsx("div", { className: props.titleClass, children: props.event.title }));
}
// Other types of fills
// -------------------------------------------------------------------------------------------------
function renderFill(fillType, options) {
return (jsx("div", { className: joinClassNames(fillType === 'non-business' ? options.nonBusinessHoursClass :
fillType === 'highlight' ? options.highlightClass : undefined, classNames.fill) }));
}
const SPACE_FROM_VIEWPORT = 10;
const ROW_BORDER_WIDTH = 1;
class MorePopover extends DateComponent {
constructor() {
super(...arguments);
// memo
this.getDateMeta = memoize(getDateMeta);
this.closeRef = createRef();
this.focusStartRef = createRef();
this.focusEndRef = createRef();
this.handleRootEl = (rootEl) => {
this.rootEl = rootEl;
if (rootEl) {
this.context.registerInteractiveComponent(this, {
el: rootEl,
useEventCenter: false,
});
}
else {
this.context.unregisterInteractiveComponent(this);
}
};
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
this.handleDocumentMouseDown = (ev) => {
// only hide the popover if the click happened outside the popover
const target = getEventTargetViaRoot(ev);
if (!this.rootEl.contains(target)) {
this.handleClose();
}
};
this.handleDocumentKeyDown = (ev) => {
if (ev.key === 'Escape') {
this.handleClose();
}
};
// for many different close techniques
// cannot accept params because might receive a browser Event
this.handleClose = () => {
let { onClose } = this.props;
if (onClose) {
onClose();
}
};
}
render() {
let { props, context } = this;
let { options, dateEnv, viewApi } = context;
let { startDate, todayRange, dateProfile } = props;
let dateMeta = this.getDateMeta(startDate, dateEnv, dateProfile, todayRange);
let textParts = dateEnv.formatToParts(startDate, options.popoverFormat);
let text = joinDateTimeFormatParts(textParts);
const dayHeaderRenderProps = {
...dateMeta,
isMajor: false,
isNarrow: false,
isSticky: false,
inPopover: true,
level: 0,
hasNavLink: false,
text,
textParts,
get weekdayText() { return findWeekdayText(textParts); },
get dayNumberText() { return findDayNumberText(textParts); },
view: viewApi,
// TODO: should know about the resource!
};
const dayCellRenderProps = {
...dateMeta,
isMajor: false,
isNarrow: false,
inPopover: true,
hasNavLink: false,
get weekdayText() { return findWeekdayText(textParts); },
get dayNumberText() { return findDayNumberText(textParts); },
get monthText() { return findMonthText(textParts); },
view: viewApi,
text: '',
textParts: [],
options: { businessHours: Boolean(options.businessHours) },
};
const fullDateStr = formatDayString(startDate);
/*
TODO: DRY with TimelineHeaderCell
*/
const { dayHeaderAlign } = options;
const align = typeof dayHeaderAlign === 'function'
? dayHeaderAlign({ level: 0, inPopover: true, isNarrow: false })
: dayHeaderAlign;
const isRtl = computeElIsRtl(props.alignEl);
return createPortal(jsxs("div", { "data-date": fullDateStr, id: props.id, role: 'dialog', "aria-labelledby": props.titleId, className: joinClassNames(options.popoverClass, classNames.flexCol, classNames.popoverZ, classNames.abs, classNames.borderBoxRoot, classNames.internalPopover), style: {
// positioning is mutated directly in updateSize, HOWEVER, we don't want popover to start
// low on screen because might cause unnecessary scrollbars
top: 0,
left: 0,
},
// HACK because of portal
dir: isRtl ? 'rtl' : undefined, "data-color-scheme": options.colorScheme || undefined, ref: this.handleRootEl, children: [jsx("div", { tabIndex: 0, style: { outline: 'none' }, ref: this.focusStartRef }), jsxs("div", { className: joinClassNames(generateClassName(options.dayHeaderClass, dayHeaderRenderProps), classNames.flexCol, classNames.borderOnlyB, align === 'center' ? classNames.alignCenter :
align === 'end' ? classNames.alignEnd :
classNames.alignStart), children: [jsx("div", { children: jsx(ContentContainer, { tag: "div", attrs: {
id: props.titleId,
// NOTE: more-popover never has nav-links
}, generatorName: "dayHeaderContent", renderProps: dayHeaderRenderProps, customGenerator: options.dayHeaderContent, defaultGenerator: renderText, classNameGenerator: options.dayHeaderInnerClass, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }) }), jsx(ContentContainer, { tag: 'button', attrs: {
'aria-label': options.closeHint,
...createAriaClickAttrs(this.handleClose)
}, elRef: this.closeRef, className: joinClassNames(options.popoverCloseClass, classNames.flexRow, classNames.cursorPointer), renderProps: {}, customGenerator: options.popoverCloseContent, generatorName: 'popoverCloseContent' })] }), jsx("div", { className: joinClassNames(generateClassName(options.dayCellClass, dayCellRenderProps), classNames.flexCol, classNames.borderNone), children: jsx("div", { className: generateClassName(options.dayCellInnerClass, dayCellRenderProps), children: props.children }) }), jsx("div", { tabIndex: 0, style: { outline: 'none' }, ref: this.focusEndRef })] }), getAppendableRoot(props.alignEl));
}
queryHit(isRtl, positionLeft, positionTop, elWidth, elHeight) {
let { rootEl, props } = this;
if (positionLeft >= 0 && positionLeft < elWidth &&
positionTop >= 0 && positionTop < elHeight) {
return {
dateProfile: props.dateProfile,
dateSpan: {
allDay: !props.forceTimed,
range: {
start: props.startDate,
end: props.endDate,
},
...props.dateSpanProps,
},
getDayEl: () => rootEl,
rect: {
left: 0,
top: 0,
right: elWidth,
bottom: elHeight,
},
layer: 1, // important when comparing with hits from other components
};
}
return null;
}
componentDidMount() {
document.addEventListener('mousedown', this.handleDocumentMouseDown);
document.addEventListener('keydown', this.handleDocumentKeyDown);
this.focusStartRef.current.addEventListener('focus', this.handleClose);
this.focusEndRef.current.addEventListener('focus', this.handleClose);
this.closeRef.current.focus({ preventScroll: true });
this.updateSize();
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
document.removeEventListener('keydown', this.handleDocumentKeyDown);
this.focusStartRef.current.removeEventListener('focus', this.handleClose);
this.focusEndRef.current.removeEventListener('focus', this.handleClose);
}
updateSize() {
let { alignEl, alignParentTop } = this.props;
let { rootEl: popoverEl } = this;
const isRtl = computeElIsRtl(alignEl);
// position relative to viewport
const alignmentRect = computeClippedClientRect(alignEl);
if (alignmentRect) {
let popoverDims = popoverEl.getBoundingClientRect();
// position relative to viewport
let popoverVPTop = alignParentTop
// HACK: subtract 1 for DayGrid, which has borders on row-bottom. Only view that uses alignParentTop
? alignEl.closest(alignParentTop).getBoundingClientRect().top - ROW_BORDER_WIDTH
: alignmentRect.top;
let popoverVPLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left;
// constrain
popoverVPTop = Math.max(popoverVPTop, SPACE_FROM_VIEWPORT);
popoverVPLeft = Math.min(popoverVPLeft, document.documentElement.clientWidth - SPACE_FROM_VIEWPORT - popoverDims.width);
popoverVPLeft = Math.max(popoverVPLeft, SPACE_FROM_VIEWPORT);
const { offsetParent } = popoverEl;
// final popover position, relative to offsetParent
let top;
let left;
// TODO: account for RTL
if (!offsetParent || offsetParent === document.body) {
top = popoverVPTop + window.scrollY;
left = popoverVPLeft + window.scrollX;
}
else {
const offsetParentRect = offsetParent.getBoundingClientRect();
top = popoverVPTop - offsetParentRect.top + offsetParent.scrollTop;
left = popoverVPLeft - offsetParentRect.left + offsetParent.scrollLeft;
}
applyStyle(popoverEl, { top, left });
}
}
}
// TODO: DRY
function renderText(renderProps) {
return renderProps.text;
}
function doCoordRangesIntersect(r0, r1) {
return r0.end > r1.start && r0.start < r1.end;
}
function intersectCoordRanges(r0, r1) {
const start = Math.max(r0.start, r1.start);
const end = Math.min(r0.end, r1.end);
if (start < end) {
return {
start,
end,
isStart: r0.isStart && start === r0.start,
isEnd: r0.isEnd && end === r0.end,
};
}
}
function joinCoordRanges(r0, r1) {
return {
start: Math.min(r0.start, r1.start),
end: Math.max(r0.end, r1.end),
};
}
function getCoordRangeEnd(r) {
return r.end;
}
// { eventRange }
// -------------------------------------------------------------------------------------------------
function computeEarliestStart(segs) {
return segs.reduce(pickEarliestStart).eventRange.range.start;
}
function computeLatestEnd(segs) {
return segs.reduce(pickLatestEnd).eventRange.range.end;
}
function pickEarliestStart(r0, r1) {
return r0.eventRange.range.start < r1.eventRange.range.start ? r0 : r1;
}
function pickLatestEnd(r0, r1) {
return r0.eventRange.range.end > r1.eventRange.range.end ? r0 : r1;
}
/*
IMPORTANT: caller is responsible for injecting moreLinkInnerClass,
either on root `classNames` or within inner element
*/
class MoreLinkContainer extends BaseComponent {
constructor() {
super(...arguments);
this.state = {
isPopoverOpen: false,
};
this.handleLinkEl = (linkEl) => {
this.linkEl = linkEl;
if (this.props.elRef) {
setRef(this.props.elRef, linkEl);
}
};
this.handleClick = (ev) => {
let { props, context } = this;
let { dateEnv, options } = context;
let { moreLinkClick } = options;
let date = computeRange(props).start;
function buildPublicSeg(seg) {
let { def, instance, range } = seg.eventRange;
return {
event: new EventImpl(context, def, instance),
start: dateEnv.toDate(range.start),
end: dateEnv.toDate(range.end),
isStart: seg.isStart,
isEnd: seg.isEnd,
};
}
if (typeof moreLinkClick === 'function') {
moreLinkClick = moreLinkClick({
date: dateEnv.toDate(date),
allDay: Boolean(props.allDayDate),
allSegs: props.segs.map(buildPublicSeg),
hiddenSegs: props.hiddenSegs.map(buildPublicSeg),
jsEvent: ev,
view: context.viewApi,
});
}
if (!moreLinkClick || moreLinkClick === 'popover') {
this.setState({ isPopoverOpen: true });
}
else if (typeof moreLinkClick === 'string') { // a view name
context.calendarApi.zoomTo(date, moreLinkClick);
}
};
this.handlePopoverClose = () => {
if (this.linkEl) { // was null sometimes when initiating drag-n-drop would hide the popover
this.linkEl.focus();
}
this.setState({ isPopoverOpen: false });
};
}
render() {
let { props, state } = this;
return (jsx(ViewContextType.Consumer, { children: (context) => {
let { viewApi, options, calendarApi, baseId } = context;
let { moreLinkText } = options;
let moreCnt = props.hiddenSegs.length;
let range = computeRange(props);
let popoverId = baseId + 'popover-' + range.start.toISOString();
let numericText = `+${moreCnt}`; // TODO: offer hook or i18n?
let longText = typeof moreLinkText === 'function' // TODO: eventually use formatWithOrdinals
? moreLinkText.call(calendarApi, moreCnt)
: `${numericText} ${moreLinkText}`;
let hint = formatWithOrdinals(options.moreLinkHint, [moreCnt], longText);
let renderProps = {
num: moreCnt,
numericText,
longText,
text: (props.isMicro || props.display === 'column') ? numericText : longText,
isNarrow: props.isNarrow,
view: viewApi,
};
return (jsxs(Fragment, { children: [Boolean(moreCnt) && (jsx(ContentContainer, { tag: 'div', elRef: this.handleLinkEl, className: joinClassNames(generateClassName(// will added to moreLinkClass
props.display === 'row'
? options.rowMoreLinkClass // row
: options.columnMoreLinkClass, // column
renderProps), props.className, props.display === 'row'
? classNames.flexRow
: classNames.flexCol, classNames.internalMoreLink, classNames.cursorPointer), style: props.style, attrs: {
...props.attrs,
...createAriaClickAttrs(this.handleClick),
title: hint,
'role': 'button',
'aria-haspopup': 'dialog',
'aria-expanded': state.isPopoverOpen,
'aria-controls': state.isPopoverOpen ? popoverId : undefined,
}, renderProps: renderProps, generatorName: "moreLinkContent", customGenerator: options.moreLinkContent, defaultGenerator: renderMoreLinkText, classNameGenerator: options.moreLinkClass, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount, children: (InnerContent) => (jsx(InnerContent, { tag: 'div', className: joinClassNames(generateClassName(options.moreLinkInnerClass, renderProps), generateClassName(props.display === 'row'
? options.rowMoreLinkInnerClass // row
: options.columnMoreLinkInnerClass, // column
renderProps), props.display === 'row'
? classNames.stickyS
: classNames.stickyT) })) })), state.isPopoverOpen && (jsx(MorePopover, { id: popoverId, titleId: popoverId + '-title', startDate: range.start, endDate: range.end, dateProfile: props.dateProfile, todayRange: props.todayRange, dateSpanProps: props.dateSpanProps, alignEl: props.alignElRef ?
props.alignElRef.current :
this.linkEl, alignParentTop: props.alignParentTop, forceTimed: props.forceTimed, onClose: this.handlePopoverClose, children: props.popoverContent() }))] }));
} }));
}
}
function renderMoreLinkText(props) {
return props.text;
}
function computeRange(props) {
if (props.allDayDate) {
return {
start: props.allDayDate,
end: addDays(props.allDayDate, 1),
};
}
return {
start: computeEarliestStart(props.hiddenSegs),
end: computeLatestEnd(props.hiddenSegs),
};
}
const DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'narrow',
});
function hasListItemDisplay(seg) {
let { display } = seg.eventRange.ui;
return display === 'list-item' || (display === 'auto' &&
!seg.eventRange.def.allDay &&
(seg.end - seg.start) === 1 && // single-day
seg.isStart && // "
seg.isEnd // "
);
}
class DayGridMoreLink extends BaseComponent {
render() {
let { props } = this;
return (jsx(MoreLinkContainer, { display: 'row', className: props.className, isNarrow: props.isNarrow, isMicro: props.isMicro, dateProfile: props.dateProfile, todayRange: props.todayRange, allDayDate: props.allDayDate, segs: props.segs, hiddenSegs: props.hiddenSegs, alignElRef: props.alignElRef, alignParentTop: props.alignParentTop, dateSpanProps: props.dateSpanProps, popoverContent: () => (jsx(Fragment, { children: props.segs.map((seg) => {
let { eventRange } = seg;
let { instanceId } = eventRange.instance;
let isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
let isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
let isInvisible = isDragging || isResizing;
return (jsx("div", { style: {
visibility: isInvisible ? 'hidden' : undefined,
}, children: jsx(StandardEvent, { display: hasListItemDisplay(seg) ? 'list-item' : 'row', eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: false, isSelected: instanceId === props.eventSelection, defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: false, ...getEventRangeMeta(eventRange, props.todayRange) }) }, instanceId));
}) })) }));
}
}
class DayGridCell extends DateComponent {
constructor() {
super(...arguments);
// memo
this.getDateMeta = memoize(getDateMeta);
this.refineRenderProps = memoizeObjArg(refineRenderProps);
// ref
this.rootElRef = createRef();
this.handleBodyEl = (bodyEl) => {
if (this.disconnectBodyHeight) {
this.disconnectBodyHeight();
this.disconnectBodyHeight = undefined;
setRef(this.props.headerHeightRef, null);
setRef(this.props.mainHeightRef, null);
}
if (bodyEl) {
// we want to fire on ANY size change, because we do more advanced stuff
this.disconnectBodyHeight = watchSize(bodyEl, (_bodyWidth, bodyHeight) => {
if (this._isUnmounting)
return;
const { props } = this;
const mainRect = bodyEl.getBoundingClientRect();
const rootRect = this.rootElRef.current.getBoundingClientRect();
const headerHeight = mainRect.top - rootRect.top;
if (!isDimsEqual(this.headerHeight, headerHeight)) {
this.headerHeight = headerHeight;
setRef(props.headerHeightRef, headerHeight);
}
if (props.fgLiquidHeight) {
setRef(props.mainHeightRef, bodyHeight);
}
});
}
};
}
render() {
let { props, context } = this;
let { options, dateEnv } = context;
// TODO: memoize this
const isMonthStart = props.showDayNumber &&
shouldDisplayMonthStart(props.date, props.dateProfile.currentRange, dateEnv);
const 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.flexCol, classNames.noMargin, classNames.noPadding);
const hasNavLink = options.navLinks;
const renderProps = this.refineRenderProps({
date: props.date,
isMajor: props.isMajor,
isNarrow: props.isNarrow,
dateMeta: dateMeta,
hasLabel: props.showDayNumber,
hasMonthLabel: isMonthStart,
hasNavLink,
renderProps: props.renderProps,
viewApi: context.viewApi,
dateEnv: context.dateEnv,
monthStartFormat: options.monthStartFormat,
dayCellFormat: options.dayCellFormat,
businessHours: Boolean(options.businessHours),
});
if (dateMeta.isDisabled) {
return (jsx("div", { role: 'gridcell', "aria-disabled": true, className: joinClassNames(generateClassName(options.dayCellClass, renderProps), props.className, baseClassName), style: {
width: props.width
} }));
}
const fullDateStr = buildDateStr(context, props.date);
return (jsx(ContentContainer, { tag: "div", elRef: this.rootElRef, className: joinClassNames(props.className, baseClassName), attrs: {
...props.attrs,
role: 'gridcell',
'aria-label': fullDateStr,
...(renderProps.isToday ? { 'aria-current': 'date' } : {}),
'data-date': formatDayString(props.date),
}, style: {
width: props.width,
}, renderProps: renderProps, generatorName: "dayCellTopContent" // !!! for top
, customGenerator: options.dayCellTopContent /* !!! for top */, defaultGenerator: renderTopInner, classNameGenerator: options.dayCellClass, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount, children: (InnerContent) => (jsxs(Fragment, { children: [jsx("div", { className: joinClassNames(classNames.rel, // puts it above bg-fills, which are positioned on TOP of this component :|
generateClassName(options.dayCellTopClass, renderProps)), children: props.showDayNumber && (jsx(InnerContent // the dayCellTopContent
, { tag: 'div', attrs: hasNavLink
? buildNavLinkAttrs(context, props.date, undefined, fullDateStr)
: { 'aria-hidden': true } // label already on cell
, className: generateClassName(options.dayCellTopInnerClass, renderProps) })) }), jsxs("div", { className: joinClassNames(classNames.flexCol, props.fgLiquidHeight ? classNames.liquid : classNames.grow), ref: this.handleBodyEl, children: [jsx("div", { className: generateClassName(options.dayCellInnerClass, renderProps), style: { minHeight: props.fgHeight }, children: props.fg }), jsx(DayGridMoreLink, { className: classNames.rel, allDayDate: props.date, segs: props.segs, hiddenSegs: props.hiddenSegs, alignElRef: this.rootElRef, alignParentTop: props.showDayNumber
? '[role=row]'
: `.${classNames.internalView}`, dateSpanProps: props.dateSpanProps, dateProfile: props.dateProfile, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, isNarrow: props.isNarrow, isMicro: props.isMicro })] }), jsx("div", { className: joinClassNames(classNames.rel, // puts it above bg-fills
generateClassName(options.dayCellBottomClass, renderProps)) })] })) }));
}
componentDidMount() {
this._isUnmounting = false;
}
componentWillUnmount() {
this._isUnmounting = true;
}
}
// Utils
// -------------------------------------------------------------------------------------------------
function renderTopInner(props) {
return props.text || jsx(Fragment, { children: "\u00A0" }); // TODO: DRY?
}
function shouldDisplayMonthStart(date, currentRange, dateEnv) {
const { start: currentStart, end: currentEnd } = currentRange;
const currentEndIncl = addMs(currentEnd, -1);
const currentFirstYear = dateEnv.getYear(currentStart);
const currentFirstMonth = dateEnv.getMonth(currentStart);
const currentLastYear = dateEnv.getYear(currentEndIncl);
const currentLastMonth = dateEnv.getMonth(currentEndIncl);
// spans more than one month?
return !(currentFirstYear === currentLastYear && currentFirstMonth === currentLastMonth) &&
Boolean(
// first date in current view?
date.valueOf() === currentStart.valueOf() ||
// a month-start that's within the current range?
(dateEnv.getDay(date) === 1 && date.valueOf() < currentEnd.valueOf()));
}
function refineRenderProps(raw) {
let { date, dateEnv, hasLabel, hasMonthLabel, hasNavLink, businessHours } = raw;
let textParts = [];
let text = '';
if (hasLabel) {
textParts = dateEnv.formatToParts(date, hasMonthLabel ? raw.monthStartFormat : raw.dayCellFormat);
text = joinDateTimeFormatParts(textParts);
}
return {
...raw.dateMeta,
...raw.renderProps,
text,
textParts,
isMajor: raw.isMajor,
isNarrow: raw.isNarrow,
inPopover: false,
hasNavLink,
get weekdayText() { return findWeekdayText(textParts); },
get dayNumberText() { return findDayNumberText(textParts); },
get monthText() { return findMonthText(textParts); },
options: { businessHours },
view: raw.viewApi,
};
}
class SegHierarchy {
constructor(segs, getSegThickness = (seg) => {
return 1;
}, strictOrder = false, // HACK
maxCoord, maxDepth, hiddenConsumes = false, // hidden segs also hide the touchingPlacement?
allowSlicing = false) {
this.getSegThickness = getSegThickness;
this.strictOrder = strictOrder;
this.maxCoord = maxCoord;
this.maxDepth = maxDepth;
this.hiddenConsumes = hiddenConsumes;
this.allowSlicing = allowSlicing;
this.placementsByLevel = [];
this.levelCoords = []; // parallel with placementsByLevel
this.hiddenSegs = [];
for (const seg of segs) {
this.insertSeg(seg, this.getSegThickness(seg));
}
}
insertSeg(seg, segThickness, isSlice) {
if (segThickness != null) {
const insertion = this.findInsertion(seg, segThickness);
if (this.isInsertionValid(insertion, segThickness)) {
this.insertSegAt(seg, insertion, segThickness, isSlice);
}
else {
const { touchingPlacement } = insertion;
// is there a touching-seg?
if (touchingPlacement) {
// should we hide or reslice touchingPlacement?
if (this.hiddenConsumes && !touchingPlacement.isZombie) {
touchingPlacement.isZombie = true; // edit in-place
this.hiddenSegs.push(touchingPlacement);
if (this.allowSlicing) {
const newSeg = Object.assign({}, touchingPlacement); // copy
// slice touchingPlacement in-place
Object.assign(touchingPlacement, intersectCoordRanges(touchingPlacement, seg));
touchingPlacement.isSlice = true;
// try to reinsert touchingPlacement's seg
this.splitSeg(newSeg, touchingPlacement.thickness, touchingPlacement);
}
}
// record seg as hidden, potentially split by touchingPlacement
if (this.allowSlicing) {
this.hiddenSegs.push({
...seg,
...intersectCoordRanges(seg, touchingPlacement),
});
this.splitSeg(seg, segThickness, touchingPlacement);
}
else {
this.hiddenSegs.push(seg);
}
// not touching anything
}
else {
this.hiddenSegs.push(seg);
}
}
}
}
/*
TODO: inline?
*/
isInsertionValid(insertion, thickness) {
return (this.maxCoord == null || insertion.levelCoord + thickness <= this.maxCoord) &&
(this.maxDepth == null || insertion.depth < this.maxDepth);
}
/*
Does not add the portion that intersects with barrier to hiddenSegs
*/
splitSeg(seg, segThickness, barrier) {
// any leftover seg on the start-side of the barrier?
if (seg.start < barrier.start) {
this.insertSeg({ ...seg, end: barrier.start, isEnd: false }, segThickness,
/* isSlice = */ true);
}
// any leftover seg on the end-side of the bar