@quasar/quasar-ui-qcalendar
Version:
QCalendar - Day/Month/Week Calendars, Popups, Date Pickers, Schedules, Agendas, Planners and Tasks for your Vue Apps
1,247 lines (1,242 loc) • 120 kB
JavaScript
/*!
* @quasar/quasar-ui-qcalendar v4.1.2
* (c) 2025 Jeff Galbraith <jeff@quasar.dev>
* Released under the MIT License.
*/
'use strict';
var vue = require('vue');
const PARSE_DATETIME = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?([^\d]+(\d{1,2}))?(:(\d{1,2}))?(:(\d{1,2}))?(.(\d{1,3}))?$/;
const PARSE_DATE = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?/;
const PARSE_TIME = /(\d\d?)(:(\d\d?)|)(:(\d\d?)|)/;
const DAYS_IN_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_IN_MONTH_LEAP = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const TIME_CONSTANTS = {
MILLISECONDS_IN: {
SECOND: 1000,
MINUTE: 60000,
HOUR: 3600000,
DAY: 86400000,
WEEK: 604800000,
},
SECONDS_IN: {
MINUTE: 60,
HOUR: 3600,
DAY: 86400,
WEEK: 604800,
},
MINUTES_IN: {
MINUTE: 1,
HOUR: 60,
DAY: 1440,
WEEK: 10080,
},
HOURS_IN: {
DAY: 24,
WEEK: 168,
},
DAYS_IN: {
WEEK: 7,
},
};
const DAYS_IN_MONTH_MIN = 28;
const DAYS_IN_MONTH_MAX = 31;
const MONTH_MAX = 12;
const MONTH_MIN = 1;
const DAY_MIN = 1;
const FIRST_HOUR = 0;
/**
* Validates the passed input ('YYY-MM-DD') as a date or ('YYY-MM-DD HH:MM') date time combination
* @param {string} input A string in the form 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'
* @returns {boolean} True if parseable
*/
function validateTimestamp(input) {
if (typeof input !== 'string')
return false;
return PARSE_DATETIME.test(input);
}
/**
* Fast low-level parser for a date string ('YYYY-MM-DD'). Does not update formatted or relative date.
* Use 'parseTimestamp' for formatted and relative updates
* @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used)
* @returns {Timestamp} This {@link Timestamp} is minimally filled in. The {@link Timestamp.date} and {@link Timestamp.time} as well as relative data will not be filled in.
*/
function parsed(input) {
if (typeof input !== 'string')
return null;
const parts = PARSE_DATETIME.exec(input);
if (!parts || !parts[1] || !parts[2])
return null;
const year = parseInt(parts[1], 10);
const month = parseInt(parts[2], 10);
const day = parseInt(parts[4] || '1', 10);
const hour = parseInt(parts[6] || '0', 10);
const minute = parseInt(parts[8] || '0', 10);
return {
date: input,
time: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
year,
month,
day,
hour,
minute,
hasDay: !!parts[4],
hasTime: true, // time is always present, even if '00:00'
past: false,
current: false,
future: false,
disabled: false,
weekday: 0,
doy: 0,
workweek: 0,
};
}
/**
* Takes a JavaScript Date and returns a {@link Timestamp}. The {@link Timestamp} is not updated with relative information.
* @param {Date} date JavaScript Date
* @param {boolean} utc If set the {@link Timestamp} will parse the Date as UTC
* @returns {Timestamp} A minimal {@link Timestamp} without updated or relative updates.
*/
function parseDate(date, utc = false) {
if (!(date instanceof Date))
return null;
const UTC = utc ? 'UTC' : '';
return updateFormatted({
date: padNumber(date[`get${UTC}FullYear`](), 4) +
'-' +
padNumber(date[`get${UTC}Month`]() + 1, 2) +
'-' +
padNumber(date[`get${UTC}Date`](), 2),
time: padNumber(date[`get${UTC}Hours`]() || 0, 2) +
':' +
padNumber(date[`get${UTC}Minutes`]() || 0, 2),
year: date[`get${UTC}FullYear`](),
month: date[`get${UTC}Month`]() + 1,
day: date[`get${UTC}Date`](),
hour: date[`get${UTC}Hours`](),
minute: date[`get${UTC}Minutes`](),
weekday: 0,
doy: 0,
workweek: 0,
hasDay: true,
hasTime: true, // Date always has time, even if it is '00:00'
past: false,
current: false,
future: false,
disabled: false,
});
}
/**
* Padds a passed in number to length (converts to a string). Good for converting '5' as '05'.
* @param {number} x The number to pad
* @param {number} length The length of the required number as a string
* @returns {string} The padded number (as a string). (ie: 5 = '05')
*/
function padNumber(x, length) {
let padded = String(x);
while (padded.length < length) {
padded = '0' + padded;
}
return padded;
}
/**
* Returns if the passed year is a leap year
* @param {number} year The year to check (ie: 1999, 2020)
* @returns {boolean} True if the year is a leap year
*/
function isLeapYear(year) {
// A year is a Gregorian leap year if it is divisible by 4,
// but not by 100, unless it is also divisible by 400.
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
/**
* Returns the days of the specified month in a year
* @param {number} year The year (ie: 1999, 2020)
* @param {number} month The month (zero-based)
* @returns {number} The number of days in the month (corrected for leap years)
*/
function daysInMonth(year, month) {
return (isLeapYear(year) ? DAYS_IN_MONTH_LEAP[month] : DAYS_IN_MONTH[month]);
}
/**
* Returns a {@link Timestamp} of next day from passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {Timestamp} The modified {@link Timestamp} as the next day
*/
function nextDay(timestamp) {
const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day + 1);
return updateFormatted(normalizeTimestamp({
...timestamp,
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
}));
}
/**
* Returns a {@link Timestamp} of previous day from passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {Timestamp} The modified {@link Timestamp} as the previous day
*/
function prevDay(timestamp) {
const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day - 1);
return updateFormatted(normalizeTimestamp({
...timestamp,
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
}));
}
/**
* Returns today's date
* @returns {string} Date string in the form 'YYYY-MM-dd'
*/
function today() {
const d = new Date(), month = d.getMonth() + 1, day = d.getDate(), year = d.getFullYear();
return [year, padNumber(month, 2), padNumber(day, 2)].join('-');
}
/**
* Takes a date string ('YYYY-MM-DD') and validates if it is today's date
* @param {string} date Date string in the form 'YYYY-MM-DD'
* @returns {boolean} True if the date is today's date
*/
function isToday(date) {
return date === today();
}
/**
* Returns the start of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the start of the week).
* If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
* @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the week
* @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
* @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
* @returns {Timestamp} The {@link Timestamp} representing the start of the week
*/
function getStartOfWeek(timestamp, weekdays, today) {
let start = copyTimestamp(timestamp);
if (!weekdays) {
return start;
}
if (start.day === 1 || start.weekday === 0) {
while (!weekdays.includes(Number(start.weekday))) {
start = nextDay(start);
}
}
start = findWeekday(start, weekdays[0], prevDay);
start = updateFormatted(start);
if (today) {
start = updateRelative(start, today, start.hasTime);
}
return start;
}
/**
* Returns the end of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the last of the week).
* If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
* @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the week
* @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
* @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
* @returns {Timestamp} The {@link Timestamp} representing the end of the week
*/
function getEndOfWeek(timestamp, weekdays, today) {
let end = copyTimestamp(timestamp);
if (!weekdays || !Array.isArray(weekdays)) {
return end;
}
// is last day of month?
const lastDay = daysInMonth(end.year, end.month);
if (lastDay === end.day || end.weekday === weekdays[weekdays.length - 1]) {
while (!weekdays.includes(Number(end.weekday))) {
end = prevDay(end);
}
}
end = findWeekday(end, weekdays[weekdays.length - 1], nextDay);
end = updateFormatted(end);
if (today) {
end = updateRelative(end, today, end.hasTime);
}
return end;
}
/**
* Finds the start of the month based on the passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the month
* @returns {Timestamp} A {@link Timestamp} of the start of the month
*/
function getStartOfMonth(timestamp) {
let start = copyTimestamp(timestamp);
start.day = DAY_MIN;
start = updateFormatted(start);
return start;
}
/**
* Finds the end of the month based on the passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the month
* @returns {Timestamp} A {@link Timestamp} of the end of the month
*/
function getEndOfMonth(timestamp) {
let end = copyTimestamp(timestamp);
end.day = daysInMonth(end.year, end.month);
end = updateFormatted(end);
return end;
}
// returns minutes since midnight
function parseTime(input) {
const type = Object.prototype.toString.call(input);
switch (type) {
case '[object Number]':
// when a number is given, it's minutes since 12:00am
return input;
case '[object String]': {
// when a string is given, it's a hh:mm:ss format where seconds are optional, but not used
const parts = PARSE_TIME.exec(input);
if (!parts) {
return false;
}
return parseInt(parts[1], 10) * 60 + parseInt(parts[3] || '0', 10);
}
case '[object Object]':
// when an object is given, it must have hour and minute
if (typeof input !== 'object' ||
typeof input.hour !== 'number' ||
typeof input.minute !== 'number') {
return false;
}
if (typeof input === 'object' && 'hour' in input && 'minute' in input) {
return input.hour * 60 + input.minute;
}
return false;
}
return false;
}
/**
* Compares two {@link Timestamp}s for exactness
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @returns {boolean} True if the two {@link Timestamp}s are an exact match
*/
function compareTimestamps(ts1, ts2) {
if (!ts1 || !ts2)
return false;
return (ts1.year === ts2.year &&
ts1.month === ts2.month &&
ts1.day === ts2.day &&
ts1.hour === ts2.hour &&
ts1.minute === ts2.minute);
}
/**
* Compares the date of two {@link Timestamp}s that have been updated with relative data
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @returns {boolean} True if the two dates are the same
*/
function compareDate(ts1, ts2) {
return getDate(ts1) === getDate(ts2);
}
/**
* Compares the time of two {@link Timestamp}s that have been updated with relative data
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @returns {boolean} True if the two times are an exact match
*/
function compareTime(ts1, ts2) {
return getTime(ts1) === getTime(ts2);
}
/**
* Compares the date and time of two {@link Timestamp}s that have been updated with relative data
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @returns {boolean} True if the date and time are an exact match
*/
function compareDateTime(ts1, ts2) {
return getDateTime(ts1) === getDateTime(ts2);
}
/**
* High-level parser that converts the passed in string to {@link Timestamp} and uses 'now' to update relative information.
* @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used)
* @param {Timestamp} now A {@link Timestamp} to use for relative data updates
* @returns {Timestamp} The {@link Timestamp.date} will be filled in as well as the {@link Timestamp.time} if a time is supplied and formatted fields (doy, weekday, workweek, etc). If 'now' is supplied, then relative data will also be updated.
*/
function parseTimestamp(input, now = null) {
let timestamp = parsed(input);
if (!timestamp)
return null;
timestamp = updateFormatted(timestamp);
if (now) {
timestamp = updateRelative(timestamp, now, timestamp.hasTime);
}
return timestamp;
}
/**
* Converts a {@link Timestamp} into a numeric date identifier based on the passed {@link Timestamp}'s date
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The numeric date identifier
*/
function getDayIdentifier(timestamp) {
return ((timestamp.year ?? 0) * 100000000 +
(timestamp.month ?? 0) * 1000000 +
(timestamp.day ?? 0) * 10000);
}
/**
* Converts a {@link Timestamp} into a numeric time identifier based on the passed {@link Timestamp}'s time
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The numeric time identifier
*/
function getTimeIdentifier(timestamp) {
return (timestamp.hour ?? 0) * 100 + (timestamp.minute ?? 0);
}
/**
* Converts a {@link Timestamp} into a numeric date and time identifier based on the passed {@link Timestamp}'s date and time
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The numeric date+time identifier
*/
function getDayTimeIdentifier(timestamp) {
return getDayIdentifier(timestamp) + getTimeIdentifier(timestamp);
}
/**
* Returns the difference between two {@link Timestamp}s
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @param {boolean=} strict Optional flag to not to return negative numbers
* @returns {number} The difference
*/
function diffTimestamp(ts1, ts2, strict = false) {
const utc1 = Date.UTC(ts1.year ?? 0, (ts1.month ?? 1) - 1, ts1.day ?? 1, ts1.hour ?? 0, ts1.minute ?? 0);
const utc2 = Date.UTC(ts2.year ?? 0, (ts2.month ?? 1) - 1, ts2.day ?? 1, ts2.hour ?? 0, ts2.minute ?? 0);
if (strict === true && utc2 < utc1) {
// Not negative number
// utc2 - utc1 < 0 -> utc2 < utc1 -> NO: utc1 >= utc2
return 0;
}
return utc2 - utc1;
}
/**
* Updates a {@link Timestamp} with relative data (past, current and future)
* @param {Timestamp} timestamp The {@link Timestamp} that needs relative data updated
* @param {Timestamp} now {@link Timestamp} that represents the current date (optional time)
* @param {boolean=} time Optional flag to include time ('timestamp' and 'now' params should have time values)
* @returns {Timestamp} A new {@link Timestamp}
*/
function updateRelative(timestamp, now, time = false) {
let ts = copyTimestamp(timestamp);
let a = getDayIdentifier(now);
let b = getDayIdentifier(ts);
let current = a === b;
if (ts.hasTime && time && current) {
a = getTimeIdentifier(now);
b = getTimeIdentifier(ts);
current = a === b;
}
ts.past = b < a;
ts.current = current;
ts.future = b > a;
ts.currentWeekday = ts.weekday === now.weekday;
return ts;
}
/**
* Sets a Timestamp{@link Timestamp} to number of minutes past midnight (modifies hour and minutes if needed)
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @param {number} minutes The number of minutes to set from midnight
* @param {Timestamp=} now Optional {@link Timestamp} representing current date and time
* @returns {Timestamp} A new {@link Timestamp}
*/
function updateMinutes(timestamp, minutes, now = null) {
let ts = copyTimestamp(timestamp);
ts.hasTime = true;
ts.hour = Math.floor(minutes / TIME_CONSTANTS.MINUTES_IN.HOUR);
ts.minute = minutes % TIME_CONSTANTS.MINUTES_IN.HOUR;
ts.time = getTime(ts);
if (now) {
ts = updateRelative(ts, now, true);
}
return ts;
}
/**
* Updates the {@link Timestamp} with the weekday
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @returns A new Timestamp
*/
function updateWeekday(timestamp) {
let ts = copyTimestamp(timestamp);
ts.weekday = getWeekday(ts);
return ts;
}
/**
* Updates the {@link Timestamp} with the day of the year (doy)
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @returns A new Timestamp
*/
function updateDayOfYear(timestamp) {
let ts = copyTimestamp(timestamp);
ts.doy = getDayOfYear(ts) || 0;
return ts;
}
/**
* Updates the {@link Timestamp} with the workweek
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @returns A new {@link Timestamp}
*/
function updateWorkWeek(timestamp) {
let ts = copyTimestamp(timestamp);
ts.workweek = getWorkWeek(ts);
return ts;
}
/**
* Updates the passed {@link Timestamp} with disabled, if needed
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @param {string} [disabledBefore] In 'YYY-MM-DD' format
* @param {string} [disabledAfter] In 'YYY-MM-DD' format
* @param {number[]} [disabledWeekdays] An array of numbers representing weekdays [0 = Sun, ..., 6 = Sat]
* @param {string[]|string[][]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range.
* @returns A new {@link Timestamp}
*/
function updateDisabled(timestamp, disabledBefore, disabledAfter, disabledWeekdays, disabledDays) {
let ts = copyTimestamp(timestamp);
const t = getDayIdentifier(ts);
if (disabledBefore !== undefined) {
const disabledDay = parsed(disabledBefore);
if (disabledDay) {
const before = getDayIdentifier(disabledDay);
if (t <= before) {
ts.disabled = true;
}
}
}
if (ts.disabled !== true && disabledAfter !== undefined) {
const disabledDay = parsed(disabledAfter);
if (disabledDay) {
const after = getDayIdentifier(disabledDay);
if (t >= after) {
ts.disabled = true;
}
}
}
if (ts.disabled !== true && Array.isArray(disabledWeekdays) && disabledWeekdays.length > 0) {
for (const weekday in disabledWeekdays) {
if (disabledWeekdays[weekday] === ts.weekday) {
ts.disabled = true;
break;
}
}
}
if (ts.disabled !== true && Array.isArray(disabledDays) && disabledDays.length > 0) {
for (const day in disabledDays) {
if (Array.isArray(disabledDays[day]) &&
disabledDays[day].length === 2 &&
disabledDays[day][0] &&
disabledDays[day][1]) {
const start = parsed(disabledDays[day][0]);
const end = parsed(disabledDays[day][1]);
if (start && end && isBetweenDates(ts, start, end)) {
ts.disabled = true;
break;
}
}
else {
const disabledDayOrRange = disabledDays[day];
// handle ranges with multiple days
if (Array.isArray(disabledDayOrRange)) {
for (const range of disabledDayOrRange) {
const disabledDay = parseTimestamp(range);
if (disabledDay) {
const d = getDayIdentifier(disabledDay);
if (d === t) {
ts.disabled = true;
break;
}
}
}
}
else if (disabledDayOrRange) {
const disabledDay = parseTimestamp(disabledDayOrRange);
if (disabledDay) {
const d = getDayIdentifier(disabledDay);
if (d === t) {
ts.disabled = true;
}
}
}
}
}
}
return ts;
}
/**
* Updates the passed {@link Timestamp} with formatted data (time string, date string, weekday, day of year and workweek)
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @returns A new {@link Timestamp}
*/
function updateFormatted(timestamp) {
let ts = copyTimestamp(timestamp);
ts.hasTime = true;
ts.time = getTime(ts);
ts.date = getDate(ts);
ts.weekday = getWeekday(ts);
ts.doy = getDayOfYear(ts) || 0;
ts.workweek = getWorkWeek(ts);
return ts;
}
/**
* Returns day of the year (doy) for the passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The day of the year
*/
function getDayOfYear(timestamp) {
if (timestamp.year === 0)
return;
return ((Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day) -
Date.UTC(timestamp.year, 0, 0)) /
24 /
60 /
60 /
1000);
}
/**
* Returns workweek for the passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The work week
*/
function getWorkWeek(timestamp) {
let ts = copyTimestamp(timestamp);
if (ts.year === 0) {
const parsedToday = parseTimestamp(today());
if (parsedToday) {
ts = parsedToday;
}
}
// Remove time components of date
const weekday = new Date(Date.UTC(ts.year, ts.month - 1, ts.day));
// Adjust the date to the correct day of the week
const dayAdjustment = 4; // thursday is 4
weekday.setUTCDate(weekday.getUTCDate() - ((weekday.getUTCDay() + 6) % 7) + dayAdjustment);
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
weekday.setUTCDate(weekday.getUTCDate() + dayAdjustment - (weekday.getUTCDay() || 7));
// Get first day of year
var yearStart = new Date(Date.UTC(weekday.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
var weekNumber = Math.ceil(((weekday.valueOf() - yearStart.valueOf()) / 86400000 + 1) / 7);
return weekNumber;
}
/**
* Returns weekday for the passed in {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @returns {number} The weekday
*/
function getWeekday(timestamp) {
let weekday = timestamp.weekday;
if (timestamp.hasDay) {
const floor = Math.floor;
const day = timestamp.day;
const month = ((timestamp.month + 9) % MONTH_MAX) + 1;
const century = floor(timestamp.year / 100);
const year = (timestamp.year % 100) - (timestamp.month <= 2 ? 1 : 0);
weekday =
(((day +
floor(2.6 * month - 0.2) -
2 * century +
year +
floor(year / 4) +
floor(century / 4)) %
7) +
7) %
7;
}
return weekday ?? 0;
}
/**
* Makes a copy of the passed in {@link Timestamp}
* @param {Timestamp} timestamp The original {@link Timestamp}
* @returns {Timestamp} A copy of the original {@link Timestamp}
*/
function copyTimestamp(timestamp) {
return { ...timestamp };
}
/**
* Used internally to convert {@link Timestamp} used with 'parsed' or 'parseDate' so the 'date' portion of the {@link Timestamp} is correct.
* @param {Timestamp} timestamp The (raw) {@link Timestamp}
* @returns {string} A formatted date ('YYYY-MM-DD')
*/
function getDate(timestamp) {
let str = `${padNumber(timestamp.year, 4)}-${padNumber(timestamp.month, 2)}`;
if (timestamp.hasDay)
str += `-${padNumber(timestamp.day, 2)}`;
return str;
}
/**
* Used intenally to convert {@link Timestamp} with 'parsed' or 'parseDate' so the 'time' portion of the {@link Timestamp} is correct.
* @param {Timestamp} timestamp The (raw) {@link Timestamp}
* @returns {string} A formatted time ('hh:mm')
*/
function getTime(timestamp) {
if (!timestamp.hasTime) {
return '';
}
return `${padNumber(timestamp.hour, 2)}:${padNumber(timestamp.minute, 2)}`;
}
/**
* Returns a formatted string date and time ('YYYY-YY-MM hh:mm')
* @param {Timestamp} timestamp The {@link Timestamp}
* @returns {string} A formatted date time ('YYYY-MM-DD HH:mm')
*/
function getDateTime(timestamp) {
return getDate(timestamp) + ' ' + (timestamp.hasTime ? getTime(timestamp) : '00:00');
}
/**
* An alias for {relativeDays}
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
* @param {number} [days=1] The number of days to move.
* @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
* @returns The modified {@link Timestamp}
*/
function moveRelativeDays(timestamp, mover = nextDay, days = 1, allowedWeekdays = [0, 1, 2, 3, 4, 5, 6]) {
const ts = copyTimestamp(timestamp);
return relativeDays(ts, mover, days, allowedWeekdays);
}
/**
* Moves the {@link Timestamp} the number of relative days
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
* @param {number} [days=1] The number of days to move.
* @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
* @returns A new {@link Timestamp}
*/
function relativeDays(timestamp, mover = nextDay, days = 1, allowedWeekdays = [0, 1, 2, 3, 4, 5, 6]) {
let ts = copyTimestamp(timestamp);
if (!allowedWeekdays.includes(Number(ts.weekday)) && ts.weekday === 0 && mover === nextDay) {
++days;
}
while (--days >= 0) {
ts = mover(ts);
if (allowedWeekdays.length < 7 && !allowedWeekdays.includes(Number(ts.weekday))) {
++days;
}
}
return ts;
}
/**
* Finds the specified weekday (forward or back) based on the {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to modify
* @param {number} weekday The weekday number (Sun = 0, ..., Sat = 6)
* @param {function} [mover=nextDay] The function to use ({prevDay} or {nextDay}).
* @param {number} [maxDays=6] The number of days to look forward or back.
* @returns A new {@link Timestamp}
*/
function findWeekday(timestamp, weekday, mover = nextDay, maxDays = 6) {
let ts = copyTimestamp(timestamp);
while (ts.weekday !== weekday && --maxDays >= 0)
ts = mover(ts);
return ts;
}
/**
* Creates an array of {@link Timestamp}s based on start and end params
* @param {Timestamp} start The starting {@link Timestamp}
* @param {Timestamp} end The ending {@link Timestamp}
* @param {Timestamp} now The relative day
* @param {number[]} weekdays An array of numbers (representing days of the week) that are 0 (=Sunday) to 6 (=Saturday)
* @param {string} [disabledBefore] Days before this date are disabled (YYYY-MM-DD)
* @param {string} [disabledAfter] Days after this date are disabled (YYYY-MM-DD)
* @param {number[]} [disabledWeekdays] An array representing weekdays that are disabled [0 = Sun, ..., 6 = Sat]
* @param {string[]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range.
* @param {number} [max=42] Max days to do
* @param {number} [min=0] Min days to do
* @returns {Timestamp[]} The requested array of {@link Timestamp}s
*/
function createDayList(start, end, now, weekdays = [0, 1, 2, 3, 4, 5, 6], disabledBefore = undefined, disabledAfter = undefined, disabledWeekdays = [], disabledDays = [], max = 42, min = 0) {
const begin = getDayIdentifier(start);
const stop = getDayIdentifier(end);
const days = [];
let current = copyTimestamp(start);
let currentIdentifier = 0;
let stopped = currentIdentifier === stop;
if (stop < begin) {
return days;
}
while ((!stopped || days.length < min) && days.length < max) {
currentIdentifier = getDayIdentifier(current);
stopped = stopped || (currentIdentifier > stop && days.length >= min);
if (stopped) {
break;
}
if (!weekdays.includes(Number(current.weekday))) {
current = relativeDays(current, nextDay);
continue;
}
let day = copyTimestamp(current);
day = updateFormatted(day);
day = updateRelative(day, now);
day = updateDisabled(day, disabledBefore, disabledAfter, disabledWeekdays, disabledDays);
days.push(day);
current = relativeDays(current, nextDay);
}
return days;
}
/**
* Creates an array of interval {@link Timestamp}s based on params
* @param {Timestamp} timestamp The starting {@link Timestamp}
* @param {number} first The starting interval time
* @param {number} minutes How many minutes between intervals (ie: 60, 30, 15 would be common ones)
* @param {number} count The number of intervals needed
* @param {Timestamp} now A relative {@link Timestamp} with time
* @returns {Timestamp[]} The requested array of interval {@link Timestamp}s
*/
function createIntervalList(timestamp, first, minutes, count, now) {
const intervals = [];
for (let i = 0; i < count; ++i) {
const mins = (first + i) * minutes;
const ts = copyTimestamp(timestamp);
intervals.push(updateMinutes(ts, mins, now));
}
return intervals;
}
/**
* @callback getOptions
* @param {Timestamp} timestamp A {@link Timestamp} object
* @param {boolean} short True if using short options
* @returns {Object} An Intl object representing optioons to be used
*/
/**
* @callback formatter
* @param {Timestamp} timestamp The {@link Timestamp} being used
* @param {boolean} short If short format is being requested
* @returns {string} The localized string of the formatted {@link Timestamp}
*/
/**
* Returns a function that uses Intl.DateTimeFormat formatting
* @param {string} locale The locale to use (ie: en-US)
* @param {getOptions} cb The function to call for options. This function should return an Intl formatted object. The function is passed (timestamp, short).
* @returns {formatter} The function has params (timestamp, short). The short is to use the short options.
*/
function createNativeLocaleFormatter(locale, cb) {
const emptyFormatter = () => '';
/* istanbul ignore next */
if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
return emptyFormatter;
}
return (timestamp, short) => {
try {
const intlFormatter = new Intl.DateTimeFormat(locale || undefined, cb(timestamp, short));
return intlFormatter.format(makeDateTime(timestamp));
}
catch (e) /* istanbul ignore next */ {
console.error(`Intl.DateTimeFormat: ${e.message} -> ${getDateTime(timestamp)}`);
return '';
}
};
}
/**
* Makes a JavaScript Date from the passed {@link Timestamp}
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @param {boolean} utc True to get Date object using UTC
* @returns {Date} A JavaScript Date
*/
function makeDate(timestamp, utc = true) {
if (utc)
return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0));
return new Date(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0);
}
/**
* Makes a JavaScript Date from the passed {@link Timestamp} (with time)
* @param {Timestamp} timestamp The {@link Timestamp} to use
* @param {boolean} utc True to get Date object using UTC
* @returns {Date} A JavaScript Date
*/
function makeDateTime(timestamp, utc = true) {
if (utc)
return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute));
return new Date(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute);
}
/**
* Validates if the input is a finite number.
*
* @param input - The value to be validated. Can be a string or a number.
* @returns A boolean indicating whether the input is a finite number.
* Returns true if the input is a finite number, false otherwise.
*/
function validateNumber(input) {
return isFinite(Number(input));
}
/**
* Given an array of {@link Timestamp}s, finds the max date (and possible time)
* @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
* @param {boolean=} useTime Default false; if true, uses time in the comparison as well
* @returns The {@link Timestamp} with the highest date (and possibly time) value
*/
function maxTimestamp(timestamps, useTime = false) {
const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier;
return timestamps.reduce((prev, cur) => {
return Math.max(func(prev), func(cur)) === func(prev) ? prev : cur;
});
}
/**
* Given an array of {@link Timestamp}s, finds the min date (and possible time)
* @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
* @param {boolean=} useTime Default false; if true, uses time in the comparison as well
* @returns The {@link Timestamp} with the lowest date (and possibly time) value
*/
function minTimestamp(timestamps, useTime = false) {
const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier;
return timestamps.reduce((prev, cur) => {
return Math.min(func(prev), func(cur)) === func(prev) ? prev : cur;
});
}
/**
* Determines if the passed {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
* @param {Timestamp} timestamp The {@link Timestamp} for testing
* @param {Timestamp} startTimestamp The starting {@link Timestamp}
* @param {Timestamp} endTimestamp The ending {@link Timestamp}
* @param {boolean=} useTime If true, use time from the {@link Timestamp}s
* @returns {boolean} True if {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
*/
function isBetweenDates(timestamp, startTimestamp, endTimestamp, useTime = false) {
const cd = getDayIdentifier(timestamp) + (useTime === true ? getTimeIdentifier(timestamp) : 0);
const sd = getDayIdentifier(startTimestamp) + (useTime === true ? getTimeIdentifier(startTimestamp) : 0);
const ed = getDayIdentifier(endTimestamp) + (useTime === true ? getTimeIdentifier(endTimestamp) : 0);
return cd >= sd && cd <= ed;
}
/**
* Determine if two ranges of {@link Timestamp}s overlap each other
* @param {Timestamp} startTimestamp The starting {@link Timestamp} of first range
* @param {Timestamp} endTimestamp The endinging {@link Timestamp} of first range
* @param {Timestamp} firstTimestamp The starting {@link Timestamp} of second range
* @param {Timestamp} lastTimestamp The ending {@link Timestamp} of second range
* @returns {boolean} True if the two ranges overlap each other
*/
function isOverlappingDates(startTimestamp, endTimestamp, firstTimestamp, lastTimestamp) {
const start = getDayIdentifier(startTimestamp);
const end = getDayIdentifier(endTimestamp);
const first = getDayIdentifier(firstTimestamp);
const last = getDayIdentifier(lastTimestamp);
return ((start >= first && start <= last) || // overlap left
(end >= first && end <= last) || // overlap right
(first >= start && end >= last) // surrounding
);
}
/**
* Add or decrements years, months, days, hours or minutes to a timestamp
* @param {Timestamp} timestamp The {@link Timestamp} object
* @param {Object} options configuration data
* @param {number=} options.year If positive, adds years. If negative, removes years.
* @param {number=} options.month If positive, adds months. If negative, removes month.
* @param {number=} options.day If positive, adds days. If negative, removes days.
* @param {number=} options.hour If positive, adds hours. If negative, removes hours.
* @param {number=} options.minute If positive, adds minutes. If negative, removes minutes.
* @returns {Timestamp} A modified copy of the passed in {@link Timestamp}
*/
function addToDate(timestamp, options) {
const ts = copyTimestamp(timestamp);
if (options.year)
ts.year += options.year;
if (options.month)
ts.month += options.month;
if (options.day)
ts.day += options.day;
if (options.hour)
ts.hour += options.hour;
if (options.minute)
ts.minute += options.minute;
return updateFormatted(normalizeTimestamp(ts));
}
/**
* Normalizes a timestamp object by creating a JavaScript Date object and extracting standardized values.
* This function ensures that the timestamp values are consistent and correctly represent a valid date and time.
*
* @param {Object} ts - The timestamp object to normalize.
* @param {number} ts.year - The year of the timestamp.
* @param {number} ts.month - The month of the timestamp (1-12).
* @param {number} ts.day - The day of the month.
* @param {number} ts.hour - The hour of the day (0-23).
* @param {number} ts.minute - The minute of the hour (0-59).
* @returns {Object} A new object with normalized timestamp values.
* The returned object includes all properties from the input object,
* with year, month, day, hour, and minute properties updated to normalized values.
*/
function normalizeTimestamp(ts) {
const date = new Date(ts.year, ts.month - 1, ts.day, ts.hour, ts.minute);
return {
...ts,
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
};
}
/**
* Returns number of days between two {@link Timestamp}s
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
* @returns Number of days
*/
function daysBetween(ts1, ts2) {
const diff = diffTimestamp(ts1, ts2, true);
return Math.floor(diff / TIME_CONSTANTS.MILLISECONDS_IN.DAY);
}
/**
* Returns number of weeks between two {@link Timestamp}s
* @param {Timestamp} ts1 The first {@link Timestamp}
* @param {Timestamp} ts2 The second {@link Timestamp}
*/
function weeksBetween(ts1, ts2) {
let t1 = copyTimestamp(ts1);
let t2 = copyTimestamp(ts2);
t1 = findWeekday(t1, 0);
t2 = findWeekday(t2, 6);
return Math.ceil(daysBetween(t1, t2) / TIME_CONSTANTS.DAYS_IN.WEEK);
}
// Known dates
const weekdayDateMap = {
Sun: new Date('2020-01-05T00:00:00.000Z'),
Mon: new Date('2020-01-06T00:00:00.000Z'),
Tue: new Date('2020-01-07T00:00:00.000Z'),
Wed: new Date('2020-01-08T00:00:00.000Z'),
Thu: new Date('2020-01-09T00:00:00.000Z'),
Fri: new Date('2020-01-10T00:00:00.000Z'),
Sat: new Date('2020-01-11T00:00:00.000Z'),
};
/**
* Returns a function that uses Intl.DateTimeFormat to format weekdays.
*
* @function getWeekdayFormatter
* @returns {function} A function that formats weekdays.
*
* @example
* const formatWeekday = getWeekdayFormatter();
* console.log(formatWeekday('Mon', 'long', 'en-US')); // "Monday"
* console.log(formatWeekday('Mon', 'short', 'fr-FR')); // "lun."
*
* @param {string} weekday - The abbreviation of the weekday (e.g., 'Mon', 'Tue', 'Wed', etc.).
* @param {string} [type='long'] - The type of formatting to use ('narrow', 'short', or 'long').
* @param {string} [locale=''] - The locale to use for formatting.
*
* @returns {string} The formatted weekday.
*/
function getWeekdayFormatter() {
const emptyFormatter = () => '';
const options = {
long: { timeZone: 'UTC', weekday: 'long' },
short: { timeZone: 'UTC', weekday: 'short' },
narrow: { timeZone: 'UTC', weekday: 'narrow' },
};
/* istanbul ignore next */
if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
return emptyFormatter;
}
/**
* Formats a given weekday into a localized string based on the specified type and locale.
*
* @param {number} weekday - The day of the week (0 for Sunday, 1 for Monday, etc.).
* @param {string} type - The format type (e.g., 'narrow', 'short', 'long') to use for formatting.
* @param {string} [locale] - The locale string (e.g., 'en-US') to use for formatting. Defaults to the user's locale if not provided.
* @returns {string} The formatted weekday string.
*/
function weekdayFormatter(weekday, type, locale) {
try {
const intlFormatter = new Intl.DateTimeFormat(locale || undefined,
/// @ts-expect-error ignore for now
options[type] || options['long']);
return intlFormatter.format(weekdayDateMap[weekday]);
}
catch (e) /* istanbul ignore next */ {
if (e instanceof Error) {
console.error(`Intl.DateTimeFormat: ${e.message} -> day of week: ${weekday}`);
}
return '';
}
}
return weekdayFormatter;
}
/**
* Retrieves an array of localized weekday names.
*
* @param {string} type - The format type for the weekday names. Can be 'narrow', 'short', or 'long'.
* @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
* @returns {string[]} An array of localized weekday names in the specified format.
*/
function getWeekdayNames(type, locale) {
const shortWeekdays = Object.keys(weekdayDateMap);
const weekdayFormatter = getWeekdayFormatter();
return shortWeekdays.map((weekday) => String(weekdayFormatter(weekday, type, locale)));
}
/**
* Creates and returns a function for formatting month names based on locale and format type.
*
* @returns {Function} A function that formats month names.
* The returned function accepts the following parameters:
* @param {number} month - The month to format (0-11, where 0 is January).
* @param {string} [type='long'] - The format type: 'narrow', 'short', or 'long'.
* @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
* @returns {string} The formatted month name.
*
* @throws {Error} If Intl or Intl.DateTimeFormat is not supported in the environment.
*/
function getMonthFormatter() {
const emptyFormatter = () => '';
const options = {
long: { timeZone: 'UTC', month: 'long' },
short: { timeZone: 'UTC', month: 'short' },
narrow: { timeZone: 'UTC', month: 'narrow' },
};
/* istanbul ignore next */
if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
return emptyFormatter;
}
/**
* Formats a given month into a string based on the specified type and locale.
*
* @param {number} month - The month to format (0 for January, 11 for December).
* @param {string} type - The format type (e.g., 'narrow', 'long', 'short', etc.).
* @param {string} [locale] - The locale to use for formatting (defaults to the system locale if not provided).
* @returns {string} The formatted month string.
*/
function monthFormatter(month, type, locale) {
try {
const intlFormatter = new Intl.DateTimeFormat(locale || undefined,
/// @ts-expect-error ignore for now
options[type] || options['long']);
const date = new Date();
date.setDate(1);
date.setMonth(month);
return intlFormatter.format(date);
}
catch (e) /* istanbul ignore next */ {
if (e instanceof Error) {
console.error(`Intl.DateTimeFormat: ${e.message} -> month: ${month}`);
}
return '';
}
}
return monthFormatter;
}
/**
* Retrieves an array of localized month names.
*
* @param {string} type - The format type for the month names. Can be 'narrow', 'short', or 'long'.
* @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
* @returns {string[]} An array of localized month names in the specified format.
*/
function getMonthNames(type, locale) {
const monthFormatter = getMonthFormatter();
return [...Array(12).keys()].map((month) => monthFormatter(month, type, locale));
}
function convertToUnit(input, unit = 'px') {
if (!input) {
return undefined;
}
else if (isNaN(input)) {
return String(input);
}
else if (input === 'auto') {
return input;
}
else {
return `${Number(input)}${unit}`;
}
}
function indexOf(array, cb) {
for (let i = 0; i < array.length; i++) {
if (cb(array[i], i) === true) {
return i;
}
}
return -1;
}
function minCharWidth(str, count) {
if (count === 0)
return str;
return str.slice(0, count);
}
/**
* Converts a kebab-case string to camelCase.
* @param {string} str - The kebab-case string.
* @returns {string} - The camelCase formatted string.
*/
const toCamelCase = (str) => str.replace(/(-\w)/g, (m) => m[1].toUpperCase());
/**
* Generates mouse event handlers based on event listeners.
* @param {Function} emit - Vue's emit function.
* @param {Ref<Listeners>} listeners - Vue ref containing event listeners.
* @param {Record<string, MouseEventOptions>} events - Object defining mouse events and their options.
* @param {GetEventFunction} getEvent - Function to transform event objects.
* @returns {Record<string, Function | Function[]>} - The mapped mouse event handlers.
*/
function getMouseEventHandlers(emit, listeners, events, getEvent) {
const on = {};
for (const eventName in events) {
const eventOptions = events[eventName];
const eventKey = toCamelCase('on-' + eventName);
if (!listeners.value) {
console.warn('$listeners has not been set up');
return {};
}
if (listeners.value[eventKey] === undefined)
continue;
const key = 'on' + eventOptions.event.charAt(0).toUpperCase() + eventOptions.event.slice(1);
const handler = (event) => {
if (eventOptions.button === undefined ||
('buttons' in event && event.buttons > 0 && event.button === eventOptions.button)) {
if (eventOptions.prevent) {
event.preventDefault();
}
if (eventOptions.stop) {
event.stopPropagation();
}
emit(eventName, getEvent(event, eventName));
}
return eventOptions.result;
};
if (key in on) {
if (Array.isArray(on[key])) {
on[key].push(handler);
}
else {
on[key] = [on[key], handler];
}
}
else {
on[key] = handler;
}
}
return on;
}
/**
* Returns default mouse event handlers based on a suffix.
* @param {Function} emit - Vue's emit function.
* @param {Ref<Listeners>} listeners - Vue ref containing event listeners.
* @param {string} suffix - Event suffix (e.g., '-day' for 'click-day').
* @param {GetEventFunction} getEvent - Function to transform event objects.
* @returns {Record<string, Function | Function[]>} - The mapped event handlers.
*/
function getDefaultMouseEventHandlers(emit, listeners, suffix, getEvent) {
return getMouseEventHandlers(emit, listeners, getMouseEventName(suffix), getEvent);
}
/**
* Generates event names for different mouse interactions.
* @param {string} suffix - Event suffix (e.g., '-day' for 'click-day').
* @returns {Record<string, MouseEventOptions>} - The event name map.
*/
function getMouseEventName(suffix) {
return {
['click' + suffix]: { event: 'click' },
['contextmenu' + suffix]: { event: 'contextmenu', prevent: true, result: false },
['mousedown' + suffix]: { event: 'mousedown' },
['mousemove' + suffix]: { event: 'mousemove' },
['mouseup' + suffix]: { event: 'mouseup' },
['mouseenter' + suffix]: { event: 'mouseenter' },
['mouseleave' + suffix]: { event: 'mouseleave' },
['touchstart' + suffix]: { event: 'touchstart' },
['touchmove' + suffix]: { event: 'touchmove' },
['touchend' + suffix]: { event: 'touchend' },
};
}
/**
* Returns an array of raw event names based on a suffix.
* @param {string} suffix - Event suffix.
* @returns {string[]} - The raw event names.
*/
function getRawMouseEvents(suffix) {
return Object.keys(getMouseEventName(suffix));
}
/**
* Default function export that provides mouse event handling utilities.
* @param {Function} emit - Vue's emit function.
* @param {Ref<Listeners>} listeners - Vue ref containing event listeners.
* @returns {Object} - Functions for managing mouse event handlers.
*/
function useMouseEvents(