diff -r 34716fd837a4 -r be944660c56a wp/wp-admin/js/dashboard.js --- a/wp/wp-admin/js/dashboard.js Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-admin/js/dashboard.js Wed Sep 21 18:19:35 2022 +0200 @@ -5,13 +5,14 @@ /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, ajaxWidgets */ /* global ajaxPopulateWidgets, quickPressLoad, */ window.wp = window.wp || {}; +window.communityEventsData = window.communityEventsData || {}; /** * Initializes the dashboard widget functionality. * * @since 2.7.0 */ -jQuery(document).ready( function($) { +jQuery( function($) { var welcomePanel = $( '#welcome-panel' ), welcomePanelHide = $('#wp_welcome_panel-hide'), updateWelcomePanel; @@ -39,7 +40,7 @@ } // Hide the welcome panel when the dismiss button or close button is clicked. - $('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).click( function(e) { + $('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).on( 'click', function(e) { e.preventDefault(); welcomePanel.addClass('hidden'); updateWelcomePanel( 0 ); @@ -47,7 +48,7 @@ }); // Set welcome panel visibility based on Welcome Option checkbox value. - welcomePanelHide.click( function() { + welcomePanelHide.on( 'click', function() { welcomePanel.toggleClass('hidden', ! this.checked ); updateWelcomePanel( this.checked ? 1 : 0 ); }); @@ -135,7 +136,7 @@ // Enable the submit buttons. $( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false ); - t = $('#quick-press').submit( function( e ) { + t = $('#quick-press').on( 'submit', function( e ) { e.preventDefault(); // Show a spinner. @@ -153,7 +154,7 @@ highlightLatestPost(); // Focus the title to allow for quickly drafting another post. - $('#title').focus(); + $('#title').trigger( 'focus' ); }); /** @@ -171,7 +172,7 @@ } ); // Change the QuickPost action to the publish value. - $('#publish').click( function() { act.val( 'post-quickpress-publish' ); } ); + $('#publish').on( 'click', function() { act.val( 'post-quickpress-publish' ); } ); $('#quick-press').on( 'click focusin', function() { wpActiveEditor = 'content'; @@ -265,7 +266,12 @@ jQuery( function( $ ) { 'use strict'; - var communityEventsData = window.communityEventsData || {}, + var communityEventsData = window.communityEventsData, + dateI18n = wp.date.dateI18n, + format = wp.date.format, + sprintf = wp.i18n.sprintf, + __ = wp.i18n.__, + _x = wp.i18n._x, app; /** @@ -322,7 +328,7 @@ * @return {void} */ $container.on( 'submit', '.community-events-form', function( event ) { - var location = $.trim( $( '#community-events-location' ).val() ); + var location = $( '#community-events-location' ).val().trim(); event.preventDefault(); @@ -385,7 +391,7 @@ * lose their place. */ if ( $target.hasClass( 'community-events-cancel' ) ) { - $toggleButton.focus(); + $toggleButton.trigger( 'focus' ); } } else { $toggleButton.attr( 'aria-expanded', 'true' ); @@ -441,6 +447,7 @@ .fail( function() { app.renderEventsTemplate({ 'location' : false, + 'events' : [], 'error' : true }, initiatedBy ); }); @@ -460,11 +467,15 @@ renderEventsTemplate: function( templateParams, initiatedBy ) { var template, elementVisibility, - l10nPlaceholder = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc. $toggleButton = $( '.community-events-toggle-location' ), $locationMessage = $( '#community-events-location-message' ), $results = $( '.community-events-results' ); + templateParams.events = app.populateDynamicEventFields( + templateParams.events, + communityEventsData.time_format + ); + /* * Hide all toggleable elements by default, to keep the logic simple. * Otherwise, each block below would have to turn hide everything that @@ -494,7 +505,7 @@ * If the API determined the location by geolocating an IP, it will * provide events, but not a specific location. */ - $locationMessage.text( communityEventsData.l10n.attend_event_near_generic ); + $locationMessage.text( __( 'Attend an upcoming event near you.' ) ); if ( templateParams.events.length ) { template = wp.template( 'community-events-event-list' ); @@ -521,7 +532,14 @@ } if ( 'user' === initiatedBy ) { - wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location.description ), 'assertive' ); + wp.a11y.speak( + sprintf( + /* translators: %s: The name of a city. */ + __( 'City updated. Listing events near %s.' ), + templateParams.location.description + ), + 'assertive' + ); } elementVisibility['#community-events-location-message'] = true; @@ -531,7 +549,28 @@ } else if ( templateParams.unknownCity ) { template = wp.template( 'community-events-could-not-locate' ); $( '.community-events-could-not-locate' ).html( template( templateParams ) ); - wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) ); + wp.a11y.speak( + sprintf( + /* + * These specific examples were chosen to highlight the fact that a + * state is not needed, even for cities whose name is not unique. + * It would be too cumbersome to include that in the instructions + * to the user, so it's left as an implication. + */ + /* + * translators: %s is the name of the city we couldn't locate. + * Replace the examples with cities related to your locale. Test that + * they match the expected location and have upcoming events before + * including them. If no cities related to your locale have events, + * then use cities related to your locale that would be recognizable + * to most users. Use only the city name itself, without any region + * or country. Use the endonym (native locale name) instead of the + * English name if possible. + */ + __( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ), + templateParams.unknownCity + ) + ); elementVisibility['.community-events-errors'] = true; elementVisibility['.community-events-could-not-locate'] = true; @@ -543,12 +582,12 @@ * Showing error messages for an event that user isn't aware of * could be confusing or unnecessarily distracting. */ - wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again ); + wp.a11y.speak( __( 'An error occurred. Please try again.' ) ); elementVisibility['.community-events-errors'] = true; elementVisibility['.community-events-error-occurred'] = true; } else { - $locationMessage.text( communityEventsData.l10n.enter_closest_city ); + $locationMessage.text( __( 'Enter your closest city to find nearby events.' ) ); elementVisibility['#community-events-location-message'] = true; elementVisibility['.community-events-toggle-location'] = true; @@ -571,11 +610,200 @@ * bring the focus back to the toggle button so users relying * on screen readers don't lose their place. */ - $toggleButton.focus(); + $toggleButton.trigger( 'focus' ); } } else { app.toggleLocationForm( 'show' ); } + }, + + /** + * Populate event fields that have to be calculated on the fly. + * + * These can't be stored in the database, because they're dependent on + * the user's current time zone, locale, etc. + * + * @since 5.5.2 + * + * @param {Array} rawEvents The events that should have dynamic fields added to them. + * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`. + * + * @returns {Array} + */ + populateDynamicEventFields: function( rawEvents, timeFormat ) { + // Clone the parameter to avoid mutating it, so that this can remain a pure function. + var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) ); + + $.each( populatedEvents, function( index, event ) { + var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 ); + + event.user_formatted_date = app.getFormattedDate( + event.start_unix_timestamp * 1000, + event.end_unix_timestamp * 1000, + timeZone + ); + + event.user_formatted_time = dateI18n( + timeFormat, + event.start_unix_timestamp * 1000, + timeZone + ); + + event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 ); + } ); + + return populatedEvents; + }, + + /** + * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`. + * + * @since 5.5.2 + * + * @param startTimestamp + * + * @returns {string|number} + */ + getTimeZone: function( startTimestamp ) { + /* + * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This + * doesn't need to take `startTimestamp` into account for that reason. + */ + var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + /* + * Fall back to an offset for IE11, which declares the property but doesn't assign a value. + */ + if ( 'undefined' === typeof timeZone ) { + /* + * It's important to use the _event_ time, not the _current_ + * time, so that daylight savings time is accounted for. + */ + timeZone = app.getFlippedTimeZoneOffset( startTimestamp ); + } + + return timeZone; + }, + + /** + * Get intuitive time zone offset. + * + * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones + * that are _behind_ UTC, and a _negative_ value for ones that are ahead. + * + * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off. + * + * @since 5.5.2 + * + * @param {number} startTimestamp + * + * @returns {number} + */ + getFlippedTimeZoneOffset: function( startTimestamp ) { + return new Date( startTimestamp ).getTimezoneOffset() * -1; + }, + + /** + * Get a short time zone name, like `PST`. + * + * @since 5.5.2 + * + * @param {number} startTimestamp + * + * @returns {string} + */ + getTimeZoneAbbreviation: function( startTimestamp ) { + var timeZoneAbbreviation, + eventDateTime = new Date( startTimestamp ); + + /* + * Leaving the `locales` argument undefined is important, so that the browser + * displays the abbreviation that's most appropriate for the current locale. For + * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`. + * + * This doesn't need to take `startTimestamp` into account, because a name like + * `America/Chicago` automatically tracks daylight savings. + */ + var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' ); + + if ( 3 === shortTimeStringParts.length ) { + timeZoneAbbreviation = shortTimeStringParts[2]; + } + + if ( 'undefined' === typeof timeZoneAbbreviation ) { + /* + * It's important to use the _event_ time, not the _current_ + * time, so that daylight savings time is accounted for. + */ + var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ), + sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+'; + + // translators: Used as part of a string like `GMT+5` in the Events Widget. + timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 ); + } + + return timeZoneAbbreviation; + }, + + /** + * Format a start/end date in the user's local time zone and locale. + * + * @since 5.5.2 + * + * @param {int} startDate The Unix timestamp in milliseconds when the the event starts. + * @param {int} endDate The Unix timestamp in milliseconds when the the event ends. + * @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`. + * + * @returns {string} + */ + getFormattedDate: function( startDate, endDate, timeZone ) { + var formattedDate; + + /* + * The `date_format` option is not used because it's important + * in this context to keep the day of the week in the displayed date, + * so that users can tell at a glance if the event is on a day they + * are available, without having to open the link. + * + * The case of crossing a year boundary is intentionally not handled. + * It's so rare in practice that it's not worth the complexity + * tradeoff. The _ending_ year should be passed to + * `multiple_month_event`, though, just in case. + */ + /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */ + var singleDayEvent = __( 'l, M j, Y' ), + /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */ + multipleDayEvent = __( '%1$s %2$d–%3$d, %4$d' ), + /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */ + multipleMonthEvent = __( '%1$s %2$d – %3$s %4$d, %5$d' ); + + // Detect single-day events. + if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) { + formattedDate = dateI18n( singleDayEvent, startDate, timeZone ); + + // Multiple day events. + } else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) { + formattedDate = sprintf( + multipleDayEvent, + dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ), + dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone ) + ); + + // Multi-day events that cross a month boundary. + } else { + formattedDate = sprintf( + multipleMonthEvent, + dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ), + dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ), + dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone ) + ); + } + + return formattedDate; } }; @@ -591,3 +819,21 @@ }); } }); + +/** + * Removed in 5.6.0, needed for back-compatibility. + * + * @since 4.8.0 + * @deprecated 5.6.0 + * + * @type {object} +*/ +window.communityEventsData.l10n = window.communityEventsData.l10n || { + enter_closest_city: '', + error_occurred_please_try_again: '', + attend_event_near_generic: '', + could_not_locate_city: '', + city_updated: '' +}; + +window.communityEventsData.l10n = window.wp.deprecateL10nObject( 'communityEventsData.l10n', window.communityEventsData.l10n, '5.6.0' );