From d3525f7c6cd42c2641423472a651a34f9fd75b90 Mon Sep 17 00:00:00 2001 From: Vicente Canales <1157901+vcanales@users.noreply.github.com> Date: Mon, 7 Dec 2020 11:45:31 -0300 Subject: [PATCH 1/3] Revert date changes from branch 'replace-moment' (#27550) * Revert "Fix c and r formats, add tests." This reverts commit 1f737025dd65449707f3e8cde789ac0b9740ff7e. * Revert date changes from branch 'replace-moment' * fix merge error --- package-lock.json | 32 +- packages/date/README.md | 16 +- packages/date/package.json | 2 - packages/date/src/index.js | 508 +++++++----------- packages/date/src/test/index.js | 326 ----------- .../src/components/post-schedule/index.js | 1 - .../src/components/post-schedule/label.js | 18 +- 7 files changed, 215 insertions(+), 688 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5417bc68aa9e2..86d373db12677 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17565,22 +17565,8 @@ "version": "file:packages/date", "requires": { "@babel/runtime": "^7.11.2", - "date-fns": "^2.16.1", - "date-fns-tz": "^1.0.12", "moment": "^2.22.1", "moment-timezone": "^0.5.31" - }, - "dependencies": { - "date-fns": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", - "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==" - }, - "date-fns-tz": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.0.12.tgz", - "integrity": "sha512-Ca+9pjGkU90XDHnclfSjz9o7g/ZqyYyYI0aCYmbf65P75oy8gktuaRslO3UPXl3ADgAnF9/KCykQkpU3/xvtWQ==" - } } }, "@wordpress/dependency-extraction-webpack-plugin": { @@ -30479,12 +30465,6 @@ "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", "dev": true }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "has-ansi": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", @@ -31912,6 +31892,12 @@ "whatwg-url": "^7.0.0" } }, + "date-fns": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", + "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", + "dev": true + }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -45399,12 +45385,6 @@ "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } - }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true } } }, diff --git a/packages/date/README.md b/packages/date/README.md index 4708927a01033..954ea388ece67 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -28,7 +28,7 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `(Date|string|null)`: Date object or ISO string, parsable by moment.js. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. - _timezone_ `(string|number|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site. _Returns_ @@ -50,7 +50,7 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `(Date|string|null)`: Date object or string, parsable by moment.js. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. - _timezone_ `(string|number|boolean|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. _Returns_ @@ -64,7 +64,7 @@ Formats a date. Does not alter the date's timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `(Date|string|null)`: Date object or ISO string. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. _Returns_ @@ -89,7 +89,7 @@ Formats a date (like `date()` in PHP), in the UTC timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `(Date|string|null)`: Date object or ISO string. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. _Returns_ @@ -103,7 +103,7 @@ and using the UTC timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `(Date|string|null)`: Date object or ISO string. +- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js. _Returns_ @@ -115,7 +115,7 @@ Check whether a date is considered in the future according to the WordPress sett _Parameters_ -- _dateValue_ `(string|Date)`: Date String or Date object in the Defined WP Timezone. +- _dateValue_ `string`: Date String or Date object in the Defined WP Timezone. _Returns_ @@ -129,10 +129,6 @@ _Parameters_ - _dateSettings_ `Object`: Settings, including locale data. -# **zonedTimeToUtc** - -Undocumented declaration. - diff --git a/packages/date/package.json b/packages/date/package.json index 30cd0f0481140..5f64325f5fde9 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -23,8 +23,6 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.11.2", - "date-fns": "^2.16.1", - "date-fns-tz": "^1.0.12", "moment": "^2.22.1", "moment-timezone": "^0.5.31" }, diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 510ae811d0a3a..4145e3fcd5223 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -1,23 +1,13 @@ /** * External dependencies */ -import { - addHours, - format as dateFnsFormat, - getDaysInMonth, - isFuture, - isLeapYear, - parseISO, -} from 'date-fns'; -import { - format as formatTZ, - utcToZonedTime, - zonedTimeToUtc, - toDate, -} from 'date-fns-tz'; -import originalLocale from 'date-fns/locale/en-US/index'; -import buildLocalizeFn from 'date-fns/locale/_lib/buildLocalizeFn'; -import buildFormatLongFn from 'date-fns/locale/_lib/buildFormatLongFn'; +import momentLib from 'moment'; +import 'moment-timezone/moment-timezone'; +import 'moment-timezone/moment-timezone-utils'; + +/** @typedef {import('moment').Moment} Moment */ + +const WP_ZONE = 'WP'; // This regular expression tests positive for UTC offsets as described in ISO 8601. // See: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC @@ -85,13 +75,14 @@ let settings = { }, }, formats: { - time: 'g:i a', + time: 'g: i a', date: 'F j, Y', datetime: 'F j, Y g: i a', datetimeAbbreviated: 'M j, Y g: i a', }, timezone: { offset: '0', string: '', abbr: '' }, }; + /** * Adds a locale to moment, using the format supplied by `wp_localize_script()`. * @@ -99,6 +90,41 @@ let settings = { */ export function setSettings( dateSettings ) { settings = dateSettings; + + // Backup and restore current locale. + const currentLocale = momentLib.locale(); + momentLib.updateLocale( dateSettings.l10n.locale, { + // Inherit anything missing from the default locale. + parentLocale: currentLocale, + months: dateSettings.l10n.months, + monthsShort: dateSettings.l10n.monthsShort, + weekdays: dateSettings.l10n.weekdays, + weekdaysShort: dateSettings.l10n.weekdaysShort, + meridiem( hour, minute, isLowercase ) { + if ( hour < 12 ) { + return isLowercase + ? dateSettings.l10n.meridiem.am + : dateSettings.l10n.meridiem.AM; + } + return isLowercase + ? dateSettings.l10n.meridiem.pm + : dateSettings.l10n.meridiem.PM; + }, + longDateFormat: { + LT: dateSettings.formats.time, + LTS: null, + L: null, + LL: dateSettings.formats.date, + LLL: dateSettings.formats.datetime, + LLLL: null, + }, + // From human_time_diff? + // Set to `(number, withoutSuffix, key, isFuture) => {}` instead. + relativeTime: dateSettings.l10n.relative, + } ); + momentLib.locale( currentLocale ); + + setupWPTimezone(); } /** @@ -110,6 +136,18 @@ export function __experimentalGetSettings() { return settings; } +function setupWPTimezone() { + // Create WP timezone based off dateSettings. + momentLib.tz.add( + momentLib.tz.pack( { + name: WP_ZONE, + abbrs: [ WP_ZONE ], + untils: [ null ], + offsets: [ -settings.timezone.offset * 60 || 0 ], + } ) + ); +} + // Date constants. /** * Number of seconds in one minute. @@ -144,46 +182,41 @@ const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS; */ const formatMap = { // Day - d: 'dd', - D: 'EEE', - j: 'd', - l: 'EEEE', - N: 'i', + d: 'DD', + D: 'ddd', + j: 'D', + l: 'dddd', + N: 'E', /** * Gets the ordinal suffix. * - * @param {Date} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - S( dateValue ) { - const num = dateFnsFormat( dateValue, 'd' ); - const withOrdinal = dateFnsFormat( dateValue, 'do' ); + S( momentDate ) { + // Do - D + const num = momentDate.format( 'D' ); + const withOrdinal = momentDate.format( 'Do' ); return withOrdinal.replace( num, '' ); }, - /** - * Returns the day of the week (zero-indexed). - * - * @param {string} dateValue - */ - w( dateValue ) { - return `${ parseInt( dateFnsFormat( dateValue, 'i' ), 10 ) - 1 }`; - }, + + w: 'd', /** * Gets the day of the year (zero-indexed). * - * @param {Date} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - z( dateValue ) { + z( momentDate ) { // DDD - 1 - return `${ parseInt( dateFnsFormat( dateValue, 'DDD' ), 10 ) - 1 }`; + return '' + parseInt( momentDate.format( 'DDD' ), 10 ) - 1; }, // Week - W: 'II', + W: 'W', // Month F: 'MMMM', @@ -193,58 +226,50 @@ const formatMap = { /** * Gets the days in the month. * - * @param {Date} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - t( dateValue ) { - return getDaysInMonth( dateValue ); + t( momentDate ) { + return momentDate.daysInMonth(); }, // Year /** * Gets whether the current year is a leap year. * - * @param {Date} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - L( dateValue ) { - return isLeapYear( dateValue ) ? '1' : '0'; + L( momentDate ) { + return momentDate.isLeapYear() ? '1' : '0'; }, - o: 'R', - Y: 'yyyy', - y: 'yy', + o: 'GGGG', + Y: 'YYYY', + y: 'YY', // Time - a( dateValue ) { - return formatTZ( dateValue, 'aa', { - timeZone: getActualTimezone(), - } ).toLowerCase(); - }, - A: 'bb', + a: 'a', + A: 'A', /** - * Gets the given time in Swatch Internet Time (.beats). + * Gets the current time in Swatch Internet Time (.beats). * - * @param {Date} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - B( dateValue ) { - const parsedDate = addHours( zonedTimeToUtc( dateValue ), 1 ); - const seconds = parseInt( dateFnsFormat( parsedDate, 's' ), 10 ), - minutes = parseInt( dateFnsFormat( parsedDate, 'm' ), 10 ), - hours = parseInt( dateFnsFormat( parsedDate, 'H' ), 10 ); - - /* - * Rounding up to match results on the same timestamp using - * PHP's date_format. - */ - return Math.ceil( + B( momentDate ) { + const timezoned = momentLib( momentDate ).utcOffset( 60 ); + const seconds = parseInt( timezoned.format( 's' ), 10 ), + minutes = parseInt( timezoned.format( 'm' ), 10 ), + hours = parseInt( timezoned.format( 'H' ), 10 ); + return parseInt( ( seconds + minutes * MINUTE_IN_SECONDS + hours * HOUR_IN_SECONDS ) / - 86.4 + 86.4, + 10 ); }, g: 'h', @@ -256,49 +281,32 @@ const formatMap = { u: 'SSSSSS', v: 'SSS', // Timezone - /** - * Return the timezone identifier for the given date. - * - * @param {Date} dateValue Date ISO string or object. - * - * @return {string} Formatted date. - */ - e( dateValue ) { - return formatTZ( dateValue, 'zzzz', { timeZone: getActualTimezone() } ); - }, + e: 'zz', /** * Gets whether the timezone is in DST currently. * + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - I() { - return ''; // @todo - }, - O( dateValue ) { - return formatTZ( dateValue, 'xx', { timeZone: getActualTimezone() } ); - }, - P( dateValue ) { - return formatTZ( dateValue, 'xxx', { timeZone: getActualTimezone() } ); - }, - T( dateValue ) { - return formatTZ( dateValue, 'z', { timeZone: getActualTimezone() } ); + I( momentDate ) { + return momentDate.isDST() ? '1' : '0'; }, + O: 'ZZ', + P: 'Z', + T: 'z', /** * Gets the timezone offset in seconds. * - * @param {Date|string} dateValue Date ISO string or object. + * @param {Moment} momentDate Moment instance. * * @return {string} Formatted date. */ - Z( dateValue ) { + Z( momentDate ) { // Timezone offset in seconds. - const offset = dateFnsFormat( - utcToZonedTime( dateValue, 'UTC' ), - 'XXX' - ); + const offset = momentDate.format( 'Z' ); const sign = offset[ 0 ] === '-' ? -1 : 1; - const parts = offset.substring( 1 ).split( ':' ).map( Number ); + const parts = offset.substring( 1 ).split( ':' ); return ( sign * ( parts[ 0 ] * HOUR_IN_MINUTES + parts[ 1 ] ) * @@ -306,203 +314,50 @@ const formatMap = { ); }, // Full date/time - c( dateValue ) { - return formatTZ( - utcToZonedTime( - zonedTimeToUtc( dateValue, getActualTimezone() ), - 'UTC' - ), // Offsets the time to the correct timezone - "yyyy-MM-dd'T'HH:mm:ssXXX", - { - timeZone: getActualTimezone(), // Adds the timezone offset to the Date object that will be formatted. - } - ); - }, // .toISOString - r( dateValue ) { - return formatTZ( - utcToZonedTime( - zonedTimeToUtc( dateValue, getActualTimezone() ), - 'UTC' - ), // Offsets the time to the correct timezone - 'iii, d MMM yyyy HH:mm:ss XX', - { - timeZone: getActualTimezone(), // Adds the timezone offset to the Date object that will be formatted. - } - ); - }, - U( dateValue ) { - return formatTZ( - zonedTimeToUtc( dateValue, getActualTimezone() ), - 't' - ); - }, + c: 'YYYY-MM-DDTHH:mm:ssZ', // .toISOString + r: 'ddd, D MMM YYYY HH:mm:ss ZZ', + U: 'X', }; /** - * Applies map of PHP formatting tokens into date-fns formatting tokens to the given format and date. + * Formats a date. Does not alter the date's timezone. + * + * @param {string} dateFormat PHP-style formatting string. + * See php.net/date. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. * - * @param {string} formatString - * @param {Date|string} dateValue + * @return {string} Formatted date. */ -function translateFormat( formatString, dateValue ) { +export function format( dateFormat, dateValue = new Date() ) { let i, char; let newFormat = []; - - const parsedDate = - typeof dateValue === 'string' ? parseISO( dateValue ) : dateValue; - - for ( i = 0; i < formatString.length; i++ ) { - char = formatString[ i ]; + const momentDate = momentLib( dateValue ); + for ( i = 0; i < dateFormat.length; i++ ) { + char = dateFormat[ i ]; // Is this an escape? if ( '\\' === char ) { // Add next character, then move on. i++; - newFormat.push( "'" + formatString[ i ] + "'" ); + newFormat.push( '[' + dateFormat[ i ] + ']' ); continue; } if ( char in formatMap ) { if ( typeof formatMap[ char ] !== 'string' ) { // If the format is a function, call it. - newFormat.push( "'" + formatMap[ char ]( parsedDate ) + "'" ); + newFormat.push( '[' + formatMap[ char ]( momentDate ) + ']' ); } else { // Otherwise, add as a formatting string. newFormat.push( formatMap[ char ] ); } } else { - newFormat.push( char ); + newFormat.push( '[' + char + ']' ); } } // Join with [] between to separate characters, and replace // unneeded separators with static text. - - newFormat = newFormat.join( '' ); - - return newFormat; -} - -/** - * Build date-fns locale settings from WordPress localization settings. - */ -function getLocalizationSettings() { - const monthValues = { - abbreviated: settings.l10n.monthsShort, - wide: settings.l10n.months, - }; - - const dayValues = { - abbreviated: settings.l10n.weekdaysShort, - wide: settings.l10n.weekdays, - }; - - return { - ...originalLocale.localize, - month: buildLocalizeFn( { - values: monthValues, - defaultWidth: 'wide', - } ), - day: buildLocalizeFn( { - values: dayValues, - defaultWidth: 'wide', - } ), - formatLong: { - date: buildFormatLongFn( { - formats: { - full: settings.formats.date, - defaultWidth: 'full', - }, - } ), - time: buildFormatLongFn( { - formats: { - full: settings.formats.time, - defaultWidth: 'full', - }, - } ), - dateTime: buildFormatLongFn( { - formats: { - full: settings.formats.datetime, - short: settings.formats.datetimeAbbreviated, - defaultWidth: 'full', - }, - } ), - }, - }; -} - -/** - * Returns whether a certain UTC offset is valid or not. - * - * @param {number|string} offset a UTC offset. - * - * @return {boolean} whether a certain UTC offset is valid or not. - */ -function isValidUTCOffset( offset ) { - return VALID_UTC_OFFSET.test( offset ); -} - -/** - * Transform the given integer into a valid UTC Offset in hours. - * - * @param {number} offset A UTC offset as an integer - */ -function integerToUTCOffset( offset ) { - const offsetInHours = offset > 23 ? offset / 60 : offset; - const sign = offset < 0 ? '-' : '+'; - const absoluteOffset = - offsetInHours < 0 ? offsetInHours * -1 : offsetInHours; - - return offsetInHours < 10 - ? `${ sign }0${ absoluteOffset }` - : `${ sign }${ absoluteOffset }`; -} - -/** - * Determines whether or not the given value can be parsed as a UTC offset, - * by checking if it is parseable as an integer and if it isn't a - * valid UTC offset already. - * - * @param {string} offset An offset as an integer or a string. - */ -function shouldParseAsUTCOffset( offset ) { - const isNumber = ! Number.isNaN( Number.parseInt( offset, 10 ) ); - return isNumber && ! isValidUTCOffset( offset ); -} - -/** - * Get a properly formatted timezone from a timezone string or offset. - * Return system timezone or offset if no timezone was given. - * - * @param {string} timezone - */ -function getActualTimezone( timezone = '' ) { - if ( ! timezone ) { - const { string, offset } = settings.timezone; - - if ( string ) { - return string; - } - - if ( shouldParseAsUTCOffset( offset ) ) { - return integerToUTCOffset( offset ); - } - } - - return shouldParseAsUTCOffset( timezone ) - ? integerToUTCOffset( timezone ) - : timezone; -} - -/** - * Formats a date. Does not alter the date's timezone. - * - * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. - * @param {Date|string|null} dateValue Date object or ISO string. - * - * @return {string} Formatted date. - */ -export function format( dateFormat, dateValue = new Date() ) { - const formatString = translateFormat( dateFormat, dateValue ); - return dateFnsFormat( new Date( dateValue ), formatString ); + newFormat = newFormat.join( '[]' ); + return momentDate.format( newFormat ); } /** @@ -510,7 +365,7 @@ export function format( dateFormat, dateValue = new Date() ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|null} dateValue Date object or ISO string, parsable + * @param {Date|string|Moment|null} dateValue Date object or string, parsable * by moment.js. * @param {string|number|null} timezone Timezone to output result in or a * UTC offset. Defaults to timezone from @@ -522,10 +377,8 @@ export function format( dateFormat, dateValue = new Date() ) { * @return {string} Formatted date in English. */ export function date( dateFormat, dateValue = new Date(), timezone ) { - return format( - dateFormat, - utcToZonedTime( dateValue, getActualTimezone( timezone ) ) - ); + const dateMoment = buildMoment( dateValue, timezone ); + return format( dateFormat, dateMoment ); } /** @@ -533,12 +386,14 @@ export function date( dateFormat, dateValue = new Date(), timezone ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|null} dateValue Date object or ISO string. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. * * @return {string} Formatted date in English. */ export function gmdate( dateFormat, dateValue = new Date() ) { - return format( dateFormat, utcToZonedTime( dateValue, 'UTC' ) ); + const dateMoment = momentLib( dateValue ).utc(); + return format( dateFormat, dateMoment ); } /** @@ -549,7 +404,7 @@ export function gmdate( dateFormat, dateValue = new Date() ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|null} dateValue Date object or string, parsable by + * @param {Date|string|Moment|null} dateValue Date object or string, parsable by * moment.js. * @param {string|number|boolean|null} timezone Timezone to output result in or a * UTC offset. Defaults to timezone from @@ -571,22 +426,9 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { timezone = undefined; } - return formatTZ( - utcToZonedTime( - zonedTimeToUtc( dateValue, getActualTimezone() ), - getActualTimezone( timezone ) - ), - translateFormat( dateFormat, dateValue ), - { - timeZone: getActualTimezone( timezone ), - locale: { - ...originalLocale, - locale: settings.l10n.locale, - code: settings.l10n.locale, - localize: getLocalizationSettings(), - }, - } - ); + const dateMoment = buildMoment( dateValue, timezone ); + dateMoment.locale( settings.l10n.locale ); + return format( dateFormat, dateMoment ); } /** @@ -595,37 +437,29 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|null} dateValue Date object or ISO string. + * @param {Date|string|Moment|null} dateValue Date object or string, + * parsable by moment.js. * * @return {string} Formatted date. */ export function gmdateI18n( dateFormat, dateValue = new Date() ) { - return formatTZ( - utcToZonedTime( dateValue, 'UTC' ), - translateFormat( dateFormat, dateValue ), - { - timeZone: 'UTC', - locale: { - ...originalLocale, - locale: settings.l10n.locale, - code: settings.l10n.locale, - localize: getLocalizationSettings(), - }, - } - ); + const dateMoment = momentLib( dateValue ).utc(); + dateMoment.locale( settings.l10n.locale ); + return format( dateFormat, dateMoment ); } /** * Check whether a date is considered in the future according to the WordPress settings. * - * @param {string|Date} dateValue Date String or Date object in the Defined WP Timezone. + * @param {string} dateValue Date String or Date object in the Defined WP Timezone. * * @return {boolean} Is in the future. */ export function isInTheFuture( dateValue ) { - const dateObject = toDate( dateValue, { timeZone: getActualTimezone() } ); + const now = momentLib.tz( WP_ZONE ); + const momentObject = momentLib.tz( dateValue, WP_ZONE ); - return isFuture( dateObject ); + return momentObject.isAfter( now ); } /** @@ -636,8 +470,58 @@ export function isInTheFuture( dateValue ) { * @return {Date} Date */ export function getDate( dateString ) { - const actualDate = dateString ? new Date( dateString ) : new Date(); - return toDate( actualDate, { timeZone: getActualTimezone() } ); + if ( ! dateString ) { + return momentLib.tz( WP_ZONE ).toDate(); + } + + return momentLib.tz( dateString, WP_ZONE ).toDate(); +} + +/** + * Creates a moment instance using the given timezone or, if none is provided, using global settings. + * + * @param {Date|string|Moment|null} dateValue Date object or string, parsable + * by moment.js. + * @param {string|number|null} timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. + * + * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC + * + * @return {Moment} a moment instance. + */ +function buildMoment( dateValue, timezone = '' ) { + const dateMoment = momentLib( dateValue ); + + if ( timezone && ! isUTCOffset( timezone ) ) { + return dateMoment.tz( timezone ); + } + + if ( timezone && isUTCOffset( timezone ) ) { + return dateMoment.utcOffset( timezone ); + } + + if ( settings.timezone.string ) { + return dateMoment.tz( settings.timezone.string ); + } + + return dateMoment.utcOffset( settings.timezone.offset ); +} + +/** + * Returns whether a certain UTC offset is valid or not. + * + * @param {number|string} offset a UTC offset. + * + * @return {boolean} whether a certain UTC offset is valid or not. + */ +function isUTCOffset( offset ) { + if ( 'number' === typeof offset ) { + return true; + } + + return VALID_UTC_OFFSET.test( offset ); } -export { zonedTimeToUtc }; +setupWPTimezone(); diff --git a/packages/date/src/test/index.js b/packages/date/src/test/index.js index 2c5479cfcd4c3..d9b832e528ce6 100644 --- a/packages/date/src/test/index.js +++ b/packages/date/src/test/index.js @@ -191,332 +191,6 @@ describe( 'Function date', () => { } ); } ); -// Custom formatting token functions, in order to support PHP formatting tokens -describe( 'PHP Format Tokens', () => { - it( 'should support "d" to obtain day of the month, 2 digits with leading zeroes', () => { - const formattedDate = dateNoI18n( 'd', '2019-06-06T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '06' ); - } ); - - it( 'should support "D" to obtain textual representation of a day, three letters', () => { - const formattedDate = dateNoI18n( 'D', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'Tue' ); - } ); - - it( 'should support "j" to obtain day of the month without leading zeroes', () => { - const formattedDate = dateNoI18n( 'j', '2019-06-06T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '6' ); - } ); - - it( 'should support "l" to obtain full textual representation of the day of the week', () => { - const formattedDate = dateNoI18n( 'l', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'Tuesday' ); - } ); - - it( 'should support "N" to obtain ISO-8601 numeric representation of the day of the week', () => { - const formattedDate = dateNoI18n( 'N', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '2' ); // 2 === Tuesday - } ); - - it( 'should support "S" to obtain ordinal suffix of day of the month', () => { - const formattedDate = dateNoI18n( 'S', '2019-06-18T11:00:00.000Z' ); - - // th for 18th - expect( formattedDate ).toBe( 'th' ); - } ); - - it( 'should support "w" to obtain day of the week starting from 0', () => { - const formattedDate = dateNoI18n( 'w', '2020-01-01T12:00:00.000Z' ); // Wednesday Jan 1st, 2020 - - expect( formattedDate ).toBe( '2' ); - } ); - - it( 'should support "z" to obtain zero-indexed day of the year', () => { - const formattedDate = dateNoI18n( 'z', '2019-01-01' ); - - expect( formattedDate ).toBe( '0' ); - } ); - - it( 'should support "W" to obtain ISO-8601 week number of year, weeks starting on Monday', () => { - const formattedDate = dateNoI18n( 'W', '2019-01-06T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '01' ); - } ); - - it( 'should support "F" to obtain a full textual representation of a month', () => { - const formattedDate = dateNoI18n( 'F', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'June' ); - } ); - - it( 'should support "m" to obtain the numeric representation of a month, with leading zeroes', () => { - const formattedDate = dateNoI18n( 'm', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '06' ); - } ); - - it( 'should support "M" to obtain a three letter textual representation of a month', () => { - const formattedDate = dateNoI18n( 'M', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'Jun' ); - } ); - - it( 'should "n" to obtain the numeric representation of a month without leading zeroes', () => { - const formattedDate = dateNoI18n( 'n', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '6' ); - } ); - - it( 'should support "t" to obtain the days in a given month', () => { - const formattedDate = dateNoI18n( 't', '2019-02' ); - - expect( formattedDate ).toBe( '28' ); - } ); - - it( 'should support "L" to obtain whether or not the year is a leap year', () => { - const formattedDate = dateNoI18n( 'L', '2020' ); - - expect( formattedDate ).toBe( '1' ); - } ); - - it( 'should support "o" to obtain the ISO-8601 week-numbering year. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead.', () => { - const formattedDate = dateNoI18n( 'o', '2019-01-01T11:00:00.000Z' ); - const formattedDatePreviousYear = dateNoI18n( - 'o', - '2017-01-01T11:00:00.000Z' - ); // ISO week number belongs to previous year - - expect( formattedDate ).toBe( '2019' ); - expect( formattedDatePreviousYear ).toBe( '2016' ); - } ); - - it( 'should support "Y" to obtain a full numeric representation of a year', () => { - const formattedDate = dateNoI18n( 'Y', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '2019' ); - } ); - - it( 'should support "y" to obtain a two digit representation of a year', () => { - const formattedDate = dateNoI18n( 'y', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '19' ); - } ); - - it( 'should support "a" to obtain a lowercase ante meridiem and post meridiem', () => { - const formattedDate = dateNoI18n( 'a', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'am' ); - } ); - - it( 'should support "A" to obtain uppercase ante meridiem and post meridiem', () => { - const formattedDate = dateNoI18n( 'A', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'AM' ); - } ); - - it( 'should support "B" to obtain the time in Swatch Internet Time (.beats)', () => { - const formattedDate = dateNoI18n( 'B', '2020-10-09T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '500' ); - } ); - - it( 'should support "g" to obtain the 12-hour format of an hour without leading zeroes', () => { - const formattedDate = dateNoI18n( 'g', '2019-06-18T14:00:00.000Z' ); - - expect( formattedDate ).toBe( '2' ); - } ); - - it( 'should support "G" to obtain the 24-hour format of an hour without leading zeroes', () => { - const formattedDate = dateNoI18n( 'G', '2019-06-18T09:00:00.000Z' ); - - expect( formattedDate ).toBe( '9' ); - } ); - - it( 'should support "h" 12-hour format of an hour with leading zeroes', () => { - const formattedDate = dateNoI18n( 'h', '2019-06-18T14:00:00.000Z' ); - - expect( formattedDate ).toBe( '02' ); - } ); - - it( 'should support "H" 24-hour format of an hour with leading zeroes', () => { - const formattedDate = dateNoI18n( 'H', '2019-06-18T09:00:00.000Z' ); - - expect( formattedDate ).toBe( '09' ); - } ); - - it( 'should support "i" to obtain the minutes with leading zeroes', () => { - const formattedDate = dateNoI18n( 'i', '2019-06-18T11:01:00.000Z' ); - - expect( formattedDate ).toBe( '01' ); - } ); - - it( 'should support "s" to obtain seconds with leading zeroes', () => { - const formattedDate = dateNoI18n( 's', '2019-06-18T11:00:04.000Z' ); - - expect( formattedDate ).toBe( '04' ); - } ); - - /** - * This format is not fully compatible with JavaScript out of the box, - * as Date doesn't support sub-millisecond precision. - */ - it( 'should support "u" to obtain microseconds', () => { - const formattedDate = dateNoI18n( - 'u', - '2019-06-18T11:00:00.123456789Z' - ); - - expect( formattedDate ).toBe( '123000' ); - } ); - - it( 'should support "v" to obtain milliseconds', () => { - const formattedDate = dateNoI18n( - 'v', - '2019-06-18T11:00:00.123456789Z' - ); - - expect( formattedDate ).toBe( '123' ); - } ); - - it( 'should support "e" to obtain timezone identifier', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -4, string: 'America/New_York' }, - } ); - - const formattedDate = dateNoI18n( 'e', '2020-10-09T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'Eastern Daylight Time' ); - - setSettings( settings ); - } ); - - it.skip( 'should support "I" to obtain whether or not the timezone is observing DST', () => { - const formattedFall = dateNoI18n( 'I', '2020-10-09T11:00:00.000Z' ); - - expect( formattedFall ).toBe( '1' ); - - const formattedWinter = dateNoI18n( 'I', '2020-01-09T11:00:00.000Z' ); - - expect( formattedWinter ).toBe( '0' ); - } ); - - it( 'should support "O" to obtain difference to Greenwich time (GMT) without colon between hours and minutes', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -6 }, - } ); - - const formattedDate = dateNoI18n( 'O', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '-0600' ); - - setSettings( settings ); - } ); - - it( 'should support "P" to obtain difference to Greenwich time (GMT) without colon between hours and minutes', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -6 }, - } ); - - const formattedDate = dateNoI18n( 'P', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '-06:00' ); - - setSettings( settings ); - } ); - - it( 'should support "T" to obtain the timezone abbreviation for the given date', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -4, string: 'America/New_York' }, - } ); - - const formattedDateStandard = dateNoI18n( - 'T', - '2020-01-01T11:00:00.000Z' - ); - - expect( formattedDateStandard ).toBe( 'EST' ); - - setSettings( settings ); - } ); - - it.skip( 'should support "Z" to obtain timezone offset in seconds', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -1 }, - } ); - - const formattedDate = dateNoI18n( 'Z', '2020-10-09T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '3600' ); - - setSettings( settings ); - } ); - - it( 'should support "c" to obtain ISO 8601 date', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -5, string: 'America/Bogota' }, - } ); - - const formattedDate = dateNoI18n( 'c', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '2019-06-18T11:00:00-05:00' ); - - setSettings( settings ); - } ); - - it( 'should support "r" RFC 2822 formatted date', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { offset: -5, string: 'America/Bogota' }, - } ); - - const formattedDate = dateNoI18n( 'r', '2019-06-18T11:00:00.000Z' ); - - expect( formattedDate ).toBe( 'Tue, 18 Jun 2019 11:00:00 -0500' ); - - setSettings( settings ); - } ); - - it( 'should support "U" to get epoc for given date', () => { - const settings = __experimentalGetSettings(); - - setSettings( { - ...settings, - timezone: { string: 'UTC' }, - } ); - - const formattedDate = dateNoI18n( 'U', '2020-10-09T11:00:00.000Z' ); - - expect( formattedDate ).toBe( '1602241200' ); - - setSettings( settings ); - } ); -} ); - describe( 'Function gmdate', () => { it( 'should format date in English, ignoring locale settings', () => { const settings = __experimentalGetSettings(); diff --git a/packages/editor/src/components/post-schedule/index.js b/packages/editor/src/components/post-schedule/index.js index 115d5b96f1578..06266f1bd68d9 100644 --- a/packages/editor/src/components/post-schedule/index.js +++ b/packages/editor/src/components/post-schedule/index.js @@ -9,7 +9,6 @@ import { useRef } from '@wordpress/element'; export function PostSchedule( { date, onUpdateDate } ) { const ref = useRef(); - const settings = __experimentalGetSettings(); // To know if the current timezone is a 12 hour time with look for "a" in the time format // We also make sure this a is not escaped by a "/" diff --git a/packages/editor/src/components/post-schedule/label.js b/packages/editor/src/components/post-schedule/label.js index d8a2a1b47bc2a..5f4c2cf7c5c77 100644 --- a/packages/editor/src/components/post-schedule/label.js +++ b/packages/editor/src/components/post-schedule/label.js @@ -2,22 +2,18 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; - -import { dateI18n, __experimentalGetSettings } from '@wordpress/date'; - +import { format, __experimentalGetSettings } from '@wordpress/date'; import { withSelect } from '@wordpress/data'; export function PostScheduleLabel( { date, isFloating } ) { - if ( isFloating || ! date ) { - return __( 'Immediately' ); - } - const settings = __experimentalGetSettings(); - return dateI18n( - `${ settings.formats.date } ${ settings.formats.time }`, - date - ); + return date && ! isFloating + ? format( + `${ settings.formats.date } ${ settings.formats.time }`, + date + ) + : __( 'Immediately' ); } export default withSelect( ( select ) => { From 3dafc9e9712b04a03738137907642238a1d79b58 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 3 Dec 2020 14:38:36 +0100 Subject: [PATCH 2/3] Popover: Fix issue with undefined getBoundingClientRect (#27445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix an issue where the `Popover` component throws a runtime `TypeError: Cannot read property 'getBoundingClientRect' of undefined.` This may happen in WordPress if a Popover is rendered in an iframe. It's been observed by rendering a `BlockList` component containing `RichText` in an iframe 😵 The issue is that `instanceof` checks fail across iframe boundaries, where `anchorRef instanceof window.Element` fails because `anchorRef` when it _is_ an instance of Element in the frame but not `window.Element`. Instead of `instanceof` checks that fail across iframe boundaries, check for expected methods as the predicate. --- packages/components/src/popover/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index a71c6cc6ad895..6063c11c04e62 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -71,11 +71,17 @@ function computeAnchorRect( return; } - if ( anchorRef instanceof window.Range ) { + // Duck-type to check if `anchorRef` is an instance of Range + // `anchorRef instanceof window.Range` checks will break across document boundaries + // such as in an iframe + if ( typeof anchorRef?.cloneRange === 'function' ) { return getRectangleFromRange( anchorRef ); } - if ( anchorRef instanceof window.Element ) { + // Duck-type to check if `anchorRef` is an instance of Element + // `anchorRef instanceof window.Element` checks will break across document boundaries + // such as in an iframe + if ( typeof anchorRef?.getBoundingClientRect === 'function' ) { const rect = anchorRef.getBoundingClientRect(); if ( shouldAnchorIncludePadding ) { From 94b3cabcf70cf18f2d304fdd2905d70fd7c1f848 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Mon, 7 Dec 2020 11:01:10 +0800 Subject: [PATCH 3/3] Fallback to regular subscribe if the store doesn't exist in useSelect (#27466) --- .../src/components/use-select/test/index.js | 154 ++++++++++++++++++ packages/data/src/registry.js | 8 + 2 files changed, 162 insertions(+) diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index d5415191ffcdd..4b8d8a9ce35c5 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -283,6 +283,9 @@ describe( 'useSelect', () => { expect( selectCount2 ).toHaveBeenCalledTimes( 3 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 ); + + // Test if the unsubscribers get called correctly. + renderer.unmount(); } ); it( 'can subscribe to multiple stores at once', () => { @@ -565,5 +568,156 @@ describe( 'useSelect', () => { childCount: 0, } ); } ); + + it( 'handles non-existing stores', () => { + registry.registerStore( 'store-1', counterStore ); + + let renderer; + + const TestComponent = jest.fn( () => { + const state = useSelect( + ( select ) => ( { + count1: select( 'store-1' ).getCounter(), + blank: select( 'non-existing-store' )?.getCounter(), + } ), + [] + ); + + return
; + } ); + + act( () => { + renderer = TestRenderer.create( + + + + ); + } ); + + const testInstance = renderer.root; + + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + blank: undefined, + } ); + + act( () => { + registry.dispatch( 'store-1' ).increment(); + } ); + + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 1, + blank: undefined, + } ); + + // Test if the unsubscribers get called correctly. + renderer.unmount(); + } ); + + it( 'handles registration of a non-existing store during rendering', () => { + let renderer; + + const TestComponent = jest.fn( () => { + const state = useSelect( + ( select ) => + select( 'not-yet-registered-store' )?.getCounter(), + [] + ); + + return
; + } ); + + act( () => { + renderer = TestRenderer.create( + + + + ); + } ); + + const testInstance = renderer.root; + + expect( testInstance.findByType( 'div' ).props.data ).toBe( + undefined + ); + + act( () => { + registry.registerStore( + 'not-yet-registered-store', + counterStore + ); + } ); + + // This is not ideal, but is the way it's working before and we want to prevent breaking changes. + expect( testInstance.findByType( 'div' ).props.data ).toBe( + undefined + ); + + act( () => { + registry.dispatch( 'not-yet-registered-store' ).increment(); + } ); + + expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 ); + + // Test if the unsubscribers get called correctly. + renderer.unmount(); + } ); + + it( 'handles registration of a non-existing store of sub-registry during rendering', () => { + let renderer; + + const subRegistry = createRegistry( {}, registry ); + + const TestComponent = jest.fn( () => { + const state = useSelect( + ( select ) => + select( + 'not-yet-registered-child-store' + )?.getCounter(), + [] + ); + + return
; + } ); + + act( () => { + renderer = TestRenderer.create( + + + + + + ); + } ); + + const testInstance = renderer.root; + + expect( testInstance.findByType( 'div' ).props.data ).toBe( + undefined + ); + + act( () => { + registry.registerStore( + 'not-yet-registered-child-store', + counterStore + ); + } ); + + // This is not ideal, but is the way it's working before and we want to prevent breaking changes. + expect( testInstance.findByType( 'div' ).props.data ).toBe( + undefined + ); + + act( () => { + registry + .dispatch( 'not-yet-registered-child-store' ) + .increment(); + } ); + + expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 ); + + // Test if the unsubscribers get called correctly. + renderer.unmount(); + } ); } ); } ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index cda0b555c5480..6a0ca9fc4a7a4 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -234,6 +234,14 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return stores[ storeName ].subscribe( handler ); } + // Trying to access a store that hasn't been registered, + // this is a pattern rarely used but seen in some places. + // We fallback to regular `subscribe` here for backward-compatibility for now. + // See https://github.com/WordPress/gutenberg/pull/27466 for more info. + if ( ! parent ) { + return subscribe( handler ); + } + return parent.__experimentalSubscribeStore( storeName, handler ); }