UNPKG

novo-elements

Version:

Bullhorn's NOVO Element Repository for Angular 2

519 lines (425 loc) 17 kB
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; }