novo-elements
Version:
Bullhorn's NOVO Element Repository for Angular 2
519 lines (425 loc) • 17 kB
text/typescript
import * as dateFns from 'date-fns';
const WEEKEND_DAY_NUMBERS: number[] = [0, 6];
const DAYS_IN_WEEK: number = 7;
const HOURS_IN_DAY: number = 24;
const MINUTES_IN_HOUR: number = 60;
export enum CalendarEventResponse {
Maybe, Accepted, Rejected
}
export interface CalendarEventTimesChangedEvent {
event: CalendarEvent;
newStart: Date;
newEnd?: Date;
}
export interface WeekDay {
date: Date;
isPast: boolean;
isToday: boolean;
isFuture: boolean;
isWeekend: boolean;
}
export interface EventColor {
primary: string;
secondary: string;
}
export interface EventAction {
label: string;
cssClass?: string;
onClick({ event }: { event: CalendarEvent }): any;
}
export interface CalendarEvent {
id?:number;
start: Date;
end?: Date;
title: string;
description?: string;
color: EventColor;
type?: string;
response?: CalendarEventResponse;
actions?: EventAction[];
allDay?: boolean;
cssClass?: string;
resizable?: {
beforeStart?: boolean;
afterEnd?: boolean;
};
draggable?: boolean;
}
export interface WeekViewEvent {
event: CalendarEvent;
offset: number;
span: number;
startsBeforeWeek: boolean;
endsAfterWeek: boolean;
top?: number;
height?: number;
}
export interface WeekViewEventRow {
row: WeekViewEvent[];
}
export interface MonthViewDay extends WeekDay {
inMonth: boolean;
events: CalendarEvent[];
backgroundColor?: string;
cssClass?: string;
badgeTotal: number;
}
export interface MonthView {
rowOffsets: number[];
days: MonthViewDay[];
totalDaysVisibleInWeek: number;
}
export interface DayViewEvent {
event: CalendarEvent;
height: number;
width: number;
top: number;
left: number;
startsBeforeDay: boolean;
endsAfterDay: boolean;
}
export interface DayView {
events: DayViewEvent[];
width: number;
allDayEvents: CalendarEvent[];
}
export interface DayViewHourSegment {
isStart: boolean;
date: Date;
cssClass?: string;
}
export interface DayViewHour {
segments: DayViewHourSegment[];
}
export interface IsEventInPeriodArgs {
event: CalendarEvent;
periodStart: Date;
periodEnd: Date;
}
export interface GetEventsInPeriodArgs {
events: CalendarEvent[];
periodStart: Date;
periodEnd: Date;
}
export interface GetDayViewArgs {
events?: CalendarEvent[];
viewDate: Date;
hourSegments: number;
dayStart: {
hour: number;
minute: number;
};
dayEnd: {
hour: number;
minute: number;
};
eventWidth: number;
segmentHeight: number;
}
function getExcludedDays({ startDate, days, excluded }: { startDate: Date, days: number, excluded: number[] }): number {
if (excluded.length < 1) {
return 0;
}
let day: number = startDate.getDay();
let reduce: number = 0;
for (let i: number = 0; i < days; i++) {
if (day === DAYS_IN_WEEK) {
day = 0;
}
if (excluded.some(e => e === day)) {
reduce++;
}
day++;
}
return reduce;
}
function getWeekViewEventSpan(
{ event, offset, startOfWeek, excluded }: { event: CalendarEvent, offset: number, startOfWeek: Date, excluded: number[] }): number {
const begin: Date = event.start < startOfWeek ? startOfWeek : event.start;
let span: number = 1;
if (event.end) {
span = dateFns.differenceInDays(dateFns.addMinutes(dateFns.endOfDay(event.end), 1), dateFns.startOfDay(begin));
}
const totalLength: number = offset + span;
if (totalLength > DAYS_IN_WEEK) {
span = DAYS_IN_WEEK - offset;
}
return span - getExcludedDays({ startDate: begin, days: span, excluded });
}
export function getWeekViewEventOffset(
{ event, startOfWeek, excluded = [] }: { event: CalendarEvent, startOfWeek: Date, excluded?: number[] }): number {
if (event.start < startOfWeek) {
return 0;
}
const distance: number = dateFns.differenceInDays(event.start, startOfWeek);
return distance - getExcludedDays({ startDate: startOfWeek, days: distance, excluded });
}
function isEventIsPeriod({ event, periodStart, periodEnd }: IsEventInPeriodArgs): boolean {
const eventStart: Date = event.start;
const eventEnd: Date = 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 (dateFns.isSameSecond(eventStart, periodStart) || dateFns.isSameSecond(eventStart, periodEnd)) {
return true;
}
if (dateFns.isSameSecond(eventEnd, periodStart) || dateFns.isSameSecond(eventEnd, periodEnd)) {
return true;
}
return false;
}
function getEventsInPeriod({ events, periodStart, periodEnd }: GetEventsInPeriodArgs): CalendarEvent[] {
return events.filter((event: CalendarEvent) => isEventIsPeriod({ event, periodStart, periodEnd }));
}
function getEventsInTimeRange(events: CalendarEvent[], dayStart: any, dayEnd: any) {
return events.filter( event => {
const eventStart: Date = event.start;
const eventEnd: Date = event.end || eventStart;
const startOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfDay(eventStart), dayStart.hour), dayStart.minute);
const endOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfMinute(eventStart), dayEnd.hour), dayEnd.minute);
return dateFns.isAfter(eventEnd, startOfView)
&& dateFns.isBefore(eventStart, endOfView);
});
}
function getWeekDay({ date }: { date: Date }): WeekDay {
const today: Date = dateFns.startOfDay(new Date());
return {
date,
isPast: date < today,
isToday: dateFns.isSameDay(date, today),
isFuture: date > today,
isWeekend: WEEKEND_DAY_NUMBERS.indexOf(dateFns.getDay(date)) > -1
};
}
export function getWeekViewHeader({ viewDate, weekStartsOn, excluded = [] }:
{ viewDate: Date, weekStartsOn: number, excluded?: number[] }): WeekDay[] {
const start: Date = dateFns.startOfWeek(viewDate, { weekStartsOn });
const days: WeekDay[] = [];
for (let i: number = 0; i < DAYS_IN_WEEK; i++) {
const date: Date = dateFns.addDays(start, i);
if (!excluded.some(e => date.getDay() === e)) {
days.push(getWeekDay({ date }));
}
}
return days;
}
export function getWeekView({ events = [], viewDate, weekStartsOn, excluded = [], hourSegments, segmentHeight, dayStart, dayEnd }: { events?: CalendarEvent[], viewDate: Date, weekStartsOn: number, excluded?: number[], hourSegments:number, segmentHeight:number, dayStart:any, dayEnd:any }): WeekViewEventRow[] {
if (!events) {
events = [];
}
const startOfViewWeek: Date = dateFns.startOfWeek(viewDate, { weekStartsOn });
const endOfViewWeek: Date = dateFns.endOfWeek(viewDate, { weekStartsOn });
const maxRange: number = DAYS_IN_WEEK - excluded.length;
const eventsMapped: WeekViewEvent[] = getEventsInTimeRange(getEventsInPeriod({ events, periodStart: startOfViewWeek, periodEnd: endOfViewWeek }), dayStart, dayEnd).map(event => {
const offset: number = getWeekViewEventOffset({ event, startOfWeek: startOfViewWeek, excluded });
const span: number = 1; //getWeekViewEventSpan({ event, offset, startOfWeek: startOfViewWeek, excluded });
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 < startOfViewWeek,
endsAfterWeek: (entry.event.end || entry.event.start) > endOfViewWeek,
top: 0
})).sort((itemA, itemB): number => {
const startSecondsDiff: number = dateFns.differenceInSeconds(itemA.event.start, itemB.event.start);
if (startSecondsDiff === 0) {
return dateFns.differenceInSeconds(itemB.event.end || itemB.event.start, itemA.event.end || itemA.event.start);
}
return startSecondsDiff;
}).map((entry: WeekViewEvent) => {
const startOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfDay(entry.event.start), dayStart.hour), dayStart.minute);
const endOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfMinute(dateFns.endOfDay(entry.event.start)), dayEnd.hour), dayEnd.minute);
const eventStart: Date = entry.event.start;
const eventEnd: Date = entry.event.end || eventStart;
const hourHeightModifier: number = (hourSegments * segmentHeight) / MINUTES_IN_HOUR;
if (eventStart > startOfView) {
entry.top += dateFns.differenceInMinutes(eventStart, startOfView);
}
entry.top *= hourHeightModifier;
const startsBeforeDay: boolean = eventStart < startOfView;
const endsAfterDay: boolean = eventEnd > endOfView;
const startDate: Date = startsBeforeDay ? startOfView : eventStart;
const endDate: Date = endsAfterDay ? endOfView : eventEnd;
let height: number = dateFns.differenceInMinutes(endDate, startDate);
if (!entry.event.end) {
height = segmentHeight;
} else {
height *= hourHeightModifier;
}
entry.height = height;
return entry;
});
const eventRows: WeekViewEventRow[] = [];
const allocatedEvents: WeekViewEvent[] = [];
eventsMapped.forEach((event: WeekViewEvent, index: number) => {
if (allocatedEvents.indexOf(event) === -1) {
allocatedEvents.push(event);
const otherRowEvents: WeekViewEvent[] = eventsMapped.slice(index + 1).filter(nextEvent => {
return nextEvent.top === event.top && nextEvent.offset === event.offset;
});
if (otherRowEvents.length > 0) {
let totalEventsForRow = otherRowEvents.length + 1;
event.span = ( 1 / totalEventsForRow );
let nextOffset = event.span + event.offset;
otherRowEvents.forEach( (nextEvent:WeekViewEvent) => {
nextEvent.offset = nextOffset;
nextEvent.span = event.span;
nextOffset = nextEvent.span + nextEvent.offset;
});
allocatedEvents.push(...otherRowEvents);
}
eventRows.push({
row: [
event,
...otherRowEvents
]
});
}
});
return eventRows;
}
export function getMonthView({ events = [], viewDate, weekStartsOn, excluded = [] }: { events?: CalendarEvent[], viewDate: Date, weekStartsOn: number, excluded?: number[] }): MonthView {
if (!events) {
events = [];
}
const start: Date = dateFns.startOfWeek(dateFns.startOfMonth(viewDate), { weekStartsOn });
const end: Date = dateFns.endOfWeek(dateFns.endOfMonth(viewDate), { weekStartsOn });
const eventsInMonth: CalendarEvent[] = getEventsInPeriod({
events,
periodStart: start,
periodEnd: end
});
const days: MonthViewDay[] = [];
for (let i: number = 0; i < dateFns.differenceInDays(end, start) + 1; i++) {
const date: Date = dateFns.addDays(start, i);
if (!excluded.some(e => date.getDay() === e)) {
const day: MonthViewDay = getWeekDay({ date }) as MonthViewDay;
const calEvents: CalendarEvent[] = getEventsInPeriod({
events: eventsInMonth,
periodStart: dateFns.startOfDay(date),
periodEnd: dateFns.endOfDay(date)
});
day.inMonth = dateFns.isSameMonth(date, viewDate);
day.events = calEvents;
day.badgeTotal = calEvents.length;
days.push(day);
}
}
const totalDaysVisibleInWeek: number = DAYS_IN_WEEK - excluded.length;
const rows: number = Math.floor(days.length / totalDaysVisibleInWeek);
const rowOffsets: number[] = [];
for (let i: number = 0; i < rows; i++) {
rowOffsets.push(i * totalDaysVisibleInWeek);
}
return {
rowOffsets,
totalDaysVisibleInWeek,
days
};
}
export function getDayView({ events = [], viewDate, hourSegments, dayStart, dayEnd, eventWidth, segmentHeight }: GetDayViewArgs): DayView {
if (!events) {
events = [];
}
const startOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfDay(viewDate), dayStart.hour), dayStart.minute);
const endOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfMinute(dateFns.endOfDay(viewDate)), dayEnd.hour), dayEnd.minute);
const previousDayEvents: DayViewEvent[] = [];
const dayViewEvents: DayViewEvent[] = getEventsInTimeRange(getEventsInPeriod({
events: events.filter((event: CalendarEvent) => !event.allDay),
periodStart: startOfView,
periodEnd: endOfView
}), dayStart, dayEnd).sort((eventA: CalendarEvent, eventB: CalendarEvent) => {
return eventA.start.valueOf() - eventB.start.valueOf();
}).map((event: CalendarEvent) => {
const eventStart: Date = event.start;
const eventEnd: Date = event.end || eventStart;
const startsBeforeDay: boolean = eventStart < startOfView;
const endsAfterDay: boolean = eventEnd > endOfView;
const hourHeightModifier: number = (hourSegments * segmentHeight) / MINUTES_IN_HOUR;
let top: number = 0;
if (eventStart > startOfView) {
top += dateFns.differenceInMinutes(eventStart, startOfView);
}
top *= hourHeightModifier;
const startDate: Date = startsBeforeDay ? startOfView : eventStart;
const endDate: Date = endsAfterDay ? endOfView : eventEnd;
let height: number = dateFns.differenceInMinutes(endDate, startDate);
if (!event.end) {
height = segmentHeight;
} else {
height *= hourHeightModifier;
}
const bottom: number = top + height;
const overlappingPreviousEvents: DayViewEvent[] = previousDayEvents.filter((previousEvent: DayViewEvent) => {
const previousEventTop: number = previousEvent.top;
const previousEventBottom: number = previousEvent.top + previousEvent.height;
if (top < previousEventBottom && previousEventBottom < bottom) {
return true;
} else if (previousEventTop <= top && bottom <= previousEventBottom) {
return true;
}
return false;
});
let left: number = 0;
while (overlappingPreviousEvents.some(previousEvent => previousEvent.left === left)) {
left += eventWidth;
}
const dayEvent: DayViewEvent = {
event,
height,
width: eventWidth,
top,
left,
startsBeforeDay,
endsAfterDay
};
if (height > 0) {
previousDayEvents.push(dayEvent);
}
return dayEvent;
}).filter((dayEvent: DayViewEvent) => dayEvent.height > 0);
const width: number = Math.max(...dayViewEvents.map((event: DayViewEvent) => event.left + event.width));
const allDayEvents: CalendarEvent[] = getEventsInPeriod({
events: events.filter((event: CalendarEvent) => event.allDay),
periodStart: dateFns.startOfDay(startOfView),
periodEnd: dateFns.endOfDay(endOfView)
});
return {
events: dayViewEvents,
width,
allDayEvents
};
}
export function getDayViewHourGrid(
{ viewDate, hourSegments, dayStart, dayEnd }:
{ viewDate: Date, hourSegments: number, dayStart: any, dayEnd: any }
): DayViewHour[] {
const hours: DayViewHour[] = [];
const startOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfDay(viewDate), dayStart.hour), dayStart.minute);
const endOfView: Date = dateFns.setMinutes(dateFns.setHours(dateFns.startOfMinute(dateFns.endOfDay(viewDate)), dayEnd.hour), dayEnd.minute);
const segmentDuration: number = MINUTES_IN_HOUR / hourSegments;
const startOfViewDay: Date = dateFns.startOfDay(viewDate);
for (let i: number = 0; i < HOURS_IN_DAY; i++) {
const segments: DayViewHourSegment[] = [];
for (let j: number = 0; j < hourSegments; j++) {
const date: Date = dateFns.addMinutes(dateFns.addHours(startOfViewDay, i), j * segmentDuration);
if (date >= startOfView && date < endOfView) {
segments.push({
date,
isStart: j === 0
});
}
}
if (segments.length > 0) {
hours.push({ segments });
}
}
return hours;
}