UNPKG

v-calendar

Version:

A clean and extendable plugin for building simple attributed calendars in Vue.js.

977 lines (938 loc) 27.3 kB
/* eslint-disable no-bitwise, no-multi-assign, import/no-cycle */ import toDate from 'date-fns-tz/toDate'; import getISOWeek from 'date-fns/getISOWeek'; import getWeek from 'date-fns/getWeek'; import getWeeksInMonth from 'date-fns/getWeeksInMonth'; import addDays from 'date-fns/addDays'; import DateInfo from './dateInfo'; import defaultLocales from './defaults/locales'; import { pad, addPages, arrayHasItems } from './helpers'; import { isDate, isNumber, isString, isObject, isArray, has, defaultsDeep, clamp, pick, } from './_'; export const PATCH = { DATE_TIME: 1, DATE: 2, TIME: 3, }; const PATCH_KEYS = { 1: ['year', 'month', 'day', 'hours', 'minutes', 'seconds', 'milliseconds'], 2: ['year', 'month', 'day'], 3: ['hours', 'minutes', 'seconds', 'milliseconds'], }; const token = /d{1,2}|W{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|Z{1,4}|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g; const twoDigits = /\d\d?/; const threeDigits = /\d{3}/; const fourDigits = /\d{4}/; const word = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}/i; const literal = /\[([^]*?)\]/gm; const noop = () => {}; const monthUpdate = arrName => (d, v, l) => { const index = l[arrName].indexOf( v.charAt(0).toUpperCase() + v.substr(1).toLowerCase(), ); if (~index) { d.month = index; } }; const maskMacros = ['L', 'iso']; const daysInWeek = 7; const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const hourOptions = [ { value: 0, label: '00' }, { value: 1, label: '01' }, { value: 2, label: '02' }, { value: 3, label: '03' }, { value: 4, label: '04' }, { value: 5, label: '05' }, { value: 6, label: '06' }, { value: 7, label: '07' }, { value: 8, label: '08' }, { value: 9, label: '09' }, { value: 10, label: '10' }, { value: 11, label: '11' }, { value: 12, label: '12' }, { value: 13, label: '13' }, { value: 14, label: '14' }, { value: 15, label: '15' }, { value: 16, label: '16' }, { value: 17, label: '17' }, { value: 18, label: '18' }, { value: 19, label: '19' }, { value: 20, label: '20' }, { value: 21, label: '21' }, { value: 22, label: '22' }, { value: 23, label: '23' }, ]; const formatFlags = { D(d) { return d.day; }, DD(d) { return pad(d.day); }, Do(d, l) { return l.DoFn(d.day); }, d(d) { return d.weekday - 1; }, dd(d) { return pad(d.weekday - 1); }, W(d, l) { return l.dayNamesNarrow[d.weekday - 1]; }, WW(d, l) { return l.dayNamesShorter[d.weekday - 1]; }, WWW(d, l) { return l.dayNamesShort[d.weekday - 1]; }, WWWW(d, l) { return l.dayNames[d.weekday - 1]; }, M(d) { return d.month; }, MM(d) { return pad(d.month); }, MMM(d, l) { return l.monthNamesShort[d.month - 1]; }, MMMM(d, l) { return l.monthNames[d.month - 1]; }, YY(d) { return String(d.year).substr(2); }, YYYY(d) { return pad(d.year, 4); }, h(d) { return d.hours % 12 || 12; }, hh(d) { return pad(d.hours % 12 || 12); }, H(d) { return d.hours; }, HH(d) { return pad(d.hours); }, m(d) { return d.minutes; }, mm(d) { return pad(d.minutes); }, s(d) { return d.seconds; }, ss(d) { return pad(d.seconds); }, S(d) { return Math.round(d.milliseconds / 100); }, SS(d) { return pad(Math.round(d.milliseconds / 10), 2); }, SSS(d) { return pad(d.milliseconds, 3); }, a(d, l) { return d.hours < 12 ? l.amPm[0] : l.amPm[1]; }, A(d, l) { return d.hours < 12 ? l.amPm[0].toUpperCase() : l.amPm[1].toUpperCase(); }, Z() { return 'Z'; }, ZZ(d) { const o = d.timezoneOffset; return `${o > 0 ? '-' : '+'}${pad(Math.floor(Math.abs(o) / 60), 2)}`; }, ZZZ(d) { const o = d.timezoneOffset; return `${o > 0 ? '-' : '+'}${pad( Math.floor(Math.abs(o) / 60) * 100 + (Math.abs(o) % 60), 4, )}`; }, ZZZZ(d) { const o = d.timezoneOffset; return `${o > 0 ? '-' : '+'}${pad(Math.floor(Math.abs(o) / 60), 2)}:${pad( Math.abs(o) % 60, 2, )}`; }, }; const parseFlags = { D: [ twoDigits, (d, v) => { d.day = v; }, ], Do: [ new RegExp(twoDigits.source + word.source), (d, v) => { d.day = parseInt(v, 10); }, ], d: [twoDigits, noop], W: [word, noop], M: [ twoDigits, (d, v) => { d.month = v - 1; }, ], MMM: [word, monthUpdate('monthNamesShort')], MMMM: [word, monthUpdate('monthNames')], YY: [ twoDigits, (d, v) => { const da = new Date(); const cent = +da.getFullYear().toString().substr(0, 2); d.year = `${v > 68 ? cent - 1 : cent}${v}`; }, ], YYYY: [ fourDigits, (d, v) => { d.year = v; }, ], S: [ /\d/, (d, v) => { d.millisecond = v * 100; }, ], SS: [ /\d{2}/, (d, v) => { d.millisecond = v * 10; }, ], SSS: [ threeDigits, (d, v) => { d.millisecond = v; }, ], h: [ twoDigits, (d, v) => { d.hour = v; }, ], m: [ twoDigits, (d, v) => { d.minute = v; }, ], s: [ twoDigits, (d, v) => { d.second = v; }, ], a: [ word, (d, v, l) => { const val = v.toLowerCase(); if (val === l.amPm[0]) { d.isPm = false; } else if (val === l.amPm[1]) { d.isPm = true; } }, ], Z: [ /[^\s]*?[+-]\d\d:?\d\d|[^\s]*?Z?/, (d, v) => { if (v === 'Z') v = '+00:00'; const parts = `${v}`.match(/([+-]|\d\d)/gi); if (parts) { const minutes = +(parts[1] * 60) + parseInt(parts[2], 10); d.timezoneOffset = parts[0] === '+' ? minutes : -minutes; } }, ], }; parseFlags.DD = parseFlags.D; parseFlags.dd = parseFlags.d; parseFlags.WWWW = parseFlags.WWW = parseFlags.WW = parseFlags.W; parseFlags.MM = parseFlags.M; parseFlags.mm = parseFlags.m; parseFlags.hh = parseFlags.H = parseFlags.HH = parseFlags.h; parseFlags.ss = parseFlags.s; parseFlags.A = parseFlags.a; parseFlags.ZZZZ = parseFlags.ZZZ = parseFlags.ZZ = parseFlags.Z; export function resolveConfig(config, locales) { // Get the detected locale string const detLocale = new Intl.DateTimeFormat().resolvedOptions().locale; // Resolve the locale id let id; if (isString(config)) { id = config; } else if (has(config, 'id')) { id = config.id; } id = (id || detLocale).toLowerCase(); const localeKeys = Object.keys(locales); const validKey = k => localeKeys.find(lk => lk.toLowerCase() === k); id = validKey(id) || validKey(id.substring(0, 2)) || detLocale; // Add fallback and spread default locale to prevent repetitive update loops const defLocale = { ...locales['en-IE'], ...locales[id], id }; // Assign or merge defaults with provided config config = isObject(config) ? defaultsDeep(config, defLocale) : defLocale; // Return resolved config return config; } export default class Locale { constructor(config, { locales = defaultLocales, timezone } = {}) { const { id, firstDayOfWeek, masks } = resolveConfig(config, locales); this.id = id; this.daysInWeek = daysInWeek; this.firstDayOfWeek = clamp(firstDayOfWeek, 1, daysInWeek); this.masks = masks; this.timezone = timezone || undefined; this.dayNames = this.getDayNames('long'); this.dayNamesShort = this.getDayNames('short'); this.dayNamesShorter = this.dayNamesShort.map(s => s.substring(0, 2)); this.dayNamesNarrow = this.getDayNames('narrow'); this.monthNames = this.getMonthNames('long'); this.monthNamesShort = this.getMonthNames('short'); this.amPm = ['am', 'pm']; this.monthData = {}; // Bind methods this.getMonthComps = this.getMonthComps.bind(this); this.parse = this.parse.bind(this); this.format = this.format.bind(this); this.toPage = this.toPage.bind(this); } format(date, mask) { date = this.normalizeDate(date); if (!date) return ''; mask = this.normalizeMasks(mask)[0]; const literals = []; // Make literals inactive by replacing them with ?? mask = mask.replace(literal, ($0, $1) => { literals.push($1); return '??'; }); const timezone = /Z$/.test(mask) ? 'utc' : this.timezone; const dateParts = this.getDateParts(date, timezone); // Apply formatting rules mask = mask.replace(token, $0 => $0 in formatFlags ? formatFlags[$0](dateParts, this) : $0.slice(1, $0.length - 1), ); // Inline literal values back into the formatted value return mask.replace(/\?\?/g, () => literals.shift()); } parse(dateString, mask) { const masks = this.normalizeMasks(mask); return ( masks .map(m => { if (typeof m !== 'string') { throw new Error('Invalid mask in fecha.parse'); } // Reset string value let str = dateString; // Avoid regular expression denial of service, fail early for really long strings // https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS if (str.length > 1000) { return false; } let isValid = true; const dateInfo = {}; m.replace(token, $0 => { if (parseFlags[$0]) { const info = parseFlags[$0]; const index = str.search(info[0]); if (!~index) { isValid = false; } else { str.replace(info[0], result => { info[1](dateInfo, result, this); str = str.substr(index + result.length); return result; }); } } return parseFlags[$0] ? '' : $0.slice(1, $0.length - 1); }); if (!isValid) { return false; } const today = new Date(); if ( dateInfo.isPm === true && dateInfo.hour != null && +dateInfo.hour !== 12 ) { dateInfo.hour = +dateInfo.hour + 12; } else if (dateInfo.isPm === false && +dateInfo.hour === 12) { dateInfo.hour = 0; } let date; if (dateInfo.timezoneOffset != null) { dateInfo.minute = +(dateInfo.minute || 0) - +dateInfo.timezoneOffset; date = new Date( Date.UTC( dateInfo.year || today.getFullYear(), dateInfo.month || 0, dateInfo.day || 1, dateInfo.hour || 0, dateInfo.minute || 0, dateInfo.second || 0, dateInfo.millisecond || 0, ), ); } else { date = this.getDateFromParts({ year: dateInfo.year || today.getFullYear(), month: (dateInfo.month || 0) + 1, day: dateInfo.day || 1, hours: dateInfo.hour || 0, minutes: dateInfo.minute || 0, seconds: dateInfo.second || 0, milliseconds: dateInfo.millisecond || 0, }); } return date; }) .find(d => d) || new Date(dateString) ); } // Normalizes mask(s) as an array with replaced mask macros normalizeMasks(masks) { return ( (arrayHasItems(masks) && masks) || [ (isString(masks) && masks) || 'YYYY-MM-DD', ] ).map(m => maskMacros.reduce( (prev, curr) => prev.replace(curr, this.masks[curr] || ''), m, ), ); } normalizeDate(d, config = {}) { let result = null; let { type, fillDate } = config; const { mask, patch, time } = config; const auto = type === 'auto' || !type; if (isNumber(d)) { type = 'number'; result = new Date(+d); } else if (isString(d)) { type = 'string'; result = d ? this.parse(d, mask || 'iso') : null; } else if (isObject(d)) { type = 'object'; result = this.getDateFromParts(d); } else { type = 'date'; result = isDate(d) ? new Date(d.getTime()) : null; } if (result && patch) { fillDate = fillDate == null ? new Date() : this.normalizeDate(fillDate); const parts = { ...this.getDateParts(fillDate), ...pick(this.getDateParts(result), PATCH_KEYS[patch]), }; result = this.getDateFromParts(parts); } if (auto) config.type = type; if (result && !isNaN(result.getTime())) { if (time) { result = this.adjustTimeForDate(result, { timeAdjust: time, }); } return result; } return null; } denormalizeDate(date, { type, mask } = {}) { switch (type) { case 'number': return date ? date.getTime() : NaN; case 'string': return date ? this.format(date, mask || 'iso') : ''; default: return date ? new Date(date) : null; } } hourIsValid(hour, validHours, dateParts) { if (!validHours) return true; if (isArray(validHours)) return validHours.includes(hour); if (isObject(validHours)) { const min = validHours.min || 0; const max = validHours.max || 24; return min <= hour && max >= hour; } return validHours(hour, dateParts); } getHourOptions(validHours, dateParts) { return hourOptions.filter(opt => this.hourIsValid(opt.value, validHours, dateParts), ); } getMinuteOptions(minuteIncrement) { const options = []; minuteIncrement = minuteIncrement > 0 ? minuteIncrement : 1; for (let i = 0; i <= 59; i += minuteIncrement) { options.push({ value: i, label: pad(i, 2), }); } return options; } nearestOptionValue(value, options) { if (value == null) return value; const result = options.reduce((prev, opt) => { if (opt.disabled) return prev; if (isNaN(prev)) return opt.value; const diffPrev = Math.abs(prev - value); const diffCurr = Math.abs(opt.value - value); return diffCurr < diffPrev ? opt.value : prev; }, NaN); return isNaN(result) ? value : result; } adjustTimeForDate(date, { timeAdjust, validHours, minuteIncrement }) { if (!timeAdjust && !validHours && !minuteIncrement) return date; const dateParts = this.getDateParts(date); if (timeAdjust) { if (timeAdjust === 'now') { const timeParts = this.getDateParts(new Date()); dateParts.hours = timeParts.hours; dateParts.minutes = timeParts.minutes; dateParts.seconds = timeParts.seconds; dateParts.milliseconds = timeParts.milliseconds; } else { const d = new Date(`2000-01-01T${timeAdjust}Z`); dateParts.hours = d.getUTCHours(); dateParts.minutes = d.getUTCMinutes(); dateParts.seconds = d.getUTCSeconds(); dateParts.milliseconds = d.getUTCMilliseconds(); } } if (validHours) { const options = this.getHourOptions(validHours, dateParts); dateParts.hours = this.nearestOptionValue(dateParts.hours, options); } if (minuteIncrement) { const options = this.getMinuteOptions(minuteIncrement); dateParts.minutes = this.nearestOptionValue(dateParts.minutes, options); } date = this.getDateFromParts(dateParts); return date; } normalizeDates(dates, opts) { opts = opts || {}; opts.locale = this; // Assign dates return (isArray(dates) ? dates : [dates]) .map(d => d && (d instanceof DateInfo ? d : new DateInfo(d, opts))) .filter(d => d); } getDateParts(date, timezone = this.timezone) { if (!date) return null; let tzDate = date; if (timezone) { const normDate = new Date( date.toLocaleString('en-US', { timeZone: timezone }), ); normDate.setMilliseconds(date.getMilliseconds()); const diff = normDate.getTime() - date.getTime(); tzDate = new Date(date.getTime() + diff); } const milliseconds = tzDate.getMilliseconds(); const seconds = tzDate.getSeconds(); const minutes = tzDate.getMinutes(); const hours = tzDate.getHours(); const month = tzDate.getMonth() + 1; const year = tzDate.getFullYear(); const comps = this.getMonthComps(month, year); const day = tzDate.getDate(); const dayFromEnd = comps.days - day + 1; const weekday = tzDate.getDay() + 1; const weekdayOrdinal = Math.floor((day - 1) / 7 + 1); const weekdayOrdinalFromEnd = Math.floor((comps.days - day) / 7 + 1); const week = Math.ceil( (day + Math.abs(comps.firstWeekday - comps.firstDayOfWeek)) / 7, ); const weekFromEnd = comps.weeks - week + 1; const parts = { milliseconds, seconds, minutes, hours, day, dayFromEnd, weekday, weekdayOrdinal, weekdayOrdinalFromEnd, week, weekFromEnd, month, year, date, isValid: true, }; parts.timezoneOffset = this.getTimezoneOffset(parts); return parts; } getDateFromParts(parts) { if (!parts) return null; const d = new Date(); const { year = d.getFullYear(), month = d.getMonth() + 1, day = d.getDate(), hours: hrs = 0, minutes: min = 0, seconds: sec = 0, milliseconds: ms = 0, } = parts; if (this.timezone) { const dateString = `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}T${pad( hrs, 2, )}:${pad(min, 2)}:${pad(sec, 2)}.${pad(ms, 3)}`; return toDate(dateString, { timeZone: this.timezone }); } return new Date(year, month - 1, day, hrs, min, sec, ms); } getTimezoneOffset(parts) { const { year: y, month: m, day: d, hours: hrs = 0, minutes: min = 0, seconds: sec = 0, milliseconds: ms = 0, } = parts; let date; const utcDate = new Date(Date.UTC(y, m - 1, d, hrs, min, sec, ms)); if (this.timezone) { const dateString = `${pad(y, 4)}-${pad(m, 2)}-${pad(d, 2)}T${pad( hrs, 2, )}:${pad(min, 2)}:${pad(sec, 2)}.${pad(ms, 3)}`; date = toDate(dateString, { timeZone: this.timezone }); } else { date = new Date(y, m - 1, d, hrs, min, sec, ms); } return (date - utcDate) / 60000; } toPage(arg, fromPage) { if (isNumber(arg)) { return addPages(fromPage, arg); } if (isString(arg)) { return this.getDateParts(this.normalizeDate(arg)); } if (isDate(arg)) { return this.getDateParts(arg); } if (isObject(arg)) { return arg; } return null; } getMonthDates(year = 2000) { const dates = []; for (let i = 0; i < 12; i++) { dates.push(new Date(year, i, 15)); } return dates; } getMonthNames(length) { const dtf = new Intl.DateTimeFormat(this.id, { month: length, timezome: 'UTC', }); return this.getMonthDates().map(d => dtf.format(d)); } getWeekdayDates(firstDayOfWeek = this.firstDayOfWeek) { const dates = []; const year = 2020; const month = 1; const day = 5 + firstDayOfWeek - 1; for (let i = 0; i < daysInWeek; i++) { dates.push( this.getDateFromParts({ year, month, day: day + i, hours: 12, }), ); } return dates; } getDayNames(length) { const dtf = new Intl.DateTimeFormat(this.id, { weekday: length, timeZone: this.timezone, }); return this.getWeekdayDates(1).map(d => dtf.format(d)); } // Days/month/year components for a given month and year getMonthComps(month, year) { const key = `${month}-${year}`; let comps = this.monthData[key]; if (!comps) { const inLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; const firstDayOfMonth = new Date(year, month - 1, 1); const firstWeekday = firstDayOfMonth.getDay() + 1; const days = month === 2 && inLeapYear ? 29 : daysInMonths[month - 1]; const weekStartsOn = this.firstDayOfWeek - 1; const weeks = getWeeksInMonth(firstDayOfMonth, { weekStartsOn, }); const weeknumbers = []; const isoWeeknumbers = []; for (let i = 0; i < weeks; i++) { const date = addDays(firstDayOfMonth, i * 7); weeknumbers.push(getWeek(date, { weekStartsOn })); isoWeeknumbers.push(getISOWeek(date)); } comps = { firstDayOfWeek: this.firstDayOfWeek, inLeapYear, firstWeekday, days, weeks, month, year, weeknumbers, isoWeeknumbers, }; this.monthData[key] = comps; } return comps; } // Days/month/year components for today's month getThisMonthComps() { const { month, year } = this.getDateParts(new Date()); return this.getMonthComps(month, year); } // Day/month/year components for previous month getPrevMonthComps(month, year) { if (month === 1) return this.getMonthComps(12, year - 1); return this.getMonthComps(month - 1, year); } // Day/month/year components for next month getNextMonthComps(month, year) { if (month === 12) return this.getMonthComps(1, year + 1); return this.getMonthComps(month + 1, year); } getDayId(date) { return this.format(date, 'YYYY-MM-DD'); } // Builds day components for a given page getCalendarDays({ weeks, monthComps, prevMonthComps, nextMonthComps }) { const days = []; const { firstDayOfWeek, firstWeekday, isoWeeknumbers, weeknumbers } = monthComps; const prevMonthDaysToShow = firstWeekday + (firstWeekday < firstDayOfWeek ? daysInWeek : 0) - firstDayOfWeek; let prevMonth = true; let thisMonth = false; let nextMonth = false; // Formatter for aria labels const formatter = new Intl.DateTimeFormat(this.id, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); // Init counters with previous month's data let day = prevMonthComps.days - prevMonthDaysToShow + 1; let dayFromEnd = prevMonthComps.days - day + 1; let weekdayOrdinal = Math.floor((day - 1) / daysInWeek + 1); let weekdayOrdinalFromEnd = 1; let week = prevMonthComps.weeks; 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(); const dft = (y, m, d) => (hours, minutes, seconds, milliseconds) => this.normalizeDate({ year: y, month: m, day: d, hours, minutes, seconds, milliseconds, }); // Cycle through 6 weeks (max in month) for (let w = 1; w <= weeks; 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.days; weekdayOrdinal = Math.floor((day - 1) / daysInWeek + 1); weekdayOrdinalFromEnd = Math.floor( (monthComps.days - day) / daysInWeek + 1, ); week = 1; weekFromEnd = monthComps.weeks; month = monthComps.month; year = monthComps.year; // ...and flag we're tracking actual month days prevMonth = false; thisMonth = true; } // Append day info for the current week // Note: this might or might not be an actual month day // We don't know how the UI wants to display various days, // so we'll supply all the data we can const dateFromTime = dft(year, month, day); const range = { start: dateFromTime(0, 0, 0), end: dateFromTime(23, 59, 59, 999), }; const date = range.start; 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 === monthComps.days; const onTop = w === 1; const onBottom = w === weeks; const onLeft = i === 1; const onRight = i === daysInWeek; days.push({ id, label: day.toString(), ariaLabel: formatter.format(new Date(year, month - 1, day)), day, dayFromEnd, weekday, weekdayPosition, weekdayPositionFromEnd, weekdayOrdinal, weekdayOrdinalFromEnd, week, weekFromEnd, weeknumber, isoWeeknumber, month, year, dateFromTime, date, range, isToday, isFirstDay, isLastDay, 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 = nextMonthComps.days; weekdayOrdinal = 1; weekdayOrdinalFromEnd = Math.floor( (nextMonthComps.days - day) / daysInWeek + 1, ); week = 1; weekFromEnd = nextMonthComps.weeks; 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( (monthComps.days - day) / daysInWeek + 1, ); } } // Append week days week++; weekFromEnd--; } return days; } }