UNPKG

@ionic/core

Version:
1,126 lines (1,119 loc) • 38.7 kB
/*! * (C) Ionic http://ionicframework.com - MIT License */ import { a as printIonWarning } from './index-BpRUsN_W.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))) { 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 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; }; export { getNextWeek as A, parseMinParts as B, parseMaxParts as C, parseDate as D, warnIfValueOutOfBounds as E, parseAmPm as F, clampDate as G, convertToArrayOfNumbers as H, convertDataToISO as I, getClosestValidDate as J, isMonthFirstLocale as K, isLocaleDayPeriodRTL as L, calculateHourFromAMPM as M, getMonthAndYear as N, getLocalizedTime as O, getLocalizedDateTime as P, formatValue as Q, getPreviousMonth as a, getTodayLabel as b, getYear as c, getHourCycle as d, getInternalHourValue as e, getFormattedHour as f, getNextMonth as g, addTimePadding as h, getLocalizedDayPeriod as i, isBefore as j, isSameDay as k, isAfter as l, is24Hour as m, getNumDaysInMonth as n, getDay as o, generateDayAriaLabel as p, getPartsFromCalendarDay as q, removeDateTzOffset as r, getNextYear as s, getPreviousYear as t, getEndOfWeek as u, validateParts as v, getStartOfWeek as w, getPreviousDay as x, getNextDay as y, getPreviousWeek as z };