UNPKG

v-calendar

Version:

A calendar and date picker plugin for Vue.js.

602 lines (577 loc) 16.3 kB
import { DateSource, DayParts, DateParts, MonthParts, daysInWeek, weeksInMonth, addDays, addMonths, getDayIndex, } from './date/helpers'; import Locale from './locale'; import { pad, pick } from './helpers'; export interface CalendarDay extends DayParts { id: string; position: number; label: string; ariaLabel: string; weekdayPosition: number; weekdayPositionFromEnd: number; weekPosition: number; isoWeeknumber: number; startDate: Date; noonDate: Date; endDate: Date; isToday: boolean; isFirstDay: boolean; isLastDay: boolean; isDisabled: boolean; isFocusable: boolean; isFocused: boolean; inMonth: boolean; inPrevMonth: boolean; inNextMonth: boolean; onTop: boolean; onBottom: boolean; onLeft: boolean; onRight: boolean; classes: Array<string | Object>; locale: Locale; } export interface CalendarWeek { id: string; week: number; weekPosition: number; weeknumber: number; isoWeeknumber: number; weeknumberDisplay?: number; days: CalendarDay[]; title: string; } export interface CalendarWeekday { weekday: number; label: string; } export type PageView = 'daily' | 'weekly' | 'monthly'; export type TitlePosition = 'center' | 'left' | 'right'; export interface Page { id: string; day?: number; week?: number; month: number; year: number; view: PageView; trimWeeks: boolean; position: number; row: number; rowFromEnd: number; column: number; columnFromEnd: number; showWeeknumbers: boolean; showIsoWeeknumbers: boolean; weeknumberPosition: string; monthTitle: string; weekTitle?: string; dayTitle?: string; title: string; titlePosition: TitlePosition; shortMonthLabel: string; monthLabel: string; shortYearLabel: string; yearLabel: string; monthComps: MonthParts; prevMonthComps: MonthParts; nextMonthComps: MonthParts; days: CalendarDay[]; weeks: CalendarWeek[]; weekdays: CalendarWeekday[]; viewDays: CalendarDay[]; viewWeeks: CalendarWeek[]; } export type PageAddress = Pick<Page, 'day' | 'week' | 'month' | 'year'>; export type PageConfig = Pick< Page, | 'day' | 'week' | 'month' | 'year' | 'view' | 'titlePosition' | 'trimWeeks' | 'position' | 'row' | 'rowFromEnd' | 'column' | 'columnFromEnd' | 'showWeeknumbers' | 'showIsoWeeknumbers' | 'weeknumberPosition' >; export type CachedPage = Pick< Page, | 'id' | 'month' | 'year' | 'monthTitle' | 'shortMonthLabel' | 'monthLabel' | 'shortYearLabel' | 'yearLabel' | 'monthComps' | 'prevMonthComps' | 'nextMonthComps' | 'days' | 'weeks' | 'weekdays' >; const viewAddressKeys: Record<PageView, (keyof DateParts)[]> = { daily: ['year', 'month', 'day'], weekly: ['year', 'month', 'week'], monthly: ['year', 'month'], }; function getDays( { monthComps, prevMonthComps, nextMonthComps, }: Pick<Page, 'monthComps' | 'prevMonthComps' | 'nextMonthComps'>, locale: Locale, ): CalendarDay[] { const days: CalendarDay[] = []; const { firstDayOfWeek, firstWeekday, isoWeeknumbers, weeknumbers, numDays, numWeeks, } = monthComps; const prevMonthDaysToShow = firstWeekday + (firstWeekday < firstDayOfWeek ? daysInWeek : 0) - firstDayOfWeek; let prevMonth = true; let thisMonth = false; let nextMonth = false; let position = 0; // Formatter for aria labels const formatter = new Intl.DateTimeFormat(locale.id, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', }); // Init counters with previous month's data let day = prevMonthComps.numDays - prevMonthDaysToShow + 1; let dayFromEnd = prevMonthComps.numDays - day + 1; let weekdayOrdinal = Math.floor((day - 1) / daysInWeek + 1); let weekdayOrdinalFromEnd = 1; let week = prevMonthComps.numWeeks; let weekFromEnd = 1; let month = prevMonthComps.month; let year = prevMonthComps.year; // Store todays comps const today = new Date(); const todayDay = today.getDate(); const todayMonth = today.getMonth() + 1; const todayYear = today.getFullYear(); // Cycle through max weeks in month for (let w = 1; w <= weeksInMonth; w++) { // Cycle through days in week for ( let i = 1, weekday = firstDayOfWeek; i <= daysInWeek; i++, weekday += weekday === daysInWeek ? 1 - daysInWeek : 1 ) { // We need to know when to start counting actual month days if (prevMonth && weekday === firstWeekday) { // Reset counters for current month day = 1; dayFromEnd = monthComps.numDays; weekdayOrdinal = Math.floor((day - 1) / daysInWeek + 1); weekdayOrdinalFromEnd = Math.floor((numDays - day) / daysInWeek + 1); week = 1; weekFromEnd = numWeeks; month = monthComps.month; year = monthComps.year; // ...and flag we're tracking actual month days prevMonth = false; thisMonth = true; } const startDate = locale.getDateFromParams(year, month, day, 0, 0, 0, 0); const noonDate = locale.getDateFromParams(year, month, day, 12, 0, 0, 0); const endDate = locale.getDateFromParams( year, month, day, 23, 59, 59, 999, ); const date = startDate; const id = `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}`; const weekdayPosition = i; const weekdayPositionFromEnd = daysInWeek - i; const weeknumber = weeknumbers[w - 1]; const isoWeeknumber = isoWeeknumbers[w - 1]; const isToday = day === todayDay && month === todayMonth && year === todayYear; const isFirstDay = thisMonth && day === 1; const isLastDay = thisMonth && day === numDays; const onTop = w === 1; const onBottom = w === numWeeks; const onLeft = i === 1; const onRight = i === daysInWeek; const dayIndex = getDayIndex(year, month, day); days.push({ locale, id, position: ++position, label: day.toString(), ariaLabel: formatter.format(new Date(year, month - 1, day)), day, dayFromEnd, weekday, weekdayPosition, weekdayPositionFromEnd, weekdayOrdinal, weekdayOrdinalFromEnd, week, weekFromEnd, weekPosition: w, weeknumber, isoWeeknumber, month, year, date, startDate, endDate, noonDate, dayIndex, isToday, isFirstDay, isLastDay, isDisabled: !thisMonth, isFocusable: !thisMonth, isFocused: false, inMonth: thisMonth, inPrevMonth: prevMonth, inNextMonth: nextMonth, onTop, onBottom, onLeft, onRight, classes: [ `id-${id}`, `day-${day}`, `day-from-end-${dayFromEnd}`, `weekday-${weekday}`, `weekday-position-${weekdayPosition}`, `weekday-ordinal-${weekdayOrdinal}`, `weekday-ordinal-from-end-${weekdayOrdinalFromEnd}`, `week-${week}`, `week-from-end-${weekFromEnd}`, { 'is-today': isToday, 'is-first-day': isFirstDay, 'is-last-day': isLastDay, 'in-month': thisMonth, 'in-prev-month': prevMonth, 'in-next-month': nextMonth, 'on-top': onTop, 'on-bottom': onBottom, 'on-left': onLeft, 'on-right': onRight, }, ], }); // See if we've hit the last day of the month if (thisMonth && isLastDay) { thisMonth = false; nextMonth = true; // Reset counters to next month's data day = 1; dayFromEnd = numDays; weekdayOrdinal = 1; weekdayOrdinalFromEnd = Math.floor((numDays - day) / daysInWeek + 1); week = 1; weekFromEnd = nextMonthComps.numWeeks; month = nextMonthComps.month; year = nextMonthComps.year; // Still in the middle of the month (hasn't ended yet) } else { day++; dayFromEnd--; weekdayOrdinal = Math.floor((day - 1) / daysInWeek + 1); weekdayOrdinalFromEnd = Math.floor((numDays - day) / daysInWeek + 1); } } // Append week days week++; weekFromEnd--; } return days; } function getWeeks( days: CalendarDay[], showWeeknumbers: boolean, showIsoWeeknumbers: boolean, locale: Locale, ): CalendarWeek[] { const result = days.reduce((result: CalendarWeek[], day: CalendarDay, i) => { const weekIndex = Math.floor(i / 7); let week = result[weekIndex]; if (!week) { week = { id: `week-${weekIndex + 1}`, title: '', week: day.week, weekPosition: day.weekPosition, weeknumber: day.weeknumber, isoWeeknumber: day.isoWeeknumber, weeknumberDisplay: showWeeknumbers ? day.weeknumber : showIsoWeeknumbers ? day.isoWeeknumber : undefined, days: [], }; result[weekIndex] = week; } week.days.push(day); return result; }, Array(days.length / daysInWeek)); result.forEach(week => { const fromDay = week.days[0]; const toDay = week.days[week.days.length - 1]; if (fromDay.month === toDay.month) { week.title = `${locale.formatDate(fromDay.date, 'MMMM YYYY')}`; } else if (fromDay.year === toDay.year) { week.title = `${locale.formatDate( fromDay.date, 'MMM', )} - ${locale.formatDate(toDay.date, 'MMM YYYY')}`; } else { week.title = `${locale.formatDate( fromDay.date, 'MMM YYYY', )} - ${locale.formatDate(toDay.date, 'MMM YYYY')}`; } }); return result; } function getWeekdays(week: CalendarWeek, locale: Locale): CalendarWeekday[] { return week.days.map(day => ({ label: locale.formatDate(day.date, locale.masks.weekdays), weekday: day.weekday, })); } export function getPageAddressForDate( date: DateSource, view: PageView, locale: Locale, ) { return pick( locale.getDateParts(locale.toDate(date)), viewAddressKeys[view], ) as PageAddress; } export function addPages( { day, week, month, year }: PageAddress, count: number, view: PageView, locale: Locale, ): PageAddress { if (view === 'daily' && day) { const date = new Date(year, month - 1, day); const newDate = addDays(date, count); return { day: newDate.getDate(), month: newDate.getMonth() + 1, year: newDate.getFullYear(), }; } else if (view === 'weekly' && week) { const comps = locale.getMonthParts(month, year); const date = comps.firstDayOfMonth; const newDate = addDays(date, (week - 1 + count) * 7); const parts = locale.getDateParts(newDate); return { week: parts.week, month: parts.month, year: parts.year, }; } else { const date = new Date(year, month - 1, 1); const newDate = addMonths(date, count); return { month: newDate.getMonth() + 1, year: newDate.getFullYear(), }; } } export function pageIsValid(page: PageAddress | null | undefined) { return page != null && page.month != null && page.year != null; } export function pageIsBeforePage( page: PageAddress | null | undefined, comparePage: PageAddress | null | undefined, ) { if (!pageIsValid(page) || !pageIsValid(comparePage)) return false; page = page as PageAddress; comparePage = comparePage as PageAddress; if (page.year !== comparePage.year) return page.year < comparePage.year; if (page.month && comparePage.month && page.month !== comparePage.month) return page.month < comparePage.month; if (page.week && comparePage.week && page.week !== comparePage.week) { return page.week < comparePage.week; } if (page.day && comparePage.day && page.day !== comparePage.day) { return page.day < comparePage.day; } return false; } export function pageIsAfterPage( page: PageAddress | null | undefined, comparePage: PageAddress | null | undefined, ) { if (!pageIsValid(page) || !pageIsValid(comparePage)) return false; page = page as PageAddress; comparePage = comparePage as PageAddress; if (page.year !== comparePage.year) { return page.year > comparePage.year; } if (page.month && comparePage.month && page.month !== comparePage.month) { return page.month > comparePage.month; } if (page.week && comparePage.week && page.week !== comparePage.week) { return page.week > comparePage.week; } if (page.day && comparePage.day && page.day !== comparePage.day) { return page.day > comparePage.day; } return false; } export function pageIsBetweenPages( page: PageAddress | null | undefined, fromPage: PageAddress | null | undefined, toPage: PageAddress | null | undefined, ) { return ( (page || false) && !pageIsBeforePage(page, fromPage) && !pageIsAfterPage(page, toPage) ); } export function pageIsEqualToPage( aPage: PageAddress | null | undefined, bPage: PageAddress | null | undefined, ) { if (!aPage && bPage) return false; if (aPage && !bPage) return false; if (!aPage && !bPage) return true; aPage = aPage as PageAddress; bPage = bPage as PageAddress; return ( aPage.year === bPage.year && aPage.month === bPage.month && aPage.week === bPage.week && aPage.day === bPage.day ); } export function pageRangeToArray( from: PageAddress, to: PageAddress, view: PageView, locale: Locale, ) { if (!pageIsValid(from) || !pageIsValid(to)) return []; const result = []; while (!pageIsAfterPage(from, to)) { result.push(from); from = addPages(from, 1, view, locale); } return result; } export function getPageKey(config: PageConfig) { const { day, week, month, year } = config; let id = `${year}-${pad(month, 2)}`; if (week) id = `${id}-w${week}`; if (day) id = `${id}-${pad(day, 2)}`; return id; } export function getCachedPage(config: PageConfig, locale: Locale): CachedPage { const { month, year, showWeeknumbers, showIsoWeeknumbers } = config; const date = new Date(year, month - 1, 15); const monthComps = locale.getMonthParts(month, year); const prevMonthComps = locale.getPrevMonthParts(month, year); const nextMonthComps = locale.getNextMonthParts(month, year); const days = getDays({ monthComps, prevMonthComps, nextMonthComps }, locale); const weeks = getWeeks(days, showWeeknumbers, showIsoWeeknumbers, locale); const weekdays = getWeekdays(weeks[0], locale); return { id: getPageKey(config), month, year, monthTitle: locale.formatDate(date, locale.masks.title), shortMonthLabel: locale.formatDate(date, 'MMM'), monthLabel: locale.formatDate(date, 'MMMM'), shortYearLabel: year.toString().substring(2), yearLabel: year.toString(), monthComps, prevMonthComps, nextMonthComps, days, weeks, weekdays, }; } export function getPage(config: PageConfig, cachedPage: CachedPage) { const { day, week, view, trimWeeks } = config; const page: Page = { ...cachedPage, ...config, title: '', viewDays: [], viewWeeks: [], }; switch (view) { case 'daily': { let dayObj = page.days.find(d => d.inMonth)!; if (day) { dayObj = page.days.find(d => d.day === day && d.inMonth) || dayObj; } else if (week) { dayObj = page.days.find(d => d.week === week && d.inMonth)!; } const weekObj = page.weeks[dayObj.week - 1]; page.viewWeeks = [weekObj]; page.viewDays = [dayObj]; page.week = dayObj.week; page.weekTitle = weekObj.title; page.day = dayObj.day; page.dayTitle = dayObj.ariaLabel; page.title = page.dayTitle; break; } case 'weekly': { page.week = week || 1; const weekObj = page.weeks[page.week - 1]; page.viewWeeks = [weekObj]; page.viewDays = weekObj.days; page.weekTitle = weekObj.title; page.title = page.weekTitle; break; } default: { page.title = page.monthTitle; page.viewWeeks = page.weeks.slice( 0, trimWeeks ? page.monthComps.numWeeks : undefined, ); page.viewDays = page.days; break; } } return page; }