dashboard.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  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. /**
  8. * Initializes the dashboard widget functionality.
  9. *
  10. * @since 2.7.0
  11. */
  12. jQuery(document).ready( function($) {
  13. var welcomePanel = $( '#welcome-panel' ),
  14. welcomePanelHide = $('#wp_welcome_panel-hide'),
  15. updateWelcomePanel;
  16. /**
  17. * Saves the visibility of the welcome panel.
  18. *
  19. * @since 3.3.0
  20. *
  21. * @param {boolean} visible Should it be visible or not.
  22. *
  23. * @returns {void}
  24. */
  25. updateWelcomePanel = function( visible ) {
  26. $.post( ajaxurl, {
  27. action: 'update-welcome-panel',
  28. visible: visible,
  29. welcomepanelnonce: $( '#welcomepanelnonce' ).val()
  30. });
  31. };
  32. // Unhide the welcome panel if the Welcome Option checkbox is checked.
  33. if ( welcomePanel.hasClass('hidden') && welcomePanelHide.prop('checked') ) {
  34. welcomePanel.removeClass('hidden');
  35. }
  36. // Hide the welcome panel when the dismiss button or close button is clicked.
  37. $('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).click( function(e) {
  38. e.preventDefault();
  39. welcomePanel.addClass('hidden');
  40. updateWelcomePanel( 0 );
  41. $('#wp_welcome_panel-hide').prop('checked', false);
  42. });
  43. // Set welcome panel visibility based on Welcome Option checkbox value.
  44. welcomePanelHide.click( function() {
  45. welcomePanel.toggleClass('hidden', ! this.checked );
  46. updateWelcomePanel( this.checked ? 1 : 0 );
  47. });
  48. /**
  49. * These widgets can be populated via ajax.
  50. *
  51. * @since 2.7.0
  52. *
  53. * @type {string[]}
  54. *
  55. * @global
  56. */
  57. window.ajaxWidgets = ['dashboard_primary'];
  58. /**
  59. * Triggers widget updates via AJAX.
  60. *
  61. * @since 2.7.0
  62. *
  63. * @global
  64. *
  65. * @param {string} el Optional. Widget to fetch or none to update all.
  66. *
  67. * @returns {void}
  68. */
  69. window.ajaxPopulateWidgets = function(el) {
  70. /**
  71. * Fetch the latest representation of the widget via Ajax and show it.
  72. *
  73. * @param {number} i Number of half-seconds to use as the timeout.
  74. * @param {string} id ID of the element which is going to be checked for changes.
  75. *
  76. * @returns {void}
  77. */
  78. function show(i, id) {
  79. var p, e = $('#' + id + ' div.inside:visible').find('.widget-loading');
  80. // If the element is found in the dom, queue to load latest representation.
  81. if ( e.length ) {
  82. p = e.parent();
  83. setTimeout( function(){
  84. // Request the widget content.
  85. p.load( ajaxurl + '?action=dashboard-widgets&widget=' + id + '&pagenow=' + pagenow, '', function() {
  86. // Hide the parent and slide it out for visual fancyness.
  87. p.hide().slideDown('normal', function(){
  88. $(this).css('display', '');
  89. });
  90. });
  91. }, i * 500 );
  92. }
  93. }
  94. // If we have received a specific element to fetch, check if it is valid.
  95. if ( el ) {
  96. el = el.toString();
  97. // If the element is available as AJAX widget, show it.
  98. if ( $.inArray(el, ajaxWidgets) !== -1 ) {
  99. // Show element without any delay.
  100. show(0, el);
  101. }
  102. } else {
  103. // Walk through all ajaxWidgets, loading them after each other.
  104. $.each( ajaxWidgets, show );
  105. }
  106. };
  107. // Initially populate ajax widgets.
  108. ajaxPopulateWidgets();
  109. // Register ajax widgets as postbox toggles.
  110. postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
  111. /**
  112. * Control the Quick Press (Quick Draft) widget.
  113. *
  114. * @since 2.7.0
  115. *
  116. * @global
  117. *
  118. * @returns {void}
  119. */
  120. window.quickPressLoad = function() {
  121. var act = $('#quickpost-action'), t;
  122. // Enable the submit buttons.
  123. $( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false );
  124. t = $('#quick-press').submit( function( e ) {
  125. e.preventDefault();
  126. // Show a spinner.
  127. $('#dashboard_quick_press #publishing-action .spinner').show();
  128. // Disable the submit button to prevent duplicate submissions.
  129. $('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true);
  130. // Post the entered data to save it.
  131. $.post( t.attr( 'action' ), t.serializeArray(), function( data ) {
  132. // Replace the form, and prepend the published post.
  133. $('#dashboard_quick_press .inside').html( data );
  134. $('#quick-press').removeClass('initial-form');
  135. quickPressLoad();
  136. highlightLatestPost();
  137. // Focus the title to allow for quickly drafting another post.
  138. $('#title').focus();
  139. });
  140. /**
  141. * Highlights the latest post for one second.
  142. *
  143. * @returns {void}
  144. */
  145. function highlightLatestPost () {
  146. var latestPost = $('.drafts ul li').first();
  147. latestPost.css('background', '#fffbe5');
  148. setTimeout(function () {
  149. latestPost.css('background', 'none');
  150. }, 1000);
  151. }
  152. } );
  153. // Change the QuickPost action to the publish value.
  154. $('#publish').click( function() { act.val( 'post-quickpress-publish' ); } );
  155. $('#quick-press').on( 'click focusin', function() {
  156. wpActiveEditor = 'content';
  157. });
  158. autoResizeTextarea();
  159. };
  160. window.quickPressLoad();
  161. // Enable the dragging functionality of the widgets.
  162. $( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
  163. /**
  164. * Adjust the height of the textarea based on the content.
  165. *
  166. * @since 3.6.0
  167. *
  168. * @returns {void}
  169. */
  170. function autoResizeTextarea() {
  171. // When IE8 or older is used to render this document, exit.
  172. if ( document.documentMode && document.documentMode < 9 ) {
  173. return;
  174. }
  175. // Add a hidden div. We'll copy over the text from the textarea to measure its height.
  176. $('body').append( '<div class="quick-draft-textarea-clone" style="display: none;"></div>' );
  177. var clone = $('.quick-draft-textarea-clone'),
  178. editor = $('#content'),
  179. editorHeight = editor.height(),
  180. /*
  181. * 100px roughly accounts for browser chrome and allows the
  182. * save draft button to show on-screen at the same time.
  183. */
  184. editorMaxHeight = $(window).height() - 100;
  185. /*
  186. * Match up textarea and clone div as much as possible.
  187. * Padding cannot be reliably retrieved using shorthand in all browsers.
  188. */
  189. clone.css({
  190. 'font-family': editor.css('font-family'),
  191. 'font-size': editor.css('font-size'),
  192. 'line-height': editor.css('line-height'),
  193. 'padding-bottom': editor.css('paddingBottom'),
  194. 'padding-left': editor.css('paddingLeft'),
  195. 'padding-right': editor.css('paddingRight'),
  196. 'padding-top': editor.css('paddingTop'),
  197. 'white-space': 'pre-wrap',
  198. 'word-wrap': 'break-word',
  199. 'display': 'none'
  200. });
  201. // The 'propertychange' is used in IE < 9.
  202. editor.on('focus input propertychange', function() {
  203. var $this = $(this),
  204. // Add a non-breaking space to ensure that the height of a trailing newline is
  205. // included.
  206. textareaContent = $this.val() + '&nbsp;',
  207. // Add 2px to compensate for border-top & border-bottom.
  208. cloneHeight = clone.css('width', $this.css('width')).text(textareaContent).outerHeight() + 2;
  209. // Default to show a vertical scrollbar, if needed.
  210. editor.css('overflow-y', 'auto');
  211. // Only change the height if it has changed and both heights are below the max.
  212. if ( cloneHeight === editorHeight || ( cloneHeight >= editorMaxHeight && editorHeight >= editorMaxHeight ) ) {
  213. return;
  214. }
  215. /*
  216. * Don't allow editor to exceed the height of the window.
  217. * This is also bound in CSS to a max-height of 1300px to be extra safe.
  218. */
  219. if ( cloneHeight > editorMaxHeight ) {
  220. editorHeight = editorMaxHeight;
  221. } else {
  222. editorHeight = cloneHeight;
  223. }
  224. // Disable scrollbars because we adjust the height to the content.
  225. editor.css('overflow', 'hidden');
  226. $this.css('height', editorHeight + 'px');
  227. });
  228. }
  229. } );
  230. jQuery( function( $ ) {
  231. 'use strict';
  232. var communityEventsData = window.communityEventsData || {},
  233. app;
  234. /**
  235. * Global Community Events namespace.
  236. *
  237. * @since 4.8.0
  238. *
  239. * @memberOf wp
  240. * @namespace wp.communityEvents
  241. */
  242. app = window.wp.communityEvents = /** @lends wp.communityEvents */{
  243. initialized: false,
  244. model: null,
  245. /**
  246. * Initializes the wp.communityEvents object.
  247. *
  248. * @since 4.8.0
  249. *
  250. * @returns {void}
  251. */
  252. init: function() {
  253. if ( app.initialized ) {
  254. return;
  255. }
  256. var $container = $( '#community-events' );
  257. /*
  258. * When JavaScript is disabled, the errors container is shown, so
  259. * that "This widget requires JavaScript" message can be seen.
  260. *
  261. * When JS is enabled, the container is hidden at first, and then
  262. * revealed during the template rendering, if there actually are
  263. * errors to show.
  264. *
  265. * The display indicator switches from `hide-if-js` to `aria-hidden`
  266. * here in order to maintain consistency with all the other fields
  267. * that key off of `aria-hidden` to determine their visibility.
  268. * `aria-hidden` can't be used initially, because there would be no
  269. * way to set it to false when JavaScript is disabled, which would
  270. * prevent people from seeing the "This widget requires JavaScript"
  271. * message.
  272. */
  273. $( '.community-events-errors' )
  274. .attr( 'aria-hidden', 'true' )
  275. .removeClass( 'hide-if-js' );
  276. $container.on( 'click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm );
  277. /**
  278. * Filters events based on entered location.
  279. *
  280. * @returns {void}
  281. */
  282. $container.on( 'submit', '.community-events-form', function( event ) {
  283. var location = $.trim( $( '#community-events-location' ).val() );
  284. event.preventDefault();
  285. /*
  286. * Don't trigger a search if the search field is empty or the
  287. * search term was made of only spaces before being trimmed.
  288. */
  289. if ( ! location ) {
  290. return;
  291. }
  292. app.getEvents({
  293. location: location
  294. });
  295. });
  296. if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) {
  297. app.renderEventsTemplate( communityEventsData.cache, 'app' );
  298. } else {
  299. app.getEvents();
  300. }
  301. app.initialized = true;
  302. },
  303. /**
  304. * Toggles the visibility of the Edit Location form.
  305. *
  306. * @since 4.8.0
  307. *
  308. * @param {event|string} action 'show' or 'hide' to specify a state;
  309. * or an event object to flip between states.
  310. *
  311. * @returns {void}
  312. */
  313. toggleLocationForm: function( action ) {
  314. var $toggleButton = $( '.community-events-toggle-location' ),
  315. $cancelButton = $( '.community-events-cancel' ),
  316. $form = $( '.community-events-form' ),
  317. $target = $();
  318. if ( 'object' === typeof action ) {
  319. // The action is the event object: get the clicked element.
  320. $target = $( action.target );
  321. /*
  322. * Strict comparison doesn't work in this case because sometimes
  323. * we explicitly pass a string as value of aria-expanded and
  324. * sometimes a boolean as the result of an evaluation.
  325. */
  326. action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show';
  327. }
  328. if ( 'hide' === action ) {
  329. $toggleButton.attr( 'aria-expanded', 'false' );
  330. $cancelButton.attr( 'aria-expanded', 'false' );
  331. $form.attr( 'aria-hidden', 'true' );
  332. /*
  333. * If the Cancel button has been clicked, bring the focus back
  334. * to the toggle button so users relying on screen readers don't
  335. * lose their place.
  336. */
  337. if ( $target.hasClass( 'community-events-cancel' ) ) {
  338. $toggleButton.focus();
  339. }
  340. } else {
  341. $toggleButton.attr( 'aria-expanded', 'true' );
  342. $cancelButton.attr( 'aria-expanded', 'true' );
  343. $form.attr( 'aria-hidden', 'false' );
  344. }
  345. },
  346. /**
  347. * Sends REST API requests to fetch events for the widget.
  348. *
  349. * @since 4.8.0
  350. *
  351. * @param {Object} requestParams REST API Request parameters object.
  352. *
  353. * @returns {void}
  354. */
  355. getEvents: function( requestParams ) {
  356. var initiatedBy,
  357. app = this,
  358. $spinner = $( '.community-events-form' ).children( '.spinner' );
  359. requestParams = requestParams || {};
  360. requestParams._wpnonce = communityEventsData.nonce;
  361. requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
  362. initiatedBy = requestParams.location ? 'user' : 'app';
  363. $spinner.addClass( 'is-active' );
  364. wp.ajax.post( 'get-community-events', requestParams )
  365. .always( function() {
  366. $spinner.removeClass( 'is-active' );
  367. })
  368. .done( function( response ) {
  369. if ( 'no_location_available' === response.error ) {
  370. if ( requestParams.location ) {
  371. response.unknownCity = requestParams.location;
  372. } else {
  373. /*
  374. * No location was passed, which means that this was an automatic query
  375. * based on IP, locale, and timezone. Since the user didn't initiate it,
  376. * it should fail silently. Otherwise, the error could confuse and/or
  377. * annoy them.
  378. */
  379. delete response.error;
  380. }
  381. }
  382. app.renderEventsTemplate( response, initiatedBy );
  383. })
  384. .fail( function() {
  385. app.renderEventsTemplate({
  386. 'location' : false,
  387. 'error' : true
  388. }, initiatedBy );
  389. });
  390. },
  391. /**
  392. * Renders the template for the Events section of the Events & News widget.
  393. *
  394. * @since 4.8.0
  395. *
  396. * @param {Object} templateParams The various parameters that will get passed to wp.template.
  397. * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user;
  398. * 'app' to indicate it was triggered automatically by the app itself.
  399. *
  400. * @returns {void}
  401. */
  402. renderEventsTemplate: function( templateParams, initiatedBy ) {
  403. var template,
  404. elementVisibility,
  405. l10nPlaceholder = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc.
  406. $toggleButton = $( '.community-events-toggle-location' ),
  407. $locationMessage = $( '#community-events-location-message' ),
  408. $results = $( '.community-events-results' );
  409. /*
  410. * Hide all toggleable elements by default, to keep the logic simple.
  411. * Otherwise, each block below would have to turn hide everything that
  412. * could have been shown at an earlier point.
  413. *
  414. * The exception to that is that the .community-events container is hidden
  415. * when the page is first loaded, because the content isn't ready yet,
  416. * but once we've reached this point, it should always be shown.
  417. */
  418. elementVisibility = {
  419. '.community-events' : true,
  420. '.community-events-loading' : false,
  421. '.community-events-errors' : false,
  422. '.community-events-error-occurred' : false,
  423. '.community-events-could-not-locate' : false,
  424. '#community-events-location-message' : false,
  425. '.community-events-toggle-location' : false,
  426. '.community-events-results' : false
  427. };
  428. /*
  429. * Determine which templates should be rendered and which elements
  430. * should be displayed.
  431. */
  432. if ( templateParams.location.ip ) {
  433. /*
  434. * If the API determined the location by geolocating an IP, it will
  435. * provide events, but not a specific location.
  436. */
  437. $locationMessage.text( communityEventsData.l10n.attend_event_near_generic );
  438. if ( templateParams.events.length ) {
  439. template = wp.template( 'community-events-event-list' );
  440. $results.html( template( templateParams ) );
  441. } else {
  442. template = wp.template( 'community-events-no-upcoming-events' );
  443. $results.html( template( templateParams ) );
  444. }
  445. elementVisibility['#community-events-location-message'] = true;
  446. elementVisibility['.community-events-toggle-location'] = true;
  447. elementVisibility['.community-events-results'] = true;
  448. } else if ( templateParams.location.description ) {
  449. template = wp.template( 'community-events-attend-event-near' );
  450. $locationMessage.html( template( templateParams ) );
  451. if ( templateParams.events.length ) {
  452. template = wp.template( 'community-events-event-list' );
  453. $results.html( template( templateParams ) );
  454. } else {
  455. template = wp.template( 'community-events-no-upcoming-events' );
  456. $results.html( template( templateParams ) );
  457. }
  458. if ( 'user' === initiatedBy ) {
  459. wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location.description ), 'assertive' );
  460. }
  461. elementVisibility['#community-events-location-message'] = true;
  462. elementVisibility['.community-events-toggle-location'] = true;
  463. elementVisibility['.community-events-results'] = true;
  464. } else if ( templateParams.unknownCity ) {
  465. template = wp.template( 'community-events-could-not-locate' );
  466. $( '.community-events-could-not-locate' ).html( template( templateParams ) );
  467. wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) );
  468. elementVisibility['.community-events-errors'] = true;
  469. elementVisibility['.community-events-could-not-locate'] = true;
  470. } else if ( templateParams.error && 'user' === initiatedBy ) {
  471. /*
  472. * Errors messages are only shown for requests that were initiated
  473. * by the user, not for ones that were initiated by the app itself.
  474. * Showing error messages for an event that user isn't aware of
  475. * could be confusing or unnecessarily distracting.
  476. */
  477. wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again );
  478. elementVisibility['.community-events-errors'] = true;
  479. elementVisibility['.community-events-error-occurred'] = true;
  480. } else {
  481. $locationMessage.text( communityEventsData.l10n.enter_closest_city );
  482. elementVisibility['#community-events-location-message'] = true;
  483. elementVisibility['.community-events-toggle-location'] = true;
  484. }
  485. // Set the visibility of toggleable elements.
  486. _.each( elementVisibility, function( isVisible, element ) {
  487. $( element ).attr( 'aria-hidden', ! isVisible );
  488. });
  489. $toggleButton.attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] );
  490. if ( templateParams.location && ( templateParams.location.ip || templateParams.location.latitude ) ) {
  491. // Hide the form when there's a valid location.
  492. app.toggleLocationForm( 'hide' );
  493. if ( 'user' === initiatedBy ) {
  494. /*
  495. * When the form is programmatically hidden after a user search,
  496. * bring the focus back to the toggle button so users relying
  497. * on screen readers don't lose their place.
  498. */
  499. $toggleButton.focus();
  500. }
  501. } else {
  502. app.toggleLocationForm( 'show' );
  503. }
  504. }
  505. };
  506. if ( $( '#dashboard_primary' ).is( ':visible' ) ) {
  507. app.init();
  508. } else {
  509. $( document ).on( 'postbox-toggled', function( event, postbox ) {
  510. var $postbox = $( postbox );
  511. if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) {
  512. app.init();
  513. }
  514. });
  515. }
  516. });