UNPKG

@ionic/core

Version:
1,170 lines (1,162 loc) • 39.6 kB
/*! * (C) Ionic http://ionicframework.com - MIT License */ 'use strict'; var index = require('./index-BiQyIvoc.js'); /** * Returns true if the selected day is equal to the reference day */ const isSameDay = (baseParts, compareParts) => { return (baseParts.month === compareParts.month && baseParts.day === compareParts.day && baseParts.year === compareParts.year); }; /** * Returns true is the selected day is before the reference day. */ const isBefore = (baseParts, compareParts) => { return !!(baseParts.year < compareParts.year || (baseParts.year === compareParts.year && baseParts.month < compareParts.month) || (baseParts.year === compareParts.year && baseParts.month === compareParts.month && baseParts.day !== null && baseParts.day < compareParts.day)); }; /** * Returns true is the selected day is after the reference day. */ const isAfter = (baseParts, compareParts) => { return !!(baseParts.year > compareParts.year || (baseParts.year === compareParts.year && baseParts.month > compareParts.month) || (baseParts.year === compareParts.year && baseParts.month === compareParts.month && baseParts.day !== null && baseParts.day > compareParts.day)); }; const warnIfValueOutOfBounds = (value, min, max) => { const valueArray = Array.isArray(value) ? value : [value]; for (const val of valueArray) { if ((min !== undefined && isBefore(val, min)) || (max !== undefined && isAfter(val, max))) { index.printIonWarning('[ion-datetime] - The value provided to ion-datetime is out of bounds.\n\n' + `Min: ${JSON.stringify(min)}\n` + `Max: ${JSON.stringify(max)}\n` + `Value: ${JSON.stringify(value)}`); break; } } }; /** * Determines if given year is a * leap year. Returns `true` if year * is a leap year. Returns `false` * otherwise. */ const isLeapYear = (year) => { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; }; /** * Determines the hour cycle for a user. * If the hour cycle is explicitly defined, just use that. * Otherwise, we try to derive it from either the specified * locale extension tags or from Intl.DateTimeFormat directly. */ const getHourCycle = (locale, hourCycle) => { /** * If developer has explicitly enabled 24-hour time * then return early and do not look at the system default. */ if (hourCycle !== undefined) { return hourCycle; } /** * If hourCycle was not specified, check the locale * that is set on the user's device. We first check the * Intl.DateTimeFormat hourCycle option as developers can encode this * option into the locale string. Example: `en-US-u-hc-h23` */ const formatted = new Intl.DateTimeFormat(locale, { hour: 'numeric' }); const options = formatted.resolvedOptions(); if (options.hourCycle !== undefined) { return options.hourCycle; } /** * If hourCycle is not specified (either through lack * of browser support or locale information) then fall * back to this slower hourCycle check. */ const date = new Date('5/18/2021 00:00'); const parts = formatted.formatToParts(date); const hour = parts.find((p) => p.type === 'hour'); if (!hour) { throw new Error('Hour value not found from DateTimeFormat'); } /** * Midnight for h11 starts at 0:00am * Midnight for h12 starts at 12:00am * Midnight for h23 starts at 00:00 * Midnight for h24 starts at 24:00 */ switch (hour.value) { case '0': return 'h11'; case '12': return 'h12'; case '00': return 'h23'; case '24': return 'h24'; default: throw new Error(`Invalid hour cycle "${hourCycle}"`); } }; /** * Determine if the hour cycle uses a 24-hour format. * Returns true for h23 and h24. Returns false otherwise. * If you don't know the hourCycle, use getHourCycle above * and pass the result into this function. */ const is24Hour = (hourCycle) => { return hourCycle === 'h23' || hourCycle === 'h24'; }; /** * Given a date object, returns the number * of days in that month. * Month value begin at 1, not 0. * i.e. January = month 1. */ const getNumDaysInMonth = (month, year) => { return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : month === 2 ? isLeapYear(year) ? 29 : 28 : 31; }; /** * Certain locales display month then year while * others display year then month. * We can use Intl.DateTimeFormat to determine * the ordering for each locale. * The formatOptions param can be used to customize * which pieces of a date to compare against the month * with. For example, some locales render dd/mm/yyyy * while others render mm/dd/yyyy. This function can be * used for variations of the same "month first" check. */ const isMonthFirstLocale = (locale, formatOptions = { month: 'numeric', year: 'numeric', }) => { /** * By setting month and year we guarantee that only * month, year, and literal (slashes '/', for example) * values are included in the formatToParts results. * * The ordering of the parts will be determined by * the locale. So if the month is the first value, * then we know month should be shown first. If the * year is the first value, then we know year should be shown first. * * This ordering can be controlled by customizing the locale property. */ const parts = new Intl.DateTimeFormat(locale, formatOptions).formatToParts(new Date()); return parts[0].type === 'month'; }; /** * Determines if the given locale formats the day period (am/pm) to the * left or right of the hour. * @param locale The locale to check. * @returns `true` if the locale formats the day period to the left of the hour. */ const isLocaleDayPeriodRTL = (locale) => { const parts = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).formatToParts(new Date()); return parts[0].type === 'dayPeriod'; }; const ISO_8601_REGEXP = // eslint-disable-next-line no-useless-escape /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; // eslint-disable-next-line no-useless-escape const TIME_REGEXP = /^((\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; /** * Use to convert a string of comma separated numbers or * an array of numbers, and clean up any user input */ const convertToArrayOfNumbers = (input) => { if (input === undefined) { return; } let processedInput = input; if (typeof input === 'string') { // convert the string to an array of strings // auto remove any whitespace and [] characters processedInput = input.replace(/\[|\]|\s/g, '').split(','); } let values; if (Array.isArray(processedInput)) { // ensure each value is an actual number in the returned array values = processedInput.map((num) => parseInt(num, 10)).filter(isFinite); } else { values = [processedInput]; } return values; }; /** * Extracts date information * from a .calendar-day element * into DatetimeParts. */ const getPartsFromCalendarDay = (el) => { return { month: parseInt(el.getAttribute('data-month'), 10), day: parseInt(el.getAttribute('data-day'), 10), year: parseInt(el.getAttribute('data-year'), 10), dayOfWeek: parseInt(el.getAttribute('data-day-of-week'), 10), }; }; function parseDate(val) { if (Array.isArray(val)) { const parsedArray = []; for (const valStr of val) { const parsedVal = parseDate(valStr); /** * If any of the values weren't parsed correctly, consider * the entire batch incorrect. This simplifies the type * signatures by having "undefined" be a general error case * instead of returning (Datetime | undefined)[], which is * harder for TS to perform type narrowing on. */ if (!parsedVal) { return undefined; } parsedArray.push(parsedVal); } return parsedArray; } // manually parse IS0 cuz Date.parse cannot be trusted // ISO 8601 format: 1994-12-15T13:47:20Z let parse = null; if (val != null && val !== '') { // try parsing for just time first, HH:MM parse = TIME_REGEXP.exec(val); if (parse) { // adjust the array so it fits nicely with the datetime parse parse.unshift(undefined, undefined); parse[2] = parse[3] = undefined; } else { // try parsing for full ISO datetime parse = ISO_8601_REGEXP.exec(val); } } if (parse === null) { // wasn't able to parse the ISO datetime index.printIonWarning(`[ion-datetime] - Unable to parse date string: ${val}. Please provide a valid ISO 8601 datetime string.`); return undefined; } // ensure all the parse values exist with at least 0 for (let i = 1; i < 8; i++) { parse[i] = parse[i] !== undefined ? parseInt(parse[i], 10) : undefined; } // can also get second and millisecond from parse[6] and parse[7] if needed return { year: parse[1], month: parse[2], day: parse[3], hour: parse[4], minute: parse[5], ampm: parse[4] < 12 ? 'am' : 'pm', }; } const clampDate = (dateParts, minParts, maxParts) => { if (minParts && isBefore(dateParts, minParts)) { return minParts; } else if (maxParts && isAfter(dateParts, maxParts)) { return maxParts; } return dateParts; }; /** * Parses an hour and returns if the value is in the morning (am) or afternoon (pm). * @param hour The hour to format, should be 0-23 * @returns `pm` if the hour is greater than or equal to 12, `am` if less than 12. */ const parseAmPm = (hour) => { return hour >= 12 ? 'pm' : 'am'; }; /** * Takes a max date string and creates a DatetimeParts * object, filling in any missing information. * For example, max="2012" would fill in the missing * month, day, hour, and minute information. */ const parseMaxParts = (max, todayParts) => { const result = parseDate(max); /** * If min was not a valid date then return undefined. */ if (result === undefined) { return; } const { month, day, year, hour, minute } = result; /** * When passing in `max` or `min`, developers * can pass in any ISO-8601 string. This means * that not all of the date/time fields are defined. * For example, passing max="2012" is valid even though * there is no month, day, hour, or minute data. * However, all of this data is required when clamping the date * so that the correct initial value can be selected. As a result, * we need to fill in any omitted data with the min or max values. */ const yearValue = year ?? todayParts.year; const monthValue = month ?? 12; return { month: monthValue, day: day ?? getNumDaysInMonth(monthValue, yearValue), /** * Passing in "HH:mm" is a valid ISO-8601 * string, so we just default to the current year * in this case. */ year: yearValue, hour: hour ?? 23, minute: minute ?? 59, }; }; /** * Takes a min date string and creates a DatetimeParts * object, filling in any missing information. * For example, min="2012" would fill in the missing * month, day, hour, and minute information. */ const parseMinParts = (min, todayParts) => { const result = parseDate(min); /** * If min was not a valid date then return undefined. */ if (result === undefined) { return; } const { month, day, year, hour, minute } = result; /** * When passing in `max` or `min`, developers * can pass in any ISO-8601 string. This means * that not all of the date/time fields are defined. * For example, passing max="2012" is valid even though * there is no month, day, hour, or minute data. * However, all of this data is required when clamping the date * so that the correct initial value can be selected. As a result, * we need to fill in any omitted data with the min or max values. */ return { month: month ?? 1, day: day ?? 1, /** * Passing in "HH:mm" is a valid ISO-8601 * string, so we just default to the current year * in this case. */ year: year ?? todayParts.year, hour: hour ?? 0, minute: minute ?? 0, }; }; const twoDigit = (val) => { return ('0' + (val !== undefined ? Math.abs(val) : '0')).slice(-2); }; const fourDigit = (val) => { return ('000' + (val !== undefined ? Math.abs(val) : '0')).slice(-4); }; function convertDataToISO(data) { if (Array.isArray(data)) { return data.map((parts) => convertDataToISO(parts)); } // https://www.w3.org/TR/NOTE-datetime let rtn = ''; if (data.year !== undefined) { // YYYY rtn = fourDigit(data.year); if (data.month !== undefined) { // YYYY-MM rtn += '-' + twoDigit(data.month); if (data.day !== undefined) { // YYYY-MM-DD rtn += '-' + twoDigit(data.day); if (data.hour !== undefined) { // YYYY-MM-DDTHH:mm:SS rtn += `T${twoDigit(data.hour)}:${twoDigit(data.minute)}:00`; } } } } else if (data.hour !== undefined) { // HH:mm rtn = twoDigit(data.hour) + ':' + twoDigit(data.minute); } return rtn; } /** * Converts an 12 hour value to 24 hours. */ const convert12HourTo24Hour = (hour, ampm) => { if (ampm === undefined) { return hour; } /** * If AM and 12am * then return 00:00. * Otherwise just return * the hour since it is * already in 24 hour format. */ if (ampm === 'am') { if (hour === 12) { return 0; } return hour; } /** * If PM and 12pm * just return 12:00 * since it is already * in 24 hour format. * Otherwise add 12 hours * to the time. */ if (hour === 12) { return 12; } return hour + 12; }; const getStartOfWeek = (refParts) => { const { dayOfWeek } = refParts; if (dayOfWeek === null || dayOfWeek === undefined) { throw new Error('No day of week provided'); } return subtractDays(refParts, dayOfWeek); }; const getEndOfWeek = (refParts) => { const { dayOfWeek } = refParts; if (dayOfWeek === null || dayOfWeek === undefined) { throw new Error('No day of week provided'); } return addDays(refParts, 6 - dayOfWeek); }; const getNextDay = (refParts) => { return addDays(refParts, 1); }; const getPreviousDay = (refParts) => { return subtractDays(refParts, 1); }; const getPreviousWeek = (refParts) => { return subtractDays(refParts, 7); }; const getNextWeek = (refParts) => { return addDays(refParts, 7); }; /** * Given datetime parts, subtract * numDays from the date. * Returns a new DatetimeParts object * Currently can only go backward at most 1 month. */ const subtractDays = (refParts, numDays) => { const { month, day, year } = refParts; if (day === null) { throw new Error('No day provided'); } const workingParts = { month, day, year, }; workingParts.day = day - numDays; /** * If wrapping to previous month * update days and decrement month */ if (workingParts.day < 1) { workingParts.month -= 1; } /** * If moving to previous year, reset * month to December and decrement year */ if (workingParts.month < 1) { workingParts.month = 12; workingParts.year -= 1; } /** * Determine how many days are in the current * month */ if (workingParts.day < 1) { const daysInMonth = getNumDaysInMonth(workingParts.month, workingParts.year); /** * Take num days in month and add the * number of underflow days. This number will * be negative. * Example: 1 week before Jan 2, 2021 is * December 26, 2021 so: * 2 - 7 = -5 * 31 + (-5) = 26 */ workingParts.day = daysInMonth + workingParts.day; } return workingParts; }; /** * Given datetime parts, add * numDays to the date. * Returns a new DatetimeParts object * Currently can only go forward at most 1 month. */ const addDays = (refParts, numDays) => { const { month, day, year } = refParts; if (day === null) { throw new Error('No day provided'); } const workingParts = { month, day, year, }; const daysInMonth = getNumDaysInMonth(month, year); workingParts.day = day + numDays; /** * If wrapping to next month * update days and increment month */ if (workingParts.day > daysInMonth) { workingParts.day -= daysInMonth; workingParts.month += 1; } /** * If moving to next year, reset * month to January and increment year */ if (workingParts.month > 12) { workingParts.month = 1; workingParts.year += 1; } return workingParts; }; /** * Given DatetimeParts, generate the previous month. */ const getPreviousMonth = (refParts) => { /** * If current month is January, wrap backwards * to December of the previous year. */ const month = refParts.month === 1 ? 12 : refParts.month - 1; const year = refParts.month === 1 ? refParts.year - 1 : refParts.year; const numDaysInMonth = getNumDaysInMonth(month, year); const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day; return { month, year, day }; }; /** * Given DatetimeParts, generate the next month. */ const getNextMonth = (refParts) => { /** * If current month is December, wrap forwards * to January of the next year. */ const month = refParts.month === 12 ? 1 : refParts.month + 1; const year = refParts.month === 12 ? refParts.year + 1 : refParts.year; const numDaysInMonth = getNumDaysInMonth(month, year); const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day; return { month, year, day }; }; const changeYear = (refParts, yearDelta) => { const month = refParts.month; const year = refParts.year + yearDelta; const numDaysInMonth = getNumDaysInMonth(month, year); const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day; return { month, year, day }; }; /** * Given DatetimeParts, generate the previous year. */ const getPreviousYear = (refParts) => { return changeYear(refParts, -1); }; /** * Given DatetimeParts, generate the next year. */ const getNextYear = (refParts) => { return changeYear(refParts, 1); }; /** * If PM, then internal value should * be converted to 24-hr time. * Does not apply when public * values are already 24-hr time. */ const getInternalHourValue = (hour, use24Hour, ampm) => { if (use24Hour) { return hour; } return convert12HourTo24Hour(hour, ampm); }; /** * Unless otherwise stated, all month values are * 1 indexed instead of the typical 0 index in JS Date. * Example: * January = Month 0 when using JS Date * January = Month 1 when using this datetime util */ /** * Given the current datetime parts and a new AM/PM value * calculate what the hour should be in 24-hour time format. * Used when toggling the AM/PM segment since we store our hours * in 24-hour time format internally. */ const calculateHourFromAMPM = (currentParts, newAMPM) => { const { ampm: currentAMPM, hour } = currentParts; let newHour = hour; /** * If going from AM --> PM, need to update the * */ if (currentAMPM === 'am' && newAMPM === 'pm') { newHour = convert12HourTo24Hour(newHour, 'pm'); /** * If going from PM --> AM */ } else if (currentAMPM === 'pm' && newAMPM === 'am') { newHour = Math.abs(newHour - 12); } return newHour; }; /** * Updates parts to ensure that month and day * values are valid. For days that do not exist, * or are outside the min/max bounds, the closest * valid day is used. */ const validateParts = (parts, minParts, maxParts) => { const { month, day, year } = parts; const partsCopy = clampDate({ ...parts }, minParts, maxParts); const numDays = getNumDaysInMonth(month, year); /** * If the max number of days * is greater than the day we want * to set, update the DatetimeParts * day field to be the max days. */ if (day !== null && numDays < day) { partsCopy.day = numDays; } /** * If value is same day as min day, * make sure the time value is in bounds. */ if (minParts !== undefined && isSameDay(partsCopy, minParts)) { /** * If the hour is out of bounds, * update both the hour and minute. * This is done so that the new time * is closest to what the user selected. */ if (partsCopy.hour !== undefined && minParts.hour !== undefined) { if (partsCopy.hour < minParts.hour) { partsCopy.hour = minParts.hour; partsCopy.minute = minParts.minute; /** * If only the minute is out of bounds, * set it to the min minute. */ } else if (partsCopy.hour === minParts.hour && partsCopy.minute !== undefined && minParts.minute !== undefined && partsCopy.minute < minParts.minute) { partsCopy.minute = minParts.minute; } } } /** * If value is same day as max day, * make sure the time value is in bounds. */ if (maxParts !== undefined && isSameDay(parts, maxParts)) { /** * If the hour is out of bounds, * update both the hour and minute. * This is done so that the new time * is closest to what the user selected. */ if (partsCopy.hour !== undefined && maxParts.hour !== undefined) { if (partsCopy.hour > maxParts.hour) { partsCopy.hour = maxParts.hour; partsCopy.minute = maxParts.minute; /** * If only the minute is out of bounds, * set it to the max minute. */ } else if (partsCopy.hour === maxParts.hour && partsCopy.minute !== undefined && maxParts.minute !== undefined && partsCopy.minute > maxParts.minute) { partsCopy.minute = maxParts.minute; } } } return partsCopy; }; /** * Returns the closest date to refParts * that also meets the constraints of * the *Values params. */ const getClosestValidDate = ({ refParts, monthValues, dayValues, yearValues, hourValues, minuteValues, minParts, maxParts, }) => { const { hour, minute, day, month, year } = refParts; const copyParts = { ...refParts, dayOfWeek: undefined }; if (yearValues !== undefined) { // Filters out years that are out of the min/max bounds const filteredYears = yearValues.filter((year) => { if (minParts !== undefined && year < minParts.year) { return false; } if (maxParts !== undefined && year > maxParts.year) { return false; } return true; }); copyParts.year = findClosestValue(year, filteredYears); } if (monthValues !== undefined) { // Filters out months that are out of the min/max bounds const filteredMonths = monthValues.filter((month) => { if (minParts !== undefined && copyParts.year === minParts.year && month < minParts.month) { return false; } if (maxParts !== undefined && copyParts.year === maxParts.year && month > maxParts.month) { return false; } return true; }); copyParts.month = findClosestValue(month, filteredMonths); } // Day is nullable but cannot be undefined if (day !== null && dayValues !== undefined) { // Filters out days that are out of the min/max bounds const filteredDays = dayValues.filter((day) => { if (minParts !== undefined && isBefore({ ...copyParts, day }, minParts)) { return false; } if (maxParts !== undefined && isAfter({ ...copyParts, day }, maxParts)) { return false; } return true; }); copyParts.day = findClosestValue(day, filteredDays); } if (hour !== undefined && hourValues !== undefined) { // Filters out hours that are out of the min/max bounds const filteredHours = hourValues.filter((hour) => { if (minParts?.hour !== undefined && isSameDay(copyParts, minParts) && hour < minParts.hour) { return false; } if (maxParts?.hour !== undefined && isSameDay(copyParts, maxParts) && hour > maxParts.hour) { return false; } return true; }); copyParts.hour = findClosestValue(hour, filteredHours); copyParts.ampm = parseAmPm(copyParts.hour); } if (minute !== undefined && minuteValues !== undefined) { // Filters out minutes that are out of the min/max bounds const filteredMinutes = minuteValues.filter((minute) => { if (minParts?.minute !== undefined && isSameDay(copyParts, minParts) && copyParts.hour === minParts.hour && minute < minParts.minute) { return false; } if (maxParts?.minute !== undefined && isSameDay(copyParts, maxParts) && copyParts.hour === maxParts.hour && minute > maxParts.minute) { return false; } return true; }); copyParts.minute = findClosestValue(minute, filteredMinutes); } return copyParts; }; /** * Finds the value in "values" that is * numerically closest to "reference". * This function assumes that "values" is * already sorted in ascending order. * @param reference The reference number to use * when finding the closest value * @param values The allowed values that will be * searched to find the closest value to "reference" */ const findClosestValue = (reference, values) => { let closestValue = values[0]; let rank = Math.abs(closestValue - reference); for (let i = 1; i < values.length; i++) { const value = values[i]; /** * This code prioritizes the first * closest result. Given two values * with the same distance from reference, * this code will prioritize the smaller of * the two values. */ const valueRank = Math.abs(value - reference); if (valueRank < rank) { closestValue = value; rank = valueRank; } } return closestValue; }; const getFormattedDayPeriod = (dayPeriod) => { if (dayPeriod === undefined) { return ''; } return dayPeriod.toUpperCase(); }; /** * Including time zone options may lead to the rendered text showing a * different time from what was selected in the Datetime, which could cause * confusion. */ const stripTimeZone = (formatOptions) => { return { ...formatOptions, /** * Setting the time zone to UTC ensures that the value shown is always the * same as what was selected and safeguards against older Safari bugs with * Intl.DateTimeFormat. */ timeZone: 'UTC', /** * We do not want to display the time zone name */ timeZoneName: undefined, }; }; const getLocalizedTime = (locale, refParts, hourCycle, formatOptions = { hour: 'numeric', minute: 'numeric' }) => { const timeParts = { hour: refParts.hour, minute: refParts.minute, }; if (timeParts.hour === undefined || timeParts.minute === undefined) { return 'Invalid Time'; } return new Intl.DateTimeFormat(locale, { ...stripTimeZone(formatOptions), /** * We use hourCycle here instead of hour12 due to: * https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2 */ hourCycle, /** * Setting Z at the end indicates that this * date string is in the UTC time zone. This * prevents new Date from adding the time zone * offset when getting the ISO string. */ }).format(new Date(convertDataToISO({ /** * JS uses a simplified ISO 8601 format which allows for * date-only formats and date-time formats, but not * time-only formats: https://tc39.es/ecma262/#sec-date-time-string-format * As a result, developers who only pass a time will get * an "Invalid Date" error. To account for this, we make sure that * year/day/month values are set when passing to new Date(). * The Intl.DateTimeFormat call above only uses the hour/minute * values, so passing these date values should have no impact * on the time output. */ year: 2023, day: 1, month: 1, ...timeParts, }) + 'Z')); }; /** * Adds padding to a time value so * that it is always 2 digits. */ const addTimePadding = (value) => { const valueToString = value.toString(); if (valueToString.length > 1) { return valueToString; } return `0${valueToString}`; }; /** * Formats 24 hour times so that * it always has 2 digits. For * 12 hour times it ensures that * hour 0 is formatted as '12'. */ const getFormattedHour = (hour, hourCycle) => { /** * Midnight for h11 starts at 0:00am * Midnight for h12 starts at 12:00am * Midnight for h23 starts at 00:00 * Midnight for h24 starts at 24:00 */ if (hour === 0) { switch (hourCycle) { case 'h11': return '0'; case 'h12': return '12'; case 'h23': return '00'; case 'h24': return '24'; default: throw new Error(`Invalid hour cycle "${hourCycle}"`); } } const use24Hour = is24Hour(hourCycle); /** * h23 and h24 use 24 hour times. */ if (use24Hour) { return addTimePadding(hour); } return hour.toString(); }; /** * Generates an aria-label to be read by screen readers * given a local, a date, and whether or not that date is * today's date. */ const generateDayAriaLabel = (locale, today, refParts) => { if (refParts.day === null) { return null; } /** * MM/DD/YYYY will return midnight in the user's timezone. */ const date = getNormalizedDate(refParts); const labelString = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', timeZone: 'UTC', }).format(date); /** * If date is today, prepend "Today" so screen readers indicate * that the date is today. */ return today ? `Today, ${labelString}` : labelString; }; /** * Given a locale and a date object, * return a formatted string that includes * the month name and full year. * Example: May 2021 */ const getMonthAndYear = (locale, refParts) => { const date = getNormalizedDate(refParts); return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date); }; /** * Given a locale and a date object, * return a formatted string that includes * the numeric day. * Note: Some languages will add literal characters * to the end. This function removes those literals. * Example: 29 */ const getDay = (locale, refParts) => { return getLocalizedDateTimeParts(locale, refParts, { day: 'numeric' }).find((obj) => obj.type === 'day').value; }; /** * Given a locale and a date object, * return a formatted string that includes * the numeric year. * Example: 2022 */ const getYear = (locale, refParts) => { return getLocalizedDateTime(locale, refParts, { year: 'numeric' }); }; /** * Given reference parts, return a JS Date object * with a normalized time. */ const getNormalizedDate = (refParts) => { const timeString = refParts.hour !== undefined && refParts.minute !== undefined ? ` ${refParts.hour}:${refParts.minute}` : ''; /** * We use / notation here for the date * so we do not need to do extra work and pad values with zeroes. * Values such as YYYY-MM are still valid, so * we add fallback values so we still get * a valid date otherwise we will pass in a string * like "//2023". Some browsers, such as Chrome, will * account for this and still return a valid date. However, * this is not a consistent behavior across all browsers. */ return new Date(`${refParts.month ?? 1}/${refParts.day ?? 1}/${refParts.year ?? 2023}${timeString} GMT+0000`); }; /** * Given a locale, DatetimeParts, and options * format the DatetimeParts according to the options * and locale combination. This returns a string. If * you want an array of the individual pieces * that make up the localized date string, use * getLocalizedDateTimeParts. */ const getLocalizedDateTime = (locale, refParts, options) => { const date = getNormalizedDate(refParts); return getDateTimeFormat(locale, stripTimeZone(options)).format(date); }; /** * Given a locale, DatetimeParts, and options * format the DatetimeParts according to the options * and locale combination. This returns an array of * each piece of the date. */ const getLocalizedDateTimeParts = (locale, refParts, options) => { const date = getNormalizedDate(refParts); return getDateTimeFormat(locale, options).formatToParts(date); }; /** * Wrapper function for Intl.DateTimeFormat. * Allows developers to apply an allowed format to DatetimeParts. * This function also has built in safeguards for older browser bugs * with Intl.DateTimeFormat. */ const getDateTimeFormat = (locale, options) => { return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }); }; /** * Gets a localized version of "Today" * Falls back to "Today" in English for * browsers that do not support RelativeTimeFormat. */ const getTodayLabel = (locale) => { if ('RelativeTimeFormat' in Intl) { const label = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(0, 'day'); return label.charAt(0).toUpperCase() + label.slice(1); } else { return 'Today'; } }; /** * When calling toISOString(), the browser * will convert the date to UTC time by either adding * or subtracting the time zone offset. * To work around this, we need to either add * or subtract the time zone offset to the Date * object prior to calling toISOString(). * This allows us to get an ISO string * that is in the user's time zone. * * Example: * Time zone offset is 240 * Meaning: The browser needs to add 240 minutes * to the Date object to get UTC time. * What Ionic does: We subtract 240 minutes * from the Date object. The browser then adds * 240 minutes in toISOString(). The result * is a time that is in the user's time zone * and not UTC. * * Note: Some timezones include minute adjustments * such as 30 or 45 minutes. This is why we use setMinutes * instead of setHours. * Example: India Standard Time * Timezone offset: -330 = -5.5 hours. * * List of timezones with 30 and 45 minute timezones: * https://www.timeanddate.com/time/time-zones-interesting.html */ const removeDateTzOffset = (date) => { const tzOffset = date.getTimezoneOffset(); date.setMinutes(date.getMinutes() - tzOffset); return date; }; const DATE_AM = removeDateTzOffset(new Date('2022T01:00')); const DATE_PM = removeDateTzOffset(new Date('2022T13:00')); /** * Formats the locale's string representation of the day period (am/pm) for a given * ref parts day period. * * @param locale The locale to format the day period in. * @param value The date string, in ISO format. * @returns The localized day period (am/pm) representation of the given value. */ const getLocalizedDayPeriod = (locale, dayPeriod) => { const date = dayPeriod === 'am' ? DATE_AM : DATE_PM; const localizedDayPeriod = new Intl.DateTimeFormat(locale, { hour: 'numeric', timeZone: 'UTC', }) .formatToParts(date) .find((part) => part.type === 'dayPeriod'); if (localizedDayPeriod) { return localizedDayPeriod.value; } return getFormattedDayPeriod(dayPeriod); }; /** * Formats the datetime's value to a string, for use in the native input. * * @param value The value to format, either an ISO string or an array thereof. */ const formatValue = (value) => { return Array.isArray(value) ? value.join(',') : value; }; exports.addTimePadding = addTimePadding; exports.calculateHourFromAMPM = calculateHourFromAMPM; exports.clampDate = clampDate; exports.convertDataToISO = convertDataToISO; exports.convertToArrayOfNumbers = convertToArrayOfNumbers; exports.formatValue = formatValue; exports.generateDayAriaLabel = generateDayAriaLabel; exports.getClosestValidDate = getClosestValidDate; exports.getDay = getDay; exports.getEndOfWeek = getEndOfWeek; exports.getFormattedHour = getFormattedHour; exports.getHourCycle = getHourCycle; exports.getInternalHourValue = getInternalHourValue; exports.getLocalizedDateTime = getLocalizedDateTime; exports.getLocalizedDayPeriod = getLocalizedDayPeriod; exports.getLocalizedTime = getLocalizedTime; exports.getMonthAndYear = getMonthAndYear; exports.getNextDay = getNextDay; exports.getNextMonth = getNextMonth; exports.getNextWeek = getNextWeek; exports.getNextYear = getNextYear; exports.getNumDaysInMonth = getNumDaysInMonth; exports.getPartsFromCalendarDay = getPartsFromCalendarDay; exports.getPreviousDay = getPreviousDay; exports.getPreviousMonth = getPreviousMonth; exports.getPreviousWeek = getPreviousWeek; exports.getPreviousYear = getPreviousYear; exports.getStartOfWeek = getStartOfWeek; exports.getTodayLabel = getTodayLabel; exports.getYear = getYear; exports.is24Hour = is24Hour; exports.isAfter = isAfter; exports.isBefore = isBefore; exports.isLocaleDayPeriodRTL = isLocaleDayPeriodRTL; exports.isMonthFirstLocale = isMonthFirstLocale; exports.isSameDay = isSameDay; exports.parseAmPm = parseAmPm; exports.parseDate = parseDate; exports.parseMaxParts = parseMaxParts; exports.parseMinParts = parseMinParts; exports.removeDateTzOffset = removeDateTzOffset; exports.validateParts = validateParts; exports.warnIfValueOutOfBounds = warnIfValueOutOfBounds;