UNPKG

@wordpress/components

Version:
191 lines (173 loc) 6.45 kB
/** * External dependencies */ import { UTCDateMini } from '@date-fns/utc'; /** * WordPress dependencies */ import { date as formatDate, getDate } from '@wordpress/date'; /** * Internal dependencies */ import type { InputState } from '../input-control/reducer/state'; import type { InputAction } from '../input-control/reducer/actions'; import { COMMIT, PRESS_DOWN, PRESS_UP } from '../input-control/reducer/actions'; /** * Converts a date input to a UTC-normalized date for consistent date * manipulation. Timezoneless strings are interpreted using the timezone * offset from @wordpress/date settings. Date objects and timestamps * represent specific UTC instants. * * @param input Value to turn into a date. */ export function inputToDate( input: Date | string | number ): Date { if ( typeof input === 'string' ) { // Note that JavaScript doesn't fully support ISO-8601 time strings, so // the behavior of passing these through to the Date constructor is // non-deterministic. // // See: https://tc39.es/ecma262/#sec-date-time-string-format const hasTimezone = /Z|[+-]\d{2}(:?\d{2})?$/.test( input ); if ( hasTimezone ) { return new UTCDateMini( new Date( input ) ); } // Strings without timezone indicators are interpreted using configured // timezone offset, then converted to UTC for internal storage. return new UTCDateMini( getDate( input ).getTime() ); } // Date objects and number timestamps represent specific UTC moments. // Convert to milliseconds since epoch for consistent UTC handling. const time = input instanceof Date ? input.getTime() : input; return new UTCDateMini( time ); } /** * Returns the start of day (midnight) as a browser-local Date for the calendar * day in the configured timezone in @wordpress/date settings. This is necessary * because date-fns's startOfDay operates in browser local time, which can cause * off-by-one-day bugs when browser and configured timezones differ. * * For example, if the UTC time is Nov 16, 01:00 UTC and configured timezone * is UTC-5, the date is Nov 15. This function returns a browser-local Date * at Nov 15, 00:00 (browser local midnight) so it matches calendar days. * * @param date A Date object normalized to UTC * @return A browser-local Date at midnight for the configured timezone date */ export function startOfDayInConfiguredTimezone( date: Date ): Date { // Determine the calendar day in the configured WordPress timezone and // return a browser-local Date at midnight for that calendar day. const year = Number( formatDate( 'Y', date ) ); const month = Number( formatDate( 'n', date ) ) - 1; const day = Number( formatDate( 'j', date ) ); return new Date( year, month, day, 0, 0, 0, 0 ); } /** * Converts a 12-hour time to a 24-hour time. * @param hours * @param isPm */ export function from12hTo24h( hours: number, isPm: boolean ) { return isPm ? ( ( hours % 12 ) + 12 ) % 24 : hours % 12; } /** * Converts a 24-hour time to a 12-hour time. * @param hours */ export function from24hTo12h( hours: number ) { return hours % 12 || 12; } /** * Creates an InputControl reducer used to pad an input so that it is always a * given width. For example, the hours and minutes inputs are padded to 2 so * that '4' appears as '04'. * * @param pad How many digits the value should be. */ export function buildPadInputStateReducer( pad: number ) { return ( state: InputState, action: InputAction ) => { const nextState = { ...state }; if ( action.type === COMMIT || action.type === PRESS_UP || action.type === PRESS_DOWN ) { if ( nextState.value !== undefined ) { nextState.value = nextState.value .toString() .padStart( pad, '0' ); } } return nextState; }; } /** * Returns the number of days in a month. * * @param year The year * @param month The month, zero-indexed (0-11) * * @return The number of days in the month */ export const getDaysInMonth = ( year: number, month: number ) => // Take advantage of JavaScript's built-in date wrapping logic, where day 0 // of the next month is interpreted as the last day of the preceding month. new Date( year, month + 1, 0 ).getDate(); /** * Updates specific date fields in the configured timezone and returns a new * UTC date. * * @param date A Date object * @param updates Object with fields to update * @return A Date object normalized to UTC with the updated values */ export function setInConfiguredTimezone( date: Date, updates: Partial< { year: number; month: number; date: number; hours: number; minutes: number; seconds: number; } > ): Date { const values = { year: Number( formatDate( 'Y', date ) ), month: Number( formatDate( 'n', date ) ) - 1, date: Number( formatDate( 'j', date ) ), hours: Number( formatDate( 'H', date ) ), minutes: Number( formatDate( 'i', date ) ), seconds: Number( formatDate( 's', date ) ), ...updates, }; // Clamp the day to the last valid day of the month, to avoid producing // invalid date strings (e.g. "2026-02-31"). const daysInMonth = getDaysInMonth( values.year, values.month ); values.date = Math.min( values.date, daysInMonth ); const year = String( values.year ).padStart( 4, '0' ); const month = String( values.month + 1 ).padStart( 2, '0' ); const day = String( values.date ).padStart( 2, '0' ); const hours = String( values.hours ).padStart( 2, '0' ); const minutes = String( values.minutes ).padStart( 2, '0' ); const seconds = String( values.seconds ).padStart( 2, '0' ); const timezoneless = `${ year }-${ month }-${ day }T${ hours }:${ minutes }:${ seconds }`; // Parse as WordPress-configured timezone time and convert to a UTC instant. return new UTCDateMini( getDate( timezoneless ).getTime() ); } /** * Validates the target of a React event to ensure it is an input element and * that the input is valid. * @param event */ export function validateInputElementTarget( event: React.SyntheticEvent ) { // `instanceof` checks need to get the instance definition from the // corresponding window object — therefore, the following logic makes // the component work correctly even when rendered inside an iframe. const HTMLInputElementInstance = ( event.target as HTMLInputElement )?.ownerDocument.defaultView ?.HTMLInputElement ?? HTMLInputElement; if ( ! ( event.target instanceof HTMLInputElementInstance ) ) { return false; } return event.target.validity.valid; }