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.

654 lines
18 KiB

1 year ago
  1. /**
  2. * Contains the postboxes logic, opening and closing postboxes, reordering and saving
  3. * the state and ordering to the database.
  4. *
  5. * @since 2.5.0
  6. * @requires jQuery
  7. * @output wp-admin/js/postbox.js
  8. */
  9. /* global ajaxurl, postboxes */
  10. (function($) {
  11. var $document = $( document ),
  12. __ = wp.i18n.__;
  13. /**
  14. * This object contains all function to handle the behavior of the post boxes. The post boxes are the boxes you see
  15. * around the content on the edit page.
  16. *
  17. * @since 2.7.0
  18. *
  19. * @namespace postboxes
  20. *
  21. * @type {Object}
  22. */
  23. window.postboxes = {
  24. /**
  25. * Handles a click on either the postbox heading or the postbox open/close icon.
  26. *
  27. * Opens or closes the postbox. Expects `this` to equal the clicked element.
  28. * Calls postboxes.pbshow if the postbox has been opened, calls postboxes.pbhide
  29. * if the postbox has been closed.
  30. *
  31. * @since 4.4.0
  32. *
  33. * @memberof postboxes
  34. *
  35. * @fires postboxes#postbox-toggled
  36. *
  37. * @return {void}
  38. */
  39. handle_click : function () {
  40. var $el = $( this ),
  41. p = $el.closest( '.postbox' ),
  42. id = p.attr( 'id' ),
  43. ariaExpandedValue;
  44. if ( 'dashboard_browser_nag' === id ) {
  45. return;
  46. }
  47. p.toggleClass( 'closed' );
  48. ariaExpandedValue = ! p.hasClass( 'closed' );
  49. if ( $el.hasClass( 'handlediv' ) ) {
  50. // The handle button was clicked.
  51. $el.attr( 'aria-expanded', ariaExpandedValue );
  52. } else {
  53. // The handle heading was clicked.
  54. $el.closest( '.postbox' ).find( 'button.handlediv' )
  55. .attr( 'aria-expanded', ariaExpandedValue );
  56. }
  57. if ( postboxes.page !== 'press-this' ) {
  58. postboxes.save_state( postboxes.page );
  59. }
  60. if ( id ) {
  61. if ( !p.hasClass('closed') && typeof postboxes.pbshow === 'function' ) {
  62. postboxes.pbshow( id );
  63. } else if ( p.hasClass('closed') && typeof postboxes.pbhide === 'function' ) {
  64. postboxes.pbhide( id );
  65. }
  66. }
  67. /**
  68. * Fires when a postbox has been opened or closed.
  69. *
  70. * Contains a jQuery object with the relevant postbox element.
  71. *
  72. * @since 4.0.0
  73. * @ignore
  74. *
  75. * @event postboxes#postbox-toggled
  76. * @type {Object}
  77. */
  78. $document.trigger( 'postbox-toggled', p );
  79. },
  80. /**
  81. * Handles clicks on the move up/down buttons.
  82. *
  83. * @since 5.5.0
  84. *
  85. * @return {void}
  86. */
  87. handleOrder: function() {
  88. var button = $( this ),
  89. postbox = button.closest( '.postbox' ),
  90. postboxId = postbox.attr( 'id' ),
  91. postboxesWithinSortables = postbox.closest( '.meta-box-sortables' ).find( '.postbox:visible' ),
  92. postboxesWithinSortablesCount = postboxesWithinSortables.length,
  93. postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ),
  94. firstOrLastPositionMessage;
  95. if ( 'dashboard_browser_nag' === postboxId ) {
  96. return;
  97. }
  98. // If on the first or last position, do nothing and send an audible message to screen reader users.
  99. if ( 'true' === button.attr( 'aria-disabled' ) ) {
  100. firstOrLastPositionMessage = button.hasClass( 'handle-order-higher' ) ?
  101. __( 'The box is on the first position' ) :
  102. __( 'The box is on the last position' );
  103. wp.a11y.speak( firstOrLastPositionMessage );
  104. return;
  105. }
  106. // Move a postbox up.
  107. if ( button.hasClass( 'handle-order-higher' ) ) {
  108. // If the box is first within a sortable area, move it to the previous sortable area.
  109. if ( 0 === postboxWithinSortablesIndex ) {
  110. postboxes.handleOrderBetweenSortables( 'previous', button, postbox );
  111. return;
  112. }
  113. postbox.prevAll( '.postbox:visible' ).eq( 0 ).before( postbox );
  114. button.trigger( 'focus' );
  115. postboxes.updateOrderButtonsProperties();
  116. postboxes.save_order( postboxes.page );
  117. }
  118. // Move a postbox down.
  119. if ( button.hasClass( 'handle-order-lower' ) ) {
  120. // If the box is last within a sortable area, move it to the next sortable area.
  121. if ( postboxWithinSortablesIndex + 1 === postboxesWithinSortablesCount ) {
  122. postboxes.handleOrderBetweenSortables( 'next', button, postbox );
  123. return;
  124. }
  125. postbox.nextAll( '.postbox:visible' ).eq( 0 ).after( postbox );
  126. button.trigger( 'focus' );
  127. postboxes.updateOrderButtonsProperties();
  128. postboxes.save_order( postboxes.page );
  129. }
  130. },
  131. /**
  132. * Moves postboxes between the sortables areas.
  133. *
  134. * @since 5.5.0
  135. *
  136. * @param {string} position The "previous" or "next" sortables area.
  137. * @param {Object} button The jQuery object representing the button that was clicked.
  138. * @param {Object} postbox The jQuery object representing the postbox to be moved.
  139. *
  140. * @return {void}
  141. */
  142. handleOrderBetweenSortables: function( position, button, postbox ) {
  143. var closestSortablesId = button.closest( '.meta-box-sortables' ).attr( 'id' ),
  144. sortablesIds = [],
  145. sortablesIndex,
  146. detachedPostbox;
  147. // Get the list of sortables within the page.
  148. $( '.meta-box-sortables:visible' ).each( function() {
  149. sortablesIds.push( $( this ).attr( 'id' ) );
  150. });
  151. // Return if there's only one visible sortables area, e.g. in the block editor page.
  152. if ( 1 === sortablesIds.length ) {
  153. return;
  154. }
  155. // Find the index of the current sortables area within all the sortable areas.
  156. sortablesIndex = $.inArray( closestSortablesId, sortablesIds );
  157. // Detach the postbox to be moved.
  158. detachedPostbox = postbox.detach();
  159. // Move the detached postbox to its new position.
  160. if ( 'previous' === position ) {
  161. $( detachedPostbox ).appendTo( '#' + sortablesIds[ sortablesIndex - 1 ] );
  162. }
  163. if ( 'next' === position ) {
  164. $( detachedPostbox ).prependTo( '#' + sortablesIds[ sortablesIndex + 1 ] );
  165. }
  166. postboxes._mark_area();
  167. button.focus();
  168. postboxes.updateOrderButtonsProperties();
  169. postboxes.save_order( postboxes.page );
  170. },
  171. /**
  172. * Update the move buttons properties depending on the postbox position.
  173. *
  174. * @since 5.5.0
  175. *
  176. * @return {void}
  177. */
  178. updateOrderButtonsProperties: function() {
  179. var firstSortablesId = $( '.meta-box-sortables:visible:first' ).attr( 'id' ),
  180. lastSortablesId = $( '.meta-box-sortables:visible:last' ).attr( 'id' ),
  181. firstPostbox = $( '.postbox:visible:first' ),
  182. lastPostbox = $( '.postbox:visible:last' ),
  183. firstPostboxId = firstPostbox.attr( 'id' ),
  184. lastPostboxId = lastPostbox.attr( 'id' ),
  185. firstPostboxSortablesId = firstPostbox.closest( '.meta-box-sortables' ).attr( 'id' ),
  186. lastPostboxSortablesId = lastPostbox.closest( '.meta-box-sortables' ).attr( 'id' ),
  187. moveUpButtons = $( '.handle-order-higher' ),
  188. moveDownButtons = $( '.handle-order-lower' );
  189. // Enable all buttons as a reset first.
  190. moveUpButtons
  191. .attr( 'aria-disabled', 'false' )
  192. .removeClass( 'hidden' );
  193. moveDownButtons
  194. .attr( 'aria-disabled', 'false' )
  195. .removeClass( 'hidden' );
  196. // When there's only one "sortables" area (e.g. in the block editor) and only one visible postbox, hide the buttons.
  197. if ( firstSortablesId === lastSortablesId && firstPostboxId === lastPostboxId ) {
  198. moveUpButtons.addClass( 'hidden' );
  199. moveDownButtons.addClass( 'hidden' );
  200. }
  201. // Set an aria-disabled=true attribute on the first visible "move" buttons.
  202. if ( firstSortablesId === firstPostboxSortablesId ) {
  203. $( firstPostbox ).find( '.handle-order-higher' ).attr( 'aria-disabled', 'true' );
  204. }
  205. // Set an aria-disabled=true attribute on the last visible "move" buttons.
  206. if ( lastSortablesId === lastPostboxSortablesId ) {
  207. $( '.postbox:visible .handle-order-lower' ).last().attr( 'aria-disabled', 'true' );
  208. }
  209. },
  210. /**
  211. * Adds event handlers to all postboxes and screen option on the current page.
  212. *
  213. * @since 2.7.0
  214. *
  215. * @memberof postboxes
  216. *
  217. * @param {string} page The page we are currently on.
  218. * @param {Object} [args]
  219. * @param {Function} args.pbshow A callback that is called when a postbox opens.
  220. * @param {Function} args.pbhide A callback that is called when a postbox closes.
  221. * @return {void}
  222. */
  223. add_postbox_toggles : function (page, args) {
  224. var $handles = $( '.postbox .hndle, .postbox .handlediv' ),
  225. $orderButtons = $( '.postbox .handle-order-higher, .postbox .handle-order-lower' );
  226. this.page = page;
  227. this.init( page, args );
  228. $handles.on( 'click.postboxes', this.handle_click );
  229. // Handle the order of the postboxes.
  230. $orderButtons.on( 'click.postboxes', this.handleOrder );
  231. /**
  232. * @since 2.7.0
  233. */
  234. $('.postbox .hndle a').on( 'click', function(e) {
  235. e.stopPropagation();
  236. });
  237. /**
  238. * Hides a postbox.
  239. *
  240. * Event handler for the postbox dismiss button. After clicking the button
  241. * the postbox will be hidden.
  242. *
  243. * As of WordPress 5.5, this is only used for the browser update nag.
  244. *
  245. * @since 3.2.0
  246. *
  247. * @return {void}
  248. */
  249. $( '.postbox a.dismiss' ).on( 'click.postboxes', function( e ) {
  250. var hide_id = $(this).parents('.postbox').attr('id') + '-hide';
  251. e.preventDefault();
  252. $( '#' + hide_id ).prop('checked', false).triggerHandler('click');
  253. });
  254. /**
  255. * Hides the postbox element
  256. *
  257. * Event handler for the screen options checkboxes. When a checkbox is
  258. * clicked this function will hide or show the relevant postboxes.
  259. *
  260. * @since 2.7.0
  261. * @ignore
  262. *
  263. * @fires postboxes#postbox-toggled
  264. *
  265. * @return {void}
  266. */
  267. $('.hide-postbox-tog').on('click.postboxes', function() {
  268. var $el = $(this),
  269. boxId = $el.val(),
  270. $postbox = $( '#' + boxId );
  271. if ( $el.prop( 'checked' ) ) {
  272. $postbox.show();
  273. if ( typeof postboxes.pbshow === 'function' ) {
  274. postboxes.pbshow( boxId );
  275. }
  276. } else {
  277. $postbox.hide();
  278. if ( typeof postboxes.pbhide === 'function' ) {
  279. postboxes.pbhide( boxId );
  280. }
  281. }
  282. postboxes.save_state( page );
  283. postboxes._mark_area();
  284. /**
  285. * @since 4.0.0
  286. * @see postboxes.handle_click
  287. */
  288. $document.trigger( 'postbox-toggled', $postbox );
  289. });
  290. /**
  291. * Changes the amount of columns based on the layout preferences.
  292. *
  293. * @since 2.8.0
  294. *
  295. * @return {void}
  296. */
  297. $('.columns-prefs input[type="radio"]').on('click.postboxes', function(){
  298. var n = parseInt($(this).val(), 10);
  299. if ( n ) {
  300. postboxes._pb_edit(n);
  301. postboxes.save_order( page );
  302. }
  303. });
  304. },
  305. /**
  306. * Initializes all the postboxes, mainly their sortable behavior.
  307. *
  308. * @since 2.7.0
  309. *
  310. * @memberof postboxes
  311. *
  312. * @param {string} page The page we are currently on.
  313. * @param {Object} [args={}] The arguments for the postbox initializer.
  314. * @param {Function} args.pbshow A callback that is called when a postbox opens.
  315. * @param {Function} args.pbhide A callback that is called when a postbox
  316. * closes.
  317. *
  318. * @return {void}
  319. */
  320. init : function(page, args) {
  321. var isMobile = $( document.body ).hasClass( 'mobile' ),
  322. $handleButtons = $( '.postbox .handlediv' );
  323. $.extend( this, args || {} );
  324. $('.meta-box-sortables').sortable({
  325. placeholder: 'sortable-placeholder',
  326. connectWith: '.meta-box-sortables',
  327. items: '.postbox',
  328. handle: '.hndle',
  329. cursor: 'move',
  330. delay: ( isMobile ? 200 : 0 ),
  331. distance: 2,
  332. tolerance: 'pointer',
  333. forcePlaceholderSize: true,
  334. helper: function( event, element ) {
  335. /* `helper: 'clone'` is equivalent to `return element.clone();`
  336. * Cloning a checked radio and then inserting that clone next to the original
  337. * radio unchecks the original radio (since only one of the two can be checked).
  338. * We get around this by renaming the helper's inputs' name attributes so that,
  339. * when the helper is inserted into the DOM for the sortable, no radios are
  340. * duplicated, and no original radio gets unchecked.
  341. */
  342. return element.clone()
  343. .find( ':input' )
  344. .attr( 'name', function( i, currentName ) {
  345. return 'sort_' + parseInt( Math.random() * 100000, 10 ).toString() + '_' + currentName;
  346. } )
  347. .end();
  348. },
  349. opacity: 0.65,
  350. start: function() {
  351. $( 'body' ).addClass( 'is-dragging-metaboxes' );
  352. // Refresh the cached positions of all the sortable items so that the min-height set while dragging works.
  353. $( '.meta-box-sortables' ).sortable( 'refreshPositions' );
  354. },
  355. stop: function() {
  356. var $el = $( this );
  357. $( 'body' ).removeClass( 'is-dragging-metaboxes' );
  358. if ( $el.find( '#dashboard_browser_nag' ).is( ':visible' ) && 'dashboard_browser_nag' != this.firstChild.id ) {
  359. $el.sortable('cancel');
  360. return;
  361. }
  362. postboxes.updateOrderButtonsProperties();
  363. postboxes.save_order(page);
  364. },
  365. receive: function(e,ui) {
  366. if ( 'dashboard_browser_nag' == ui.item[0].id )
  367. $(ui.sender).sortable('cancel');
  368. postboxes._mark_area();
  369. $document.trigger( 'postbox-moved', ui.item );
  370. }
  371. });
  372. if ( isMobile ) {
  373. $(document.body).on('orientationchange.postboxes', function(){ postboxes._pb_change(); });
  374. this._pb_change();
  375. }
  376. this._mark_area();
  377. // Update the "move" buttons properties.
  378. this.updateOrderButtonsProperties();
  379. $document.on( 'postbox-toggled', this.updateOrderButtonsProperties );
  380. // Set the handle buttons `aria-expanded` attribute initial value on page load.
  381. $handleButtons.each( function () {
  382. var $el = $( this );
  383. $el.attr( 'aria-expanded', ! $el.closest( '.postbox' ).hasClass( 'closed' ) );
  384. });
  385. },
  386. /**
  387. * Saves the state of the postboxes to the server.
  388. *
  389. * It sends two lists, one with all the closed postboxes, one with all the
  390. * hidden postboxes.
  391. *
  392. * @since 2.7.0
  393. *
  394. * @memberof postboxes
  395. *
  396. * @param {string} page The page we are currently on.
  397. * @return {void}
  398. */
  399. save_state : function(page) {
  400. var closed, hidden;
  401. // Return on the nav-menus.php screen, see #35112.
  402. if ( 'nav-menus' === page ) {
  403. return;
  404. }
  405. closed = $( '.postbox' ).filter( '.closed' ).map( function() { return this.id; } ).get().join( ',' );
  406. hidden = $( '.postbox' ).filter( ':hidden' ).map( function() { return this.id; } ).get().join( ',' );
  407. $.post(ajaxurl, {
  408. action: 'closed-postboxes',
  409. closed: closed,
  410. hidden: hidden,
  411. closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(),
  412. page: page
  413. });
  414. },
  415. /**
  416. * Saves the order of the postboxes to the server.
  417. *
  418. * Sends a list of all postboxes inside a sortable area to the server.
  419. *
  420. * @since 2.8.0
  421. *
  422. * @memberof postboxes
  423. *
  424. * @param {string} page The page we are currently on.
  425. * @return {void}
  426. */
  427. save_order : function(page) {
  428. var postVars, page_columns = $('.columns-prefs input:checked').val() || 0;
  429. postVars = {
  430. action: 'meta-box-order',
  431. _ajax_nonce: $('#meta-box-order-nonce').val(),
  432. page_columns: page_columns,
  433. page: page
  434. };
  435. $('.meta-box-sortables').each( function() {
  436. postVars[ 'order[' + this.id.split( '-' )[0] + ']' ] = $( this ).sortable( 'toArray' ).join( ',' );
  437. } );
  438. $.post(
  439. ajaxurl,
  440. postVars,
  441. function( response ) {
  442. if ( response.success ) {
  443. wp.a11y.speak( __( 'The boxes order has been saved.' ) );
  444. }
  445. }
  446. );
  447. },
  448. /**
  449. * Marks empty postbox areas.
  450. *
  451. * Adds a message to empty sortable areas on the dashboard page. Also adds a
  452. * border around the side area on the post edit screen if there are no postboxes
  453. * present.
  454. *
  455. * @since 3.3.0
  456. * @access private
  457. *
  458. * @memberof postboxes
  459. *
  460. * @return {void}
  461. */
  462. _mark_area : function() {
  463. var visible = $( 'div.postbox:visible' ).length,
  464. visibleSortables = $( '#dashboard-widgets .meta-box-sortables:visible, #post-body .meta-box-sortables:visible' ),
  465. areAllVisibleSortablesEmpty = true;
  466. visibleSortables.each( function() {
  467. var t = $(this);
  468. if ( visible == 1 || t.children( '.postbox:visible' ).length ) {
  469. t.removeClass('empty-container');
  470. areAllVisibleSortablesEmpty = false;
  471. }
  472. else {
  473. t.addClass('empty-container');
  474. }
  475. });
  476. postboxes.updateEmptySortablesText( visibleSortables, areAllVisibleSortablesEmpty );
  477. },
  478. /**
  479. * Updates the text for the empty sortable areas on the Dashboard.
  480. *
  481. * @since 5.5.0
  482. *
  483. * @param {Object} visibleSortables The jQuery object representing the visible sortable areas.
  484. * @param {boolean} areAllVisibleSortablesEmpty Whether all the visible sortable areas are "empty".
  485. *
  486. * @return {void}
  487. */
  488. updateEmptySortablesText: function( visibleSortables, areAllVisibleSortablesEmpty ) {
  489. var isDashboard = $( '#dashboard-widgets' ).length,
  490. emptySortableText = areAllVisibleSortablesEmpty ? __( 'Add boxes from the Screen Options menu' ) : __( 'Drag boxes here' );
  491. if ( ! isDashboard ) {
  492. return;
  493. }
  494. visibleSortables.each( function() {
  495. if ( $( this ).hasClass( 'empty-container' ) ) {
  496. $( this ).attr( 'data-emptyString', emptySortableText );
  497. }
  498. } );
  499. },
  500. /**
  501. * Changes the amount of columns on the post edit page.
  502. *
  503. * @since 3.3.0
  504. * @access private
  505. *
  506. * @memberof postboxes
  507. *
  508. * @fires postboxes#postboxes-columnchange
  509. *
  510. * @param {number} n The amount of columns to divide the post edit page in.
  511. * @return {void}
  512. */
  513. _pb_edit : function(n) {
  514. var el = $('.metabox-holder').get(0);
  515. if ( el ) {
  516. el.className = el.className.replace(/columns-\d+/, 'columns-' + n);
  517. }
  518. /**
  519. * Fires when the amount of columns on the post edit page has been changed.
  520. *
  521. * @since 4.0.0
  522. * @ignore
  523. *
  524. * @event postboxes#postboxes-columnchange
  525. */
  526. $( document ).trigger( 'postboxes-columnchange' );
  527. },
  528. /**
  529. * Changes the amount of columns the postboxes are in based on the current
  530. * orientation of the browser.
  531. *
  532. * @since 3.3.0
  533. * @access private
  534. *
  535. * @memberof postboxes
  536. *
  537. * @return {void}
  538. */
  539. _pb_change : function() {
  540. var check = $( 'label.columns-prefs-1 input[type="radio"]' );
  541. switch ( window.orientation ) {
  542. case 90:
  543. case -90:
  544. if ( !check.length || !check.is(':checked') )
  545. this._pb_edit(2);
  546. break;
  547. case 0:
  548. case 180:
  549. if ( $( '#poststuff' ).length ) {
  550. this._pb_edit(1);
  551. } else {
  552. if ( !check.length || !check.is(':checked') )
  553. this._pb_edit(2);
  554. }
  555. break;
  556. }
  557. },
  558. /* Callbacks */
  559. /**
  560. * @since 2.7.0
  561. * @access public
  562. *
  563. * @property {Function|boolean} pbshow A callback that is called when a postbox
  564. * is opened.
  565. * @memberof postboxes
  566. */
  567. pbshow : false,
  568. /**
  569. * @since 2.7.0
  570. * @access public
  571. * @property {Function|boolean} pbhide A callback that is called when a postbox
  572. * is closed.
  573. * @memberof postboxes
  574. */
  575. pbhide : false
  576. };
  577. }(jQuery));