calendar-utils
Version:
Utility functions to generate views for calendars
622 lines • 24.7 kB
JavaScript
export var DAYS_OF_WEEK;
(function (DAYS_OF_WEEK) {
DAYS_OF_WEEK[DAYS_OF_WEEK["SUNDAY"] = 0] = "SUNDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["MONDAY"] = 1] = "MONDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["TUESDAY"] = 2] = "TUESDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["WEDNESDAY"] = 3] = "WEDNESDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["THURSDAY"] = 4] = "THURSDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["FRIDAY"] = 5] = "FRIDAY";
DAYS_OF_WEEK[DAYS_OF_WEEK["SATURDAY"] = 6] = "SATURDAY";
})(DAYS_OF_WEEK || (DAYS_OF_WEEK = {}));
const DEFAULT_WEEKEND_DAYS = [
DAYS_OF_WEEK.SUNDAY,
DAYS_OF_WEEK.SATURDAY,
];
const DAYS_IN_WEEK = 7;
const HOURS_IN_DAY = 24;
const MINUTES_IN_HOUR = 60;
export const SECONDS_IN_DAY = 60 * 60 * 24;
function getExcludedSeconds(dateAdapter, { startDate, seconds, excluded, precision, }) {
if (excluded.length < 1) {
return 0;
}
const { addSeconds, getDay, addDays } = dateAdapter;
const endDate = addSeconds(startDate, seconds - 1);
const dayStart = getDay(startDate);
const dayEnd = getDay(endDate);
let result = 0; // Calculated in seconds
let current = startDate;
while (current < endDate) {
const day = getDay(current);
if (excluded.some((excludedDay) => excludedDay === day)) {
result += calculateExcludedSeconds(dateAdapter, {
dayStart,
dayEnd,
day,
precision,
startDate,
endDate,
});
}
current = addDays(current, 1);
}
return result;
}
function calculateExcludedSeconds(dateAdapter, { precision, day, dayStart, dayEnd, startDate, endDate, }) {
const { differenceInSeconds, endOfDay, startOfDay } = dateAdapter;
if (precision === 'minutes') {
if (day === dayStart) {
return differenceInSeconds(endOfDay(startDate), startDate) + 1;
}
else if (day === dayEnd) {
return differenceInSeconds(endDate, startOfDay(endDate)) + 1;
}
}
return SECONDS_IN_DAY;
}
function getWeekViewEventSpan(dateAdapter, { event, offset, startOfWeekDate, excluded, precision, totalDaysInView, }) {
const { max, differenceInSeconds, addDays, endOfDay, differenceInDays, } = dateAdapter;
let span = SECONDS_IN_DAY;
const begin = max([event.start, startOfWeekDate]);
if (event.end) {
switch (precision) {
case 'minutes':
span = differenceInSeconds(event.end, begin);
break;
default:
span =
differenceInDays(addDays(endOfDay(event.end), 1), begin) *
SECONDS_IN_DAY;
break;
}
}
const offsetSeconds = offset * SECONDS_IN_DAY;
const totalLength = offsetSeconds + span;
// the best way to detect if an event is outside the week-view
// is to check if the total span beginning (from startOfWeekDay or event start) exceeds the total days in the view
const secondsInView = totalDaysInView * SECONDS_IN_DAY;
if (totalLength > secondsInView) {
span = secondsInView - offsetSeconds;
}
span -= getExcludedSeconds(dateAdapter, {
startDate: begin,
seconds: span,
excluded,
precision,
});
return span / SECONDS_IN_DAY;
}
function getWeekViewEventOffset(dateAdapter, { event, startOfWeek: startOfWeekDate, excluded, precision, }) {
const { differenceInDays, startOfDay, differenceInSeconds } = dateAdapter;
if (event.start < startOfWeekDate) {
return 0;
}
let offset = 0;
switch (precision) {
case 'days':
offset =
differenceInDays(startOfDay(event.start), startOfWeekDate) *
SECONDS_IN_DAY;
break;
case 'minutes':
offset = differenceInSeconds(event.start, startOfWeekDate);
break;
}
offset -= getExcludedSeconds(dateAdapter, {
startDate: startOfWeekDate,
seconds: offset,
excluded,
precision,
});
return Math.abs(offset / SECONDS_IN_DAY);
}
function isEventIsPeriod(dateAdapter, { event, periodStart, periodEnd }) {
const { isSameSecond } = dateAdapter;
const eventStart = event.start;
const eventEnd = event.end || event.start;
if (eventStart > periodStart && eventStart < periodEnd) {
return true;
}
if (eventEnd > periodStart && eventEnd < periodEnd) {
return true;
}
if (eventStart < periodStart && eventEnd > periodEnd) {
return true;
}
if (isSameSecond(eventStart, periodStart) ||
isSameSecond(eventStart, periodEnd)) {
return true;
}
if (isSameSecond(eventEnd, periodStart) ||
isSameSecond(eventEnd, periodEnd)) {
return true;
}
return false;
}
export function getEventsInPeriod(dateAdapter, { events, periodStart, periodEnd }) {
return events.filter((event) => isEventIsPeriod(dateAdapter, { event, periodStart, periodEnd }));
}
function getWeekDay(dateAdapter, { date, weekendDays = DEFAULT_WEEKEND_DAYS, }) {
const { startOfDay, isSameDay, getDay } = dateAdapter;
const today = startOfDay(new Date());
const day = getDay(date);
return {
date,
day,
isPast: date < today,
isToday: isSameDay(date, today),
isFuture: date > today,
isWeekend: weekendDays.indexOf(day) > -1,
};
}
export function getWeekViewHeader(dateAdapter, { viewDate, weekStartsOn, excluded = [], weekendDays, viewStart = dateAdapter.startOfWeek(viewDate, { weekStartsOn }), viewEnd = dateAdapter.addDays(viewStart, DAYS_IN_WEEK), }) {
const { addDays, getDay } = dateAdapter;
const days = [];
let date = viewStart;
while (date < viewEnd) {
if (!excluded.some((e) => getDay(date) === e)) {
days.push(getWeekDay(dateAdapter, { date, weekendDays }));
}
date = addDays(date, 1);
}
return days;
}
export function getDifferenceInDaysWithExclusions(dateAdapter, { date1, date2, excluded }) {
let date = date1;
let diff = 0;
while (date < date2) {
if (excluded.indexOf(dateAdapter.getDay(date)) === -1) {
diff++;
}
date = dateAdapter.addDays(date, 1);
}
return diff;
}
export function getAllDayWeekEvents(dateAdapter, { events = [], excluded = [], precision = 'days', absolutePositionedEvents = false, viewStart, viewEnd, }) {
viewStart = dateAdapter.startOfDay(viewStart);
viewEnd = dateAdapter.endOfDay(viewEnd);
const { differenceInSeconds, differenceInDays } = dateAdapter;
const maxRange = getDifferenceInDaysWithExclusions(dateAdapter, {
date1: viewStart,
date2: viewEnd,
excluded,
});
const totalDaysInView = differenceInDays(viewEnd, viewStart) + 1;
const eventsMapped = events
.filter((event) => event.allDay)
.map((event) => {
const offset = getWeekViewEventOffset(dateAdapter, {
event,
startOfWeek: viewStart,
excluded,
precision,
});
const span = getWeekViewEventSpan(dateAdapter, {
event,
offset,
startOfWeekDate: viewStart,
excluded,
precision,
totalDaysInView,
});
return { event, offset, span };
})
.filter((e) => e.offset < maxRange)
.filter((e) => e.span > 0)
.map((entry) => ({
event: entry.event,
offset: entry.offset,
span: entry.span,
startsBeforeWeek: entry.event.start < viewStart,
endsAfterWeek: (entry.event.end || entry.event.start) > viewEnd,
}))
.sort((itemA, itemB) => {
const startSecondsDiff = differenceInSeconds(itemA.event.start, itemB.event.start);
if (startSecondsDiff === 0) {
return differenceInSeconds(itemB.event.end || itemB.event.start, itemA.event.end || itemA.event.start);
}
return startSecondsDiff;
});
const allDayEventRows = [];
const allocatedEvents = [];
eventsMapped.forEach((event, index) => {
if (allocatedEvents.indexOf(event) === -1) {
allocatedEvents.push(event);
let rowSpan = event.span + event.offset;
const otherRowEvents = eventsMapped
.slice(index + 1)
.filter((nextEvent) => {
if (nextEvent.offset >= rowSpan &&
rowSpan + nextEvent.span <= totalDaysInView &&
allocatedEvents.indexOf(nextEvent) === -1) {
const nextEventOffset = nextEvent.offset - rowSpan;
if (!absolutePositionedEvents) {
nextEvent.offset = nextEventOffset;
}
rowSpan += nextEvent.span + nextEventOffset;
allocatedEvents.push(nextEvent);
return true;
}
});
const weekEvents = [event, ...otherRowEvents];
const id = weekEvents
.filter((weekEvent) => weekEvent.event.id)
.map((weekEvent) => weekEvent.event.id)
.join('-');
allDayEventRows.push(Object.assign({ row: weekEvents }, (id ? { id } : {})));
}
});
return allDayEventRows;
}
function getWeekViewHourGrid(dateAdapter, { events, viewDate, hourSegments, hourDuration, dayStart, dayEnd, weekStartsOn, excluded, weekendDays, segmentHeight, viewStart, viewEnd, minimumEventHeight, }) {
const dayViewHourGrid = getDayViewHourGrid(dateAdapter, {
viewDate,
hourSegments,
hourDuration,
dayStart,
dayEnd,
});
const weekDays = getWeekViewHeader(dateAdapter, {
viewDate,
weekStartsOn,
excluded,
weekendDays,
viewStart,
viewEnd,
});
const { setHours, setMinutes, getHours, getMinutes } = dateAdapter;
return weekDays.map((day) => {
const dayView = getDayView(dateAdapter, {
events,
viewDate: day.date,
hourSegments,
dayStart,
dayEnd,
segmentHeight,
eventWidth: 1,
hourDuration,
minimumEventHeight,
});
const hours = dayViewHourGrid.map((hour) => {
const segments = hour.segments.map((segment) => {
const date = setMinutes(setHours(day.date, getHours(segment.date)), getMinutes(segment.date));
return Object.assign(Object.assign({}, segment), { date });
});
return Object.assign(Object.assign({}, hour), { segments });
});
function getColumnCount(allEvents, prevOverlappingEvents) {
const columnCount = Math.max(...prevOverlappingEvents.map((iEvent) => iEvent.left + 1));
const nextOverlappingEvents = allEvents
.filter((iEvent) => iEvent.left >= columnCount)
.filter((iEvent) => {
return (getOverLappingWeekViewEvents(prevOverlappingEvents, iEvent.top, iEvent.top + iEvent.height).length > 0);
});
if (nextOverlappingEvents.length > 0) {
return getColumnCount(allEvents, nextOverlappingEvents);
}
else {
return columnCount;
}
}
const mappedEvents = dayView.events.map((event) => {
const columnCount = getColumnCount(dayView.events, getOverLappingWeekViewEvents(dayView.events, event.top, event.top + event.height));
const width = 100 / columnCount;
return Object.assign(Object.assign({}, event), { left: event.left * width, width });
});
return {
hours,
date: day.date,
events: mappedEvents.map((event) => {
const overLappingEvents = getOverLappingWeekViewEvents(mappedEvents.filter((otherEvent) => otherEvent.left > event.left), event.top, event.top + event.height);
if (overLappingEvents.length > 0) {
return Object.assign(Object.assign({}, event), { width: Math.min(...overLappingEvents.map((otherEvent) => otherEvent.left)) - event.left });
}
return event;
}),
};
});
}
export function getWeekView(dateAdapter, { events = [], viewDate, weekStartsOn, excluded = [], precision = 'days', absolutePositionedEvents = false, hourSegments, hourDuration, dayStart, dayEnd, weekendDays, segmentHeight, minimumEventHeight, viewStart = dateAdapter.startOfWeek(viewDate, { weekStartsOn }), viewEnd = dateAdapter.endOfWeek(viewDate, { weekStartsOn }), }) {
if (!events) {
events = [];
}
const { startOfDay, endOfDay } = dateAdapter;
viewStart = startOfDay(viewStart);
viewEnd = endOfDay(viewEnd);
const eventsInPeriod = getEventsInPeriod(dateAdapter, {
events,
periodStart: viewStart,
periodEnd: viewEnd,
});
const header = getWeekViewHeader(dateAdapter, {
viewDate,
weekStartsOn,
excluded,
weekendDays,
viewStart,
viewEnd,
});
return {
allDayEventRows: getAllDayWeekEvents(dateAdapter, {
events: eventsInPeriod,
excluded,
precision,
absolutePositionedEvents,
viewStart,
viewEnd,
}),
period: {
events: eventsInPeriod,
start: header[0].date,
end: endOfDay(header[header.length - 1].date),
},
hourColumns: getWeekViewHourGrid(dateAdapter, {
events,
viewDate,
hourSegments,
hourDuration,
dayStart,
dayEnd,
weekStartsOn,
excluded,
weekendDays,
segmentHeight,
viewStart,
viewEnd,
minimumEventHeight,
}),
};
}
export function getMonthView(dateAdapter, { events = [], viewDate, weekStartsOn, excluded = [], viewStart = dateAdapter.startOfMonth(viewDate), viewEnd = dateAdapter.endOfMonth(viewDate), weekendDays, }) {
if (!events) {
events = [];
}
const { startOfWeek, endOfWeek, differenceInDays, startOfDay, addHours, endOfDay, isSameMonth, getDay, } = dateAdapter;
const start = startOfWeek(viewStart, { weekStartsOn });
const end = endOfWeek(viewEnd, { weekStartsOn });
const eventsInMonth = getEventsInPeriod(dateAdapter, {
events,
periodStart: start,
periodEnd: end,
});
const initialViewDays = [];
let previousDate;
for (let i = 0; i < differenceInDays(end, start) + 1; i++) {
// hacky fix for https://github.com/mattlewis92/angular-calendar/issues/173
let date;
if (previousDate) {
date = startOfDay(addHours(previousDate, HOURS_IN_DAY));
if (previousDate.getTime() === date.getTime()) {
// DST change, so need to add 25 hours
/* istanbul ignore next */
date = startOfDay(addHours(previousDate, HOURS_IN_DAY + 1));
}
previousDate = date;
}
else {
date = previousDate = start;
}
if (!excluded.some((e) => getDay(date) === e)) {
const day = getWeekDay(dateAdapter, {
date,
weekendDays,
});
const eventsInPeriod = getEventsInPeriod(dateAdapter, {
events: eventsInMonth,
periodStart: startOfDay(date),
periodEnd: endOfDay(date),
});
day.inMonth = isSameMonth(date, viewDate);
day.events = eventsInPeriod;
day.badgeTotal = eventsInPeriod.length;
initialViewDays.push(day);
}
}
let days = [];
const totalDaysVisibleInWeek = DAYS_IN_WEEK - excluded.length;
if (totalDaysVisibleInWeek < DAYS_IN_WEEK) {
for (let i = 0; i < initialViewDays.length; i += totalDaysVisibleInWeek) {
const row = initialViewDays.slice(i, i + totalDaysVisibleInWeek);
const isRowInMonth = row.some((day) => viewStart <= day.date && day.date < viewEnd);
if (isRowInMonth) {
days = [...days, ...row];
}
}
}
else {
days = initialViewDays;
}
const rows = Math.floor(days.length / totalDaysVisibleInWeek);
const rowOffsets = [];
for (let i = 0; i < rows; i++) {
rowOffsets.push(i * totalDaysVisibleInWeek);
}
return {
rowOffsets,
totalDaysVisibleInWeek,
days,
period: {
start: days[0].date,
end: endOfDay(days[days.length - 1].date),
events: eventsInMonth,
},
};
}
function getOverLappingWeekViewEvents(events, top, bottom) {
return events.filter((previousEvent) => {
const previousEventTop = previousEvent.top;
const previousEventBottom = previousEvent.top + previousEvent.height;
if (top < previousEventBottom && previousEventBottom < bottom) {
return true;
}
else if (top < previousEventTop && previousEventTop < bottom) {
return true;
}
else if (previousEventTop <= top && bottom <= previousEventBottom) {
return true;
}
return false;
});
}
function getDayView(dateAdapter, { events, viewDate, hourSegments, dayStart, dayEnd, eventWidth, segmentHeight, hourDuration, minimumEventHeight, }) {
const { setMinutes, setHours, startOfDay, startOfMinute, endOfDay, differenceInMinutes, } = dateAdapter;
const startOfView = setMinutes(setHours(startOfDay(viewDate), sanitiseHours(dayStart.hour)), sanitiseMinutes(dayStart.minute));
const endOfView = setMinutes(setHours(startOfMinute(endOfDay(viewDate)), sanitiseHours(dayEnd.hour)), sanitiseMinutes(dayEnd.minute));
endOfView.setSeconds(59, 999);
const previousDayEvents = [];
const eventsInPeriod = getEventsInPeriod(dateAdapter, {
events: events.filter((event) => !event.allDay),
periodStart: startOfView,
periodEnd: endOfView,
});
const dayViewEvents = eventsInPeriod
.sort((eventA, eventB) => {
return eventA.start.valueOf() - eventB.start.valueOf();
})
.map((event) => {
const eventStart = event.start;
const eventEnd = event.end || eventStart;
const startsBeforeDay = eventStart < startOfView;
const endsAfterDay = eventEnd > endOfView;
const hourHeightModifier = (hourSegments * segmentHeight) / (hourDuration || MINUTES_IN_HOUR);
let top = 0;
if (eventStart > startOfView) {
// adjust the difference in minutes if the user's offset is different between the start of the day and the event (e.g. when going to or from DST)
const eventOffset = dateAdapter.getTimezoneOffset(eventStart);
const startOffset = dateAdapter.getTimezoneOffset(startOfView);
const diff = startOffset - eventOffset;
top += differenceInMinutes(eventStart, startOfView) + diff;
}
top *= hourHeightModifier;
top = Math.floor(top);
const startDate = startsBeforeDay ? startOfView : eventStart;
const endDate = endsAfterDay ? endOfView : eventEnd;
const timezoneOffset = dateAdapter.getTimezoneOffset(startDate) -
dateAdapter.getTimezoneOffset(endDate);
let height = differenceInMinutes(endDate, startDate) + timezoneOffset;
if (!event.end) {
height = segmentHeight;
}
else {
height *= hourHeightModifier;
}
if (minimumEventHeight && height < minimumEventHeight) {
height = minimumEventHeight;
}
height = Math.floor(height);
const bottom = top + height;
const overlappingPreviousEvents = getOverLappingWeekViewEvents(previousDayEvents, top, bottom);
let left = 0;
while (overlappingPreviousEvents.some((previousEvent) => previousEvent.left === left)) {
left += eventWidth;
}
const dayEvent = {
event,
height,
width: eventWidth,
top,
left,
startsBeforeDay,
endsAfterDay,
};
previousDayEvents.push(dayEvent);
return dayEvent;
});
const width = Math.max(...dayViewEvents.map((event) => event.left + event.width));
const allDayEvents = getEventsInPeriod(dateAdapter, {
events: events.filter((event) => event.allDay),
periodStart: startOfDay(startOfView),
periodEnd: endOfDay(endOfView),
});
return {
events: dayViewEvents,
width,
allDayEvents,
period: {
events: eventsInPeriod,
start: startOfView,
end: endOfView,
},
};
}
function sanitiseHours(hours) {
return Math.max(Math.min(23, hours), 0);
}
function sanitiseMinutes(minutes) {
return Math.max(Math.min(59, minutes), 0);
}
function getDayViewHourGrid(dateAdapter, { viewDate, hourSegments, hourDuration, dayStart, dayEnd, }) {
const { setMinutes, setHours, startOfDay, startOfMinute, endOfDay, addMinutes, addDays, } = dateAdapter;
const hours = [];
let startOfView = setMinutes(setHours(startOfDay(viewDate), sanitiseHours(dayStart.hour)), sanitiseMinutes(dayStart.minute));
let endOfView = setMinutes(setHours(startOfMinute(endOfDay(viewDate)), sanitiseHours(dayEnd.hour)), sanitiseMinutes(dayEnd.minute));
const segmentDuration = (hourDuration || MINUTES_IN_HOUR) / hourSegments;
let startOfViewDay = startOfDay(viewDate);
const endOfViewDay = endOfDay(viewDate);
let dateAdjustment = (d) => d;
// this means that we change from or to DST on this day and that's going to cause problems so we bump the date
if (dateAdapter.getTimezoneOffset(startOfViewDay) !==
dateAdapter.getTimezoneOffset(endOfViewDay)) {
startOfViewDay = addDays(startOfViewDay, 1);
startOfView = addDays(startOfView, 1);
endOfView = addDays(endOfView, 1);
dateAdjustment = (d) => addDays(d, -1);
}
const dayDuration = hourDuration
? (HOURS_IN_DAY * 60) / hourDuration
: MINUTES_IN_HOUR;
for (let i = 0; i < dayDuration; i++) {
const segments = [];
for (let j = 0; j < hourSegments; j++) {
const date = addMinutes(addMinutes(startOfView, i * (hourDuration || MINUTES_IN_HOUR)), j * segmentDuration);
if (date >= startOfView && date < endOfView) {
segments.push({
date: dateAdjustment(date),
displayDate: date,
isStart: j === 0,
});
}
}
if (segments.length > 0) {
hours.push({ segments });
}
}
return hours;
}
export var EventValidationErrorMessage;
(function (EventValidationErrorMessage) {
EventValidationErrorMessage["NotArray"] = "Events must be an array";
EventValidationErrorMessage["StartPropertyMissing"] = "Event is missing the `start` property";
EventValidationErrorMessage["StartPropertyNotDate"] = "Event `start` property should be a javascript date object. Do `new Date(event.start)` to fix it.";
EventValidationErrorMessage["EndPropertyNotDate"] = "Event `end` property should be a javascript date object. Do `new Date(event.end)` to fix it.";
EventValidationErrorMessage["EndsBeforeStart"] = "Event `start` property occurs after the `end`";
})(EventValidationErrorMessage || (EventValidationErrorMessage = {}));
export function validateEvents(events, log) {
let isValid = true;
function isError(msg, event) {
log(msg, event);
isValid = false;
}
if (!Array.isArray(events)) {
log(EventValidationErrorMessage.NotArray, events);
return false;
}
events.forEach((event) => {
if (!event.start) {
isError(EventValidationErrorMessage.StartPropertyMissing, event);
}
else if (!(event.start instanceof Date)) {
isError(EventValidationErrorMessage.StartPropertyNotDate, event);
}
if (event.end) {
if (!(event.end instanceof Date)) {
isError(EventValidationErrorMessage.EndPropertyNotDate, event);
}
if (event.start > event.end) {
isError(EventValidationErrorMessage.EndsBeforeStart, event);
}
}
});
return isValid;
}
//# sourceMappingURL=calendar-utils.js.map