@fullcalendar/web-component
Version:
Custom Element for FullCalendar
361 lines (358 loc) • 15.3 kB
JavaScript
import { constrainMarkerToRange, intersectRanges, rangesIntersect, rangeContainsMarker, greatestDurationDenominator, createDuration, asRoughDays, startOfDay, addDays, asRoughMs, parseRange, diffWholeWeeks, diffWholeDays } from '@full-ui/headless-calendar';
import { Y as computeVisibleDayRange } from './69b11357.js';
class DateProfileGenerator {
constructor(props) {
this.props = props;
this.initHiddenDays();
}
/* Date Range Computation
------------------------------------------------------------------------------------------------------------------*/
// Builds a structure with info about what the dates/ranges will be for the "prev" view.
buildPrev(currentDateProfile, currentDate, nowDate, forceToValid) {
let { dateEnv } = this.props;
let prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(prevDate, nowDate, -1, forceToValid);
}
// Builds a structure with info about what the dates/ranges will be for the "next" view.
buildNext(currentDateProfile, currentDate, nowDate, forceToValid) {
let { dateEnv } = this.props;
let nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(nextDate, nowDate, 1, forceToValid);
}
// Builds a structure holding dates/ranges for rendering around the given date.
// Optional direction param indicates whether the date is being incremented/decremented
// from its previous value. decremented = -1, incremented = 1 (default).
build(currentDate, nowDate, direction, forceToValid = true) {
let { props } = this;
let validRange;
let currentInfo;
let isRangeAllDay;
let renderRange;
let activeRange;
let isValid;
validRange = this.buildValidRange(nowDate);
validRange = this.trimHiddenDays(validRange);
if (forceToValid) {
currentDate = constrainMarkerToRange(currentDate, validRange);
}
currentInfo = this.buildCurrentRangeInfo(currentDate, direction);
isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay);
renderRange = this.trimHiddenDays(renderRange);
activeRange = renderRange;
if (!props.showNonCurrentDates) {
activeRange = intersectRanges(activeRange, currentInfo.range);
}
activeRange = this.adjustActiveRange(activeRange);
activeRange = intersectRanges(activeRange, validRange); // might return null
// it's invalid if the originally requested date is not contained,
// or if the range is completely outside of the valid range.
isValid = rangesIntersect(currentInfo.range, validRange);
// HACK: constrain to render-range so `currentDate` is more useful to view rendering
if (!rangeContainsMarker(renderRange, currentDate)) {
currentDate = renderRange.start;
}
return {
currentDate,
// constraint for where prev/next operations can go and where events can be dragged/resized to.
// an object with optional start and end properties.
validRange,
// range the view is formally responsible for.
// for example, a month view might have 1st-31st, excluding padded dates
currentRange: currentInfo.range,
// name of largest unit being displayed, like "month" or "week"
currentRangeUnit: currentInfo.unit,
isRangeAllDay,
// dates that display events and accept drag-n-drop
// will be `null` if no dates accept events
activeRange,
// date range with a rendered skeleton
// includes not-active days that need some sort of DOM
renderRange,
// Duration object that denotes the first visible time of any given day
slotMinTime: props.slotMinTime,
// Duration object that denotes the exclusive visible end time of any given day
slotMaxTime: props.slotMaxTime,
isValid,
// how far the current date will move for a prev/next operation
dateIncrement: this.buildDateIncrement(currentInfo.duration),
// pass a fallback (might be null) ^
};
}
// Builds an object with optional start/end properties.
// Indicates the minimum/maximum dates to display.
// not responsible for trimming hidden days.
buildValidRange(nowDate) {
let input = this.props.validRangeInput;
let simpleInput = typeof input === 'function'
? input.call(this.props.calendarApi, this.props.dateEnv.toDate(nowDate))
: input;
return this.refineRange(simpleInput) ||
{ start: null, end: null }; // completely open-ended
}
// Builds a structure with info about the "current" range, the range that is
// highlighted as being the current month for example.
// See build() for a description of `direction`.
// Guaranteed to have `range` and `unit` properties. `duration` is optional.
buildCurrentRangeInfo(date, direction) {
let { props } = this;
let duration = null;
let unit = null;
let range = null;
let dayCount;
if (props.duration) {
duration = props.duration;
unit = props.durationUnit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
else if ((dayCount = this.props.dayCount)) {
unit = 'day';
range = this.buildRangeFromDayCount(date, direction, dayCount);
}
else if ((range = this.buildCustomVisibleRange(date))) {
unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit;
}
else {
duration = this.getFallbackDuration();
unit = greatestDurationDenominator(duration).unit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
return { duration, unit, range };
}
getFallbackDuration() {
return createDuration({ day: 1 });
}
// Returns a new activeRange to have time values (un-ambiguate)
// slotMinTime or slotMaxTime causes the range to expand.
adjustActiveRange(range) {
let { dateEnv, usesMinMaxTime, slotMinTime, slotMaxTime } = this.props;
let { start, end } = range;
if (usesMinMaxTime) {
// expand active range if slotMinTime is negative (why not when positive?)
if (asRoughDays(slotMinTime) < 0) {
start = startOfDay(start); // necessary?
start = dateEnv.add(start, slotMinTime);
}
// expand active range if slotMaxTime is beyond one day (why not when negative?)
if (asRoughDays(slotMaxTime) > 1) {
end = startOfDay(end); // necessary?
end = addDays(end, -1);
end = dateEnv.add(end, slotMaxTime);
}
}
return { start, end };
}
// Builds the "current" range when it is specified as an explicit duration.
// `unit` is the already-computed greatestDurationDenominator unit of duration.
buildRangeFromDuration(date, direction, duration, unit) {
let { dateEnv, dateAlignment } = this.props;
let start;
let end;
let res;
// compute what the alignment should be
if (!dateAlignment) {
let { dateIncrement } = this.props;
if (dateIncrement) {
// use the smaller of the two units
if (asRoughMs(dateIncrement) < asRoughMs(duration)) {
dateAlignment = greatestDurationDenominator(dateIncrement).unit;
}
else {
dateAlignment = unit;
}
}
else {
dateAlignment = unit;
}
}
// if the view displays a single day or smaller
if (asRoughDays(duration) <= 1) {
if (this.isHiddenDay(start)) {
start = this.skipHiddenDays(start, direction);
start = startOfDay(start);
}
}
function computeRes() {
start = dateEnv.startOf(date, dateAlignment);
end = dateEnv.add(start, duration);
res = { start, end };
}
computeRes();
// if range is completely enveloped by hidden days, go past the hidden days
if (!this.trimHiddenDays(res)) {
date = this.skipHiddenDays(date, direction);
computeRes();
}
return res;
}
// Builds the "current" range when a dayCount is specified.
buildRangeFromDayCount(date, direction, dayCount) {
let { dateEnv, dateAlignment } = this.props;
let runningCount = 0;
let start = date;
let end;
if (dateAlignment) {
start = dateEnv.startOf(start, dateAlignment);
}
start = startOfDay(start);
start = this.skipHiddenDays(start, direction);
end = start;
do {
end = addDays(end, 1);
if (!this.isHiddenDay(end)) {
runningCount += 1;
}
} while (runningCount < dayCount);
return { start, end };
}
// Builds a normalized range object for the "visible" range,
// which is a way to define the currentRange and activeRange at the same time.
buildCustomVisibleRange(date) {
let { props } = this;
let input = props.visibleRangeInput;
let simpleInput = typeof input === 'function'
? input.call(props.calendarApi, props.dateEnv.toDate(date))
: input;
let range = this.refineRange(simpleInput);
if (range && (range.start == null || range.end == null)) {
return null;
}
return range;
}
// Computes the range that will represent the element/cells for *rendering*,
// but which may have voided days/times.
// not responsible for trimming hidden days.
buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) {
return currentRange;
}
// Compute the duration value that should be added/substracted to the current date
// when a prev/next operation happens.
buildDateIncrement(fallback) {
let { dateIncrement } = this.props;
let customAlignment;
if (dateIncrement) {
return dateIncrement;
}
if ((customAlignment = this.props.dateAlignment)) {
return createDuration(1, customAlignment);
}
if (fallback) {
return fallback;
}
return createDuration({ days: 1 });
}
refineRange(rangeInput) {
if (rangeInput) {
let range = parseRange(rangeInput, this.props.dateEnv);
if (range) {
range = computeVisibleDayRange(range);
}
return range;
}
return null;
}
/* Hidden Days
------------------------------------------------------------------------------------------------------------------*/
// Initializes internal variables related to calculating hidden days-of-week
initHiddenDays() {
let hiddenDays = this.props.hiddenDays || []; // array of day-of-week indices that are hidden
let isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
let dayCnt = 0;
let i;
if (this.props.weekends === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
for (i = 0; i < 7; i += 1) {
if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) {
dayCnt += 1;
}
}
if (!dayCnt) {
throw new Error('invalid hiddenDays'); // all days were hidden? bad.
}
this.isHiddenDayHash = isHiddenDayHash;
}
// Remove days from the beginning and end of the range that are computed as hidden.
// If the whole range is trimmed off, returns null
trimHiddenDays(range) {
let { start, end } = range;
if (start) {
start = this.skipHiddenDays(start);
}
if (end) {
end = this.skipHiddenDays(end, -1, true);
}
if (start == null || end == null || start < end) {
return { start, end };
}
return null;
}
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Date (used for UTC)
isHiddenDay(day) {
if (day instanceof Date) {
day = day.getUTCDay();
}
return this.isHiddenDayHash[day];
}
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// DOES NOT CONSIDER validRange!
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
skipHiddenDays(date, inc = 1, isExclusive = false) {
while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) {
date = addDays(date, inc);
}
return date;
}
}
// Utils
// -------------------------------------------------------------------------------------------------
function computeMajorUnit(dateProfile, dateEnv) {
const { currentRange } = dateProfile;
if (dateProfile.currentRangeUnit === 'year') {
if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) {
return 'year';
}
else {
return 'month';
}
}
else if (dateProfile.currentRangeUnit === 'month') {
if (dateEnv.diffWholeMonths(currentRange.start, currentRange.end) > 1) {
return 'month';
}
}
else if (dateProfile.currentRangeUnit === 'week') {
if (diffWholeWeeks(currentRange.start, currentRange.end) > 1) {
return 'week';
}
}
else if (dateProfile.currentRangeUnit === 'day') {
if (diffWholeDays(currentRange.start, currentRange.end) > 1) {
return 'day';
}
}
}
function isMajorUnit(dateMarker, majorUnit, dateEnv) {
const isStartOfDay = dateMarker.valueOf() === startOfDay(dateMarker).valueOf();
if (isStartOfDay) {
if (majorUnit === 'year') {
return !dateEnv.getMonth(dateMarker) && dateEnv.getDay(dateMarker) === 1;
}
else if (majorUnit === 'month') {
return dateEnv.getDay(dateMarker) === 1;
}
else if (majorUnit === 'week') {
return dateMarker.getUTCDay() === dateEnv.weekDow;
}
else if (majorUnit === 'day') {
return true;
}
}
return false;
}
export { DateProfileGenerator as D, computeMajorUnit as c, isMajorUnit as i };