You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

839 lines
27 KiB

1 year ago
  1. /**
  2. * @output wp-admin/js/dashboard.js
  3. */
  4. /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, ajaxWidgets */
  5. /* global ajaxPopulateWidgets, quickPressLoad, */
  6. window.wp = window.wp || {};
  7. window.communityEventsData = window.communityEventsData || {};
  8. /**
  9. * Initializes the dashboard widget functionality.
  10. *
  11. * @since 2.7.0
  12. */
  13. jQuery( function($) {
  14. var welcomePanel = $( '#welcome-panel' ),
  15. welcomePanelHide = $('#wp_welcome_panel-hide'),
  16. updateWelcomePanel;
  17. /**
  18. * Saves the visibility of the welcome panel.
  19. *
  20. * @since 3.3.0
  21. *
  22. * @param {boolean} visible Should it be visible or not.
  23. *
  24. * @return {void}
  25. */
  26. updateWelcomePanel = function( visible ) {
  27. $.post( ajaxurl, {
  28. action: 'update-welcome-panel',
  29. visible: visible,
  30. welcomepanelnonce: $( '#welcomepanelnonce' ).val()
  31. });
  32. };
  33. // Unhide the welcome panel if the Welcome Option checkbox is checked.
  34. if ( welcomePanel.hasClass('hidden') && welcomePanelHide.prop('checked') ) {
  35. welcomePanel.removeClass('hidden');
  36. }
  37. // Hide the welcome panel when the dismiss button or close button is clicked.
  38. $('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).on( 'click', function(e) {
  39. e.preventDefault();
  40. welcomePanel.addClass('hidden');
  41. updateWelcomePanel( 0 );
  42. $('#wp_welcome_panel-hide').prop('checked', false);
  43. });
  44. // Set welcome panel visibility based on Welcome Option checkbox value.
  45. welcomePanelHide.on( 'click', function() {
  46. welcomePanel.toggleClass('hidden', ! this.checked );
  47. updateWelcomePanel( this.checked ? 1 : 0 );
  48. });
  49. /**
  50. * These widgets can be populated via ajax.
  51. *
  52. * @since 2.7.0
  53. *
  54. * @type {string[]}
  55. *
  56. * @global
  57. */
  58. window.ajaxWidgets = ['dashboard_primary'];
  59. /**
  60. * Triggers widget updates via Ajax.
  61. *
  62. * @since 2.7.0
  63. *
  64. * @global
  65. *
  66. * @param {string} el Optional. Widget to fetch or none to update all.
  67. *
  68. * @return {void}
  69. */
  70. window.ajaxPopulateWidgets = function(el) {
  71. /**
  72. * Fetch the latest representation of the widget via Ajax and show it.
  73. *
  74. * @param {number} i Number of half-seconds to use as the timeout.
  75. * @param {string} id ID of the element which is going to be checked for changes.
  76. *
  77. * @return {void}
  78. */
  79. function show(i, id) {
  80. var p, e = $('#' + id + ' div.inside:visible').find('.widget-loading');
  81. // If the element is found in the dom, queue to load latest representation.
  82. if ( e.length ) {
  83. p = e.parent();
  84. setTimeout( function(){
  85. // Request the widget content.
  86. p.load( ajaxurl + '?action=dashboard-widgets&widget=' + id + '&pagenow=' + pagenow, '', function() {
  87. // Hide the parent and slide it out for visual fancyness.
  88. p.hide().slideDown('normal', function(){
  89. $(this).css('display', '');
  90. });
  91. });
  92. }, i * 500 );
  93. }
  94. }
  95. // If we have received a specific element to fetch, check if it is valid.
  96. if ( el ) {
  97. el = el.toString();
  98. // If the element is available as Ajax widget, show it.
  99. if ( $.inArray(el, ajaxWidgets) !== -1 ) {
  100. // Show element without any delay.
  101. show(0, el);
  102. }
  103. } else {
  104. // Walk through all ajaxWidgets, loading them after each other.
  105. $.each( ajaxWidgets, show );
  106. }
  107. };
  108. // Initially populate ajax widgets.
  109. ajaxPopulateWidgets();
  110. // Register ajax widgets as postbox toggles.
  111. postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
  112. /**
  113. * Control the Quick Press (Quick Draft) widget.
  114. *
  115. * @since 2.7.0
  116. *
  117. * @global
  118. *
  119. * @return {void}
  120. */
  121. window.quickPressLoad = function() {
  122. var act = $('#quickpost-action'), t;
  123. // Enable the submit buttons.
  124. $( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false );
  125. t = $('#quick-press').on( 'submit', function( e ) {
  126. e.preventDefault();
  127. // Show a spinner.
  128. $('#dashboard_quick_press #publishing-action .spinner').show();
  129. // Disable the submit button to prevent duplicate submissions.
  130. $('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true);
  131. // Post the entered data to save it.
  132. $.post( t.attr( 'action' ), t.serializeArray(), function( data ) {
  133. // Replace the form, and prepend the published post.
  134. $('#dashboard_quick_press .inside').html( data );
  135. $('#quick-press').removeClass('initial-form');
  136. quickPressLoad();
  137. highlightLatestPost();
  138. // Focus the title to allow for quickly drafting another post.
  139. $('#title').trigger( 'focus' );
  140. });
  141. /**
  142. * Highlights the latest post for one second.
  143. *
  144. * @return {void}
  145. */
  146. function highlightLatestPost () {
  147. var latestPost = $('.drafts ul li').first();
  148. latestPost.css('background', '#fffbe5');
  149. setTimeout(function () {
  150. latestPost.css('background', 'none');
  151. }, 1000);
  152. }
  153. } );
  154. // Change the QuickPost action to the publish value.
  155. $('#publish').on( 'click', function() { act.val( 'post-quickpress-publish' ); } );
  156. $('#quick-press').on( 'click focusin', function() {
  157. wpActiveEditor = 'content';
  158. });
  159. autoResizeTextarea();
  160. };
  161. window.quickPressLoad();
  162. // Enable the dragging functionality of the widgets.
  163. $( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
  164. /**
  165. * Adjust the height of the textarea based on the content.
  166. *
  167. * @since 3.6.0
  168. *
  169. * @return {void}
  170. */
  171. function autoResizeTextarea() {
  172. // When IE8 or older is used to render this document, exit.
  173. if ( document.documentMode && document.documentMode < 9 ) {
  174. return;
  175. }
  176. // Add a hidden div. We'll copy over the text from the textarea to measure its height.
  177. $('body').append( '<div class="quick-draft-textarea-clone" style="display: none;"></div>' );
  178. var clone = $('.quick-draft-textarea-clone'),
  179. editor = $('#content'),
  180. editorHeight = editor.height(),
  181. /*
  182. * 100px roughly accounts for browser chrome and allows the
  183. * save draft button to show on-screen at the same time.
  184. */
  185. editorMaxHeight = $(window).height() - 100;
  186. /*
  187. * Match up textarea and clone div as much as possible.
  188. * Padding cannot be reliably retrieved using shorthand in all browsers.
  189. */
  190. clone.css({
  191. 'font-family': editor.css('font-family'),
  192. 'font-size': editor.css('font-size'),
  193. 'line-height': editor.css('line-height'),
  194. 'padding-bottom': editor.css('paddingBottom'),
  195. 'padding-left': editor.css('paddingLeft'),
  196. 'padding-right': editor.css('paddingRight'),
  197. 'padding-top': editor.css('paddingTop'),
  198. 'white-space': 'pre-wrap',
  199. 'word-wrap': 'break-word',
  200. 'display': 'none'
  201. });
  202. // The 'propertychange' is used in IE < 9.
  203. editor.on('focus input propertychange', function() {
  204. var $this = $(this),
  205. // Add a non-breaking space to ensure that the height of a trailing newline is
  206. // included.
  207. textareaContent = $this.val() + '&nbsp;',
  208. // Add 2px to compensate for border-top & border-bottom.
  209. cloneHeight = clone.css('width', $this.css('width')).text(textareaContent).outerHeight() + 2;
  210. // Default to show a vertical scrollbar, if needed.
  211. editor.css('overflow-y', 'auto');
  212. // Only change the height if it has changed and both heights are below the max.
  213. if ( cloneHeight === editorHeight || ( cloneHeight >= editorMaxHeight && editorHeight >= editorMaxHeight ) ) {
  214. return;
  215. }
  216. /*
  217. * Don't allow editor to exceed the height of the window.
  218. * This is also bound in CSS to a max-height of 1300px to be extra safe.
  219. */
  220. if ( cloneHeight > editorMaxHeight ) {
  221. editorHeight = editorMaxHeight;
  222. } else {
  223. editorHeight = cloneHeight;
  224. }
  225. // Disable scrollbars because we adjust the height to the content.
  226. editor.css('overflow', 'hidden');
  227. $this.css('height', editorHeight + 'px');
  228. });
  229. }
  230. } );
  231. jQuery( function( $ ) {
  232. 'use strict';
  233. var communityEventsData = window.communityEventsData,
  234. dateI18n = wp.date.dateI18n,
  235. format = wp.date.format,
  236. sprintf = wp.i18n.sprintf,
  237. __ = wp.i18n.__,
  238. _x = wp.i18n._x,
  239. app;
  240. /**
  241. * Global Community Events namespace.
  242. *
  243. * @since 4.8.0
  244. *
  245. * @memberOf wp
  246. * @namespace wp.communityEvents
  247. */
  248. app = window.wp.communityEvents = /** @lends wp.communityEvents */{
  249. initialized: false,
  250. model: null,
  251. /**
  252. * Initializes the wp.communityEvents object.
  253. *
  254. * @since 4.8.0
  255. *
  256. * @return {void}
  257. */
  258. init: function() {
  259. if ( app.initialized ) {
  260. return;
  261. }
  262. var $container = $( '#community-events' );
  263. /*
  264. * When JavaScript is disabled, the errors container is shown, so
  265. * that "This widget requires JavaScript" message can be seen.
  266. *
  267. * When JS is enabled, the container is hidden at first, and then
  268. * revealed during the template rendering, if there actually are
  269. * errors to show.
  270. *
  271. * The display indicator switches from `hide-if-js` to `aria-hidden`
  272. * here in order to maintain consistency with all the other fields
  273. * that key off of `aria-hidden` to determine their visibility.
  274. * `aria-hidden` can't be used initially, because there would be no
  275. * way to set it to false when JavaScript is disabled, which would
  276. * prevent people from seeing the "This widget requires JavaScript"
  277. * message.
  278. */
  279. $( '.community-events-errors' )
  280. .attr( 'aria-hidden', 'true' )
  281. .removeClass( 'hide-if-js' );
  282. $container.on( 'click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm );
  283. /**
  284. * Filters events based on entered location.
  285. *
  286. * @return {void}
  287. */
  288. $container.on( 'submit', '.community-events-form', function( event ) {
  289. var location = $( '#community-events-location' ).val().trim();
  290. event.preventDefault();
  291. /*
  292. * Don't trigger a search if the search field is empty or the
  293. * search term was made of only spaces before being trimmed.
  294. */
  295. if ( ! location ) {
  296. return;
  297. }
  298. app.getEvents({
  299. location: location
  300. });
  301. });
  302. if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) {
  303. app.renderEventsTemplate( communityEventsData.cache, 'app' );
  304. } else {
  305. app.getEvents();
  306. }
  307. app.initialized = true;
  308. },
  309. /**
  310. * Toggles the visibility of the Edit Location form.
  311. *
  312. * @since 4.8.0
  313. *
  314. * @param {event|string} action 'show' or 'hide' to specify a state;
  315. * or an event object to flip between states.
  316. *
  317. * @return {void}
  318. */
  319. toggleLocationForm: function( action ) {
  320. var $toggleButton = $( '.community-events-toggle-location' ),
  321. $cancelButton = $( '.community-events-cancel' ),
  322. $form = $( '.community-events-form' ),
  323. $target = $();
  324. if ( 'object' === typeof action ) {
  325. // The action is the event object: get the clicked element.
  326. $target = $( action.target );
  327. /*
  328. * Strict comparison doesn't work in this case because sometimes
  329. * we explicitly pass a string as value of aria-expanded and
  330. * sometimes a boolean as the result of an evaluation.
  331. */
  332. action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show';
  333. }
  334. if ( 'hide' === action ) {
  335. $toggleButton.attr( 'aria-expanded', 'false' );
  336. $cancelButton.attr( 'aria-expanded', 'false' );
  337. $form.attr( 'aria-hidden', 'true' );
  338. /*
  339. * If the Cancel button has been clicked, bring the focus back
  340. * to the toggle button so users relying on screen readers don't
  341. * lose their place.
  342. */
  343. if ( $target.hasClass( 'community-events-cancel' ) ) {
  344. $toggleButton.trigger( 'focus' );
  345. }
  346. } else {
  347. $toggleButton.attr( 'aria-expanded', 'true' );
  348. $cancelButton.attr( 'aria-expanded', 'true' );
  349. $form.attr( 'aria-hidden', 'false' );
  350. }
  351. },
  352. /**
  353. * Sends REST API requests to fetch events for the widget.
  354. *
  355. * @since 4.8.0
  356. *
  357. * @param {Object} requestParams REST API Request parameters object.
  358. *
  359. * @return {void}
  360. */
  361. getEvents: function( requestParams ) {
  362. var initiatedBy,
  363. app = this,
  364. $spinner = $( '.community-events-form' ).children( '.spinner' );
  365. requestParams = requestParams || {};
  366. requestParams._wpnonce = communityEventsData.nonce;
  367. requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
  368. initiatedBy = requestParams.location ? 'user' : 'app';
  369. $spinner.addClass( 'is-active' );
  370. wp.ajax.post( 'get-community-events', requestParams )
  371. .always( function() {
  372. $spinner.removeClass( 'is-active' );
  373. })
  374. .done( function( response ) {
  375. if ( 'no_location_available' === response.error ) {
  376. if ( requestParams.location ) {
  377. response.unknownCity = requestParams.location;
  378. } else {
  379. /*
  380. * No location was passed, which means that this was an automatic query
  381. * based on IP, locale, and timezone. Since the user didn't initiate it,
  382. * it should fail silently. Otherwise, the error could confuse and/or
  383. * annoy them.
  384. */
  385. delete response.error;
  386. }
  387. }
  388. app.renderEventsTemplate( response, initiatedBy );
  389. })
  390. .fail( function() {
  391. app.renderEventsTemplate({
  392. 'location' : false,
  393. 'events' : [],
  394. 'error' : true
  395. }, initiatedBy );
  396. });
  397. },
  398. /**
  399. * Renders the template for the Events section of the Events & News widget.
  400. *
  401. * @since 4.8.0
  402. *
  403. * @param {Object} templateParams The various parameters that will get passed to wp.template.
  404. * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user;
  405. * 'app' to indicate it was triggered automatically by the app itself.
  406. *
  407. * @return {void}
  408. */
  409. renderEventsTemplate: function( templateParams, initiatedBy ) {
  410. var template,
  411. elementVisibility,
  412. $toggleButton = $( '.community-events-toggle-location' ),
  413. $locationMessage = $( '#community-events-location-message' ),
  414. $results = $( '.community-events-results' );
  415. templateParams.events = app.populateDynamicEventFields(
  416. templateParams.events,
  417. communityEventsData.time_format
  418. );
  419. /*
  420. * Hide all toggleable elements by default, to keep the logic simple.
  421. * Otherwise, each block below would have to turn hide everything that
  422. * could have been shown at an earlier point.
  423. *
  424. * The exception to that is that the .community-events container is hidden
  425. * when the page is first loaded, because the content isn't ready yet,
  426. * but once we've reached this point, it should always be shown.
  427. */
  428. elementVisibility = {
  429. '.community-events' : true,
  430. '.community-events-loading' : false,
  431. '.community-events-errors' : false,
  432. '.community-events-error-occurred' : false,
  433. '.community-events-could-not-locate' : false,
  434. '#community-events-location-message' : false,
  435. '.community-events-toggle-location' : false,
  436. '.community-events-results' : false
  437. };
  438. /*
  439. * Determine which templates should be rendered and which elements
  440. * should be displayed.
  441. */
  442. if ( templateParams.location.ip ) {
  443. /*
  444. * If the API determined the location by geolocating an IP, it will
  445. * provide events, but not a specific location.
  446. */
  447. $locationMessage.text( __( 'Attend an upcoming event near you.' ) );
  448. if ( templateParams.events.length ) {
  449. template = wp.template( 'community-events-event-list' );
  450. $results.html( template( templateParams ) );
  451. } else {
  452. template = wp.template( 'community-events-no-upcoming-events' );
  453. $results.html( template( templateParams ) );
  454. }
  455. elementVisibility['#community-events-location-message'] = true;
  456. elementVisibility['.community-events-toggle-location'] = true;
  457. elementVisibility['.community-events-results'] = true;
  458. } else if ( templateParams.location.description ) {
  459. template = wp.template( 'community-events-attend-event-near' );
  460. $locationMessage.html( template( templateParams ) );
  461. if ( templateParams.events.length ) {
  462. template = wp.template( 'community-events-event-list' );
  463. $results.html( template( templateParams ) );
  464. } else {
  465. template = wp.template( 'community-events-no-upcoming-events' );
  466. $results.html( template( templateParams ) );
  467. }
  468. if ( 'user' === initiatedBy ) {
  469. wp.a11y.speak(
  470. sprintf(
  471. /* translators: %s: The name of a city. */
  472. __( 'City updated. Listing events near %s.' ),
  473. templateParams.location.description
  474. ),
  475. 'assertive'
  476. );
  477. }
  478. elementVisibility['#community-events-location-message'] = true;
  479. elementVisibility['.community-events-toggle-location'] = true;
  480. elementVisibility['.community-events-results'] = true;
  481. } else if ( templateParams.unknownCity ) {
  482. template = wp.template( 'community-events-could-not-locate' );
  483. $( '.community-events-could-not-locate' ).html( template( templateParams ) );
  484. wp.a11y.speak(
  485. sprintf(
  486. /*
  487. * These specific examples were chosen to highlight the fact that a
  488. * state is not needed, even for cities whose name is not unique.
  489. * It would be too cumbersome to include that in the instructions
  490. * to the user, so it's left as an implication.
  491. */
  492. /*
  493. * translators: %s is the name of the city we couldn't locate.
  494. * Replace the examples with cities related to your locale. Test that
  495. * they match the expected location and have upcoming events before
  496. * including them. If no cities related to your locale have events,
  497. * then use cities related to your locale that would be recognizable
  498. * to most users. Use only the city name itself, without any region
  499. * or country. Use the endonym (native locale name) instead of the
  500. * English name if possible.
  501. */
  502. __( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
  503. templateParams.unknownCity
  504. )
  505. );
  506. elementVisibility['.community-events-errors'] = true;
  507. elementVisibility['.community-events-could-not-locate'] = true;
  508. } else if ( templateParams.error && 'user' === initiatedBy ) {
  509. /*
  510. * Errors messages are only shown for requests that were initiated
  511. * by the user, not for ones that were initiated by the app itself.
  512. * Showing error messages for an event that user isn't aware of
  513. * could be confusing or unnecessarily distracting.
  514. */
  515. wp.a11y.speak( __( 'An error occurred. Please try again.' ) );
  516. elementVisibility['.community-events-errors'] = true;
  517. elementVisibility['.community-events-error-occurred'] = true;
  518. } else {
  519. $locationMessage.text( __( 'Enter your closest city to find nearby events.' ) );
  520. elementVisibility['#community-events-location-message'] = true;
  521. elementVisibility['.community-events-toggle-location'] = true;
  522. }
  523. // Set the visibility of toggleable elements.
  524. _.each( elementVisibility, function( isVisible, element ) {
  525. $( element ).attr( 'aria-hidden', ! isVisible );
  526. });
  527. $toggleButton.attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] );
  528. if ( templateParams.location && ( templateParams.location.ip || templateParams.location.latitude ) ) {
  529. // Hide the form when there's a valid location.
  530. app.toggleLocationForm( 'hide' );
  531. if ( 'user' === initiatedBy ) {
  532. /*
  533. * When the form is programmatically hidden after a user search,
  534. * bring the focus back to the toggle button so users relying
  535. * on screen readers don't lose their place.
  536. */
  537. $toggleButton.trigger( 'focus' );
  538. }
  539. } else {
  540. app.toggleLocationForm( 'show' );
  541. }
  542. },
  543. /**
  544. * Populate event fields that have to be calculated on the fly.
  545. *
  546. * These can't be stored in the database, because they're dependent on
  547. * the user's current time zone, locale, etc.
  548. *
  549. * @since 5.5.2
  550. *
  551. * @param {Array} rawEvents The events that should have dynamic fields added to them.
  552. * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`.
  553. *
  554. * @returns {Array}
  555. */
  556. populateDynamicEventFields: function( rawEvents, timeFormat ) {
  557. // Clone the parameter to avoid mutating it, so that this can remain a pure function.
  558. var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) );
  559. $.each( populatedEvents, function( index, event ) {
  560. var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 );
  561. event.user_formatted_date = app.getFormattedDate(
  562. event.start_unix_timestamp * 1000,
  563. event.end_unix_timestamp * 1000,
  564. timeZone
  565. );
  566. event.user_formatted_time = dateI18n(
  567. timeFormat,
  568. event.start_unix_timestamp * 1000,
  569. timeZone
  570. );
  571. event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 );
  572. } );
  573. return populatedEvents;
  574. },
  575. /**
  576. * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`.
  577. *
  578. * @since 5.5.2
  579. *
  580. * @param startTimestamp
  581. *
  582. * @returns {string|number}
  583. */
  584. getTimeZone: function( startTimestamp ) {
  585. /*
  586. * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This
  587. * doesn't need to take `startTimestamp` into account for that reason.
  588. */
  589. var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  590. /*
  591. * Fall back to an offset for IE11, which declares the property but doesn't assign a value.
  592. */
  593. if ( 'undefined' === typeof timeZone ) {
  594. /*
  595. * It's important to use the _event_ time, not the _current_
  596. * time, so that daylight savings time is accounted for.
  597. */
  598. timeZone = app.getFlippedTimeZoneOffset( startTimestamp );
  599. }
  600. return timeZone;
  601. },
  602. /**
  603. * Get intuitive time zone offset.
  604. *
  605. * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones
  606. * that are _behind_ UTC, and a _negative_ value for ones that are ahead.
  607. *
  608. * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off.
  609. *
  610. * @since 5.5.2
  611. *
  612. * @param {number} startTimestamp
  613. *
  614. * @returns {number}
  615. */
  616. getFlippedTimeZoneOffset: function( startTimestamp ) {
  617. return new Date( startTimestamp ).getTimezoneOffset() * -1;
  618. },
  619. /**
  620. * Get a short time zone name, like `PST`.
  621. *
  622. * @since 5.5.2
  623. *
  624. * @param {number} startTimestamp
  625. *
  626. * @returns {string}
  627. */
  628. getTimeZoneAbbreviation: function( startTimestamp ) {
  629. var timeZoneAbbreviation,
  630. eventDateTime = new Date( startTimestamp );
  631. /*
  632. * Leaving the `locales` argument undefined is important, so that the browser
  633. * displays the abbreviation that's most appropriate for the current locale. For
  634. * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`.
  635. *
  636. * This doesn't need to take `startTimestamp` into account, because a name like
  637. * `America/Chicago` automatically tracks daylight savings.
  638. */
  639. var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' );
  640. if ( 3 === shortTimeStringParts.length ) {
  641. timeZoneAbbreviation = shortTimeStringParts[2];
  642. }
  643. if ( 'undefined' === typeof timeZoneAbbreviation ) {
  644. /*
  645. * It's important to use the _event_ time, not the _current_
  646. * time, so that daylight savings time is accounted for.
  647. */
  648. var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ),
  649. sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+';
  650. // translators: Used as part of a string like `GMT+5` in the Events Widget.
  651. timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 );
  652. }
  653. return timeZoneAbbreviation;
  654. },
  655. /**
  656. * Format a start/end date in the user's local time zone and locale.
  657. *
  658. * @since 5.5.2
  659. *
  660. * @param {int} startDate The Unix timestamp in milliseconds when the the event starts.
  661. * @param {int} endDate The Unix timestamp in milliseconds when the the event ends.
  662. * @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`.
  663. *
  664. * @returns {string}
  665. */
  666. getFormattedDate: function( startDate, endDate, timeZone ) {
  667. var formattedDate;
  668. /*
  669. * The `date_format` option is not used because it's important
  670. * in this context to keep the day of the week in the displayed date,
  671. * so that users can tell at a glance if the event is on a day they
  672. * are available, without having to open the link.
  673. *
  674. * The case of crossing a year boundary is intentionally not handled.
  675. * It's so rare in practice that it's not worth the complexity
  676. * tradeoff. The _ending_ year should be passed to
  677. * `multiple_month_event`, though, just in case.
  678. */
  679. /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */
  680. var singleDayEvent = __( 'l, M j, Y' ),
  681. /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
  682. multipleDayEvent = __( '%1$s %2$d–%3$d, %4$d' ),
  683. /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */
  684. multipleMonthEvent = __( '%1$s %2$d – %3$s %4$d, %5$d' );
  685. // Detect single-day events.
  686. if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) {
  687. formattedDate = dateI18n( singleDayEvent, startDate, timeZone );
  688. // Multiple day events.
  689. } else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) {
  690. formattedDate = sprintf(
  691. multipleDayEvent,
  692. dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
  693. dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
  694. dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
  695. dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
  696. );
  697. // Multi-day events that cross a month boundary.
  698. } else {
  699. formattedDate = sprintf(
  700. multipleMonthEvent,
  701. dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
  702. dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
  703. dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ),
  704. dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
  705. dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
  706. );
  707. }
  708. return formattedDate;
  709. }
  710. };
  711. if ( $( '#dashboard_primary' ).is( ':visible' ) ) {
  712. app.init();
  713. } else {
  714. $( document ).on( 'postbox-toggled', function( event, postbox ) {
  715. var $postbox = $( postbox );
  716. if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) {
  717. app.init();
  718. }
  719. });
  720. }
  721. });
  722. /**
  723. * Removed in 5.6.0, needed for back-compatibility.
  724. *
  725. * @since 4.8.0
  726. * @deprecated 5.6.0
  727. *
  728. * @type {object}
  729. */
  730. window.communityEventsData.l10n = window.communityEventsData.l10n || {
  731. enter_closest_city: '',
  732. error_occurred_please_try_again: '',
  733. attend_event_near_generic: '',
  734. could_not_locate_city: '',
  735. city_updated: ''
  736. };
  737. window.communityEventsData.l10n = window.wp.deprecateL10nObject( 'communityEventsData.l10n', window.communityEventsData.l10n, '5.6.0' );