diff --git a/Gruntfile.js b/Gruntfile.js index 61f18481e23a8..1b05ec073ce4f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1403,6 +1403,7 @@ module.exports = function(grunt) { 'src/wp-includes/js/media/views/iframe.js' : 'src/js/media/views/iframe.js', 'src/wp-includes/js/media/views/image-details.js' : 'src/js/media/views/image-details.js', 'src/wp-includes/js/media/views/label.js' : 'src/js/media/views/label.js', + 'src/wp-includes/js/media/views/library-settings.js' : 'src/js/media/views/library-settings.js', 'src/wp-includes/js/media/views/media-details.js' : 'src/js/media/views/media-details.js', 'src/wp-includes/js/media/views/media-frame.js' : 'src/js/media/views/media-frame.js', 'src/wp-includes/js/media/views/menu-item.js' : 'src/js/media/views/menu-item.js', diff --git a/src/js/_enqueues/wp/media/views.js b/src/js/_enqueues/wp/media/views.js index d87046fcf0e2b..0cd31f73aca8f 100644 --- a/src/js/_enqueues/wp/media/views.js +++ b/src/js/_enqueues/wp/media/views.js @@ -128,6 +128,7 @@ media.view.AttachmentFilters = require( '../../../media/views/attachment-filters media.view.DateFilter = require( '../../../media/views/attachment-filters/date.js' ); media.view.AttachmentFilters.Uploaded = require( '../../../media/views/attachment-filters/uploaded.js' ); media.view.AttachmentFilters.All = require( '../../../media/views/attachment-filters/all.js' ); +media.view.LibrarySettings = require( '../../../media/views/library-settings.js' ); media.view.AttachmentsBrowser = require( '../../../media/views/attachments/browser.js' ); media.view.Selection = require( '../../../media/views/selection.js' ); media.view.Attachment.Selection = require( '../../../media/views/attachment/selection.js' ); diff --git a/src/js/media/views/attachments.js b/src/js/media/views/attachments.js index b2e91624cb159..aad055ba9b64e 100644 --- a/src/js/media/views/attachments.js +++ b/src/js/media/views/attachments.js @@ -1,7 +1,6 @@ var View = wp.media.View, $ = jQuery, - Attachments, - infiniteScrolling = wp.media.view.settings.infiniteScrolling; + Attachments; Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ tagName: 'ul', @@ -54,7 +53,7 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ * calculating the total number of columns. */ _.defaults( this.options, { - infiniteScrolling: infiniteScrolling || false, + infiniteScrolling: !! wp.media.view.settings.infiniteScrolling, refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, refreshThreshold: 3, AttachmentView: wp.media.view.Attachment, @@ -90,11 +89,12 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ this.controller.on( 'library:selection:add', this.attachmentFocus, this ); - if ( this.options.infiniteScrolling ) { - // Throttle the scroll handler and bind this. - this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); + // Throttle the scroll handler and bind this. Done even when infinite + // scrolling is off, since it can be turned on at any time. + this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); + this.options.scrollElement = this.options.scrollElement || this.el; - this.options.scrollElement = this.options.scrollElement || this.el; + if ( this.options.infiniteScrolling ) { $( this.options.scrollElement ).on( 'scroll', this.scroll ); } @@ -114,6 +114,28 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ } }, + /** + * Turns infinite scrolling on or off after the view has been created. + * + * @since 7.1.0 + * + * @param {boolean} enabled Whether to load more attachments on scroll. + * + * @return {void} + */ + setInfiniteScrolling: function( enabled ) { + this.options.infiniteScrolling = enabled; + + $( this.options.scrollElement ).off( 'scroll', this.scroll ); + + if ( enabled ) { + $( this.options.scrollElement ).on( 'scroll', this.scroll ); + + // The list may already be scrolled past the point where more are loaded. + this.scroll(); + } + }, + /** * Listens to the resizeEvent on the window. * @@ -231,6 +253,8 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ */ dispose: function() { this.collection.props.off( null, null, this ); + $( this.options.scrollElement ).off( 'scroll', this.scroll ); + if ( this.options.resize ) { this.$window.off( this.resizeEvent ); } @@ -440,7 +464,7 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ scrollTop = $(document).scrollTop(); } - if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { + if ( ! this.options.infiniteScrolling || ! $(el).is(':visible') || ! this.collection.hasMore() ) { return; } diff --git a/src/js/media/views/attachments/browser.js b/src/js/media/views/attachments/browser.js index 82b7359eb832a..aa67952ea63e3 100644 --- a/src/js/media/views/attachments/browser.js +++ b/src/js/media/views/attachments/browser.js @@ -3,7 +3,8 @@ var View = wp.media.View, l10n = wp.media.view.l10n, $ = jQuery, AttachmentsBrowser, - infiniteScrolling = wp.media.view.settings.infiniteScrolling, + settings = wp.media.view.settings, + librarySettings = wp.media.view.settings.librarySettings, __ = wp.i18n.__, sprintf = wp.i18n.sprintf; @@ -34,6 +35,8 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro className: 'attachments-browser', initialize: function() { + var infiniteScrolling = !! settings.infiniteScrolling; + _.defaults( this.options, { filters: false, search: true, @@ -45,6 +48,7 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro this.controller.on( 'toggle:upload:attachment', this.toggleUploader, this ); this.controller.on( 'edit:selection', this.editSelection ); + this.controller.on( 'library:infinite-scrolling', this.setInfiniteScrolling, this ); // In the Media Library, the sidebar is used to display errors before the attachments grid. if ( this.options.sidebar && 'errors' === this.options.sidebar ) { @@ -111,6 +115,42 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro this.collection.on( 'attachments:received', this.announceSearchResults, this ); }, + /** + * Switches between infinite scrolling and the Load more button in place. + * + * The Load more view is created on demand and then kept, hidden by the + * `has-load-more` class, so that switching back and forth is cheap. + * + * @since 7.1.0 + * + * @param {boolean} enabled Whether to load more attachments on scroll. + * + * @return {void} + */ + setInfiniteScrolling: function( enabled ) { + if ( enabled === Boolean( this.attachments.options.infiniteScrolling ) ) { + return; + } + + this.$el.toggleClass( 'has-load-more', ! enabled ); + this.attachments.setInfiniteScrolling( enabled ); + + if ( enabled ) { + this.collection.off( 'add remove reset', this.updateLoadMoreView, this ); + this.$el.removeClass( 'more-loaded' ); + this.$el.find( '.found-media' ).removeClass( 'found-media' ); + this.$el.find( '.new-media' ).removeClass( 'new-media' ); + return; + } + + if ( ! this.loadMoreWrapper ) { + this.createLoadMoreView(); + } + + this.collection.on( 'add remove reset', this.updateLoadMoreView, this ); + this.updateLoadMoreView(); + }, + /** * Updates the `wp.a11y.speak()` ARIA live region with a message to communicate * the number of search results to screen reader users. This function is @@ -125,7 +165,7 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro /* translators: Accessibility text. %d: Number of attachments found in a search. */ mediaFoundHasMoreResultsMessage = __( 'Number of media items displayed: %d. Click load more for more results.' ); - if ( infiniteScrolling ) { + if ( this.attachments.options.infiniteScrolling ) { /* translators: Accessibility text. %d: Number of attachments found in a search. */ mediaFoundHasMoreResultsMessage = __( 'Number of media items displayed: %d. Scroll the page for more results.' ); } @@ -393,6 +433,14 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro model: this.collection.props, priority: 60 }).render() ); + + // Only for users allowed to save the preference, see `wp_enqueue_media()`. + if ( librarySettings ) { + this.toolbar.set( 'librarySettings', new wp.media.view.LibrarySettings({ + controller: this.controller, + priority: 70 + }).render() ); + } } if ( this.options.dragInfo ) { diff --git a/src/js/media/views/library-settings.js b/src/js/media/views/library-settings.js new file mode 100644 index 0000000000000..2ceb8fb3da831 --- /dev/null +++ b/src/js/media/views/library-settings.js @@ -0,0 +1,194 @@ +var View = wp.media.View, + settings = wp.media.view.settings, + $ = jQuery, + __ = wp.i18n.__, + LibrarySettings; + +/** + * wp.media.view.LibrarySettings + * + * A toolbar control opening a modal dialog with the personal options for the + * Media Library. + * + * @since 7.1.0 + * + * @memberOf wp.media.view + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +LibrarySettings = View.extend(/** @lends wp.media.view.LibrarySettings.prototype */{ + tagName: 'button', + className: 'button button-compact media-library-settings__toggle', + template: wp.template( 'media-library-settings-toggle' ), + + attributes: { + type: 'button', + 'aria-haspopup': 'dialog' + }, + + events: { + 'click': 'open' + }, + + /** + * Whether a request is in progress. + * + * @type {boolean} + */ + isSaving: false, + + initialize: function() { + // Several media frames can be attached at once, so IDs are per instance. + this.uid = _.uniqueId( 'media-library-settings-' ); + }, + + prepare: function() { + return { + titleId: this.uid + '-title', + infiniteScrollingId: this.uid + '-infinite-scrolling' + }; + }, + + /** + * Removes the dialog along with the view. + * + * @return {wp.media.view.LibrarySettings} Returns itself to allow chaining. + */ + dispose: function() { + if ( this.dialog ) { + $( this.dialog ).remove(); + } + + return View.prototype.dispose.apply( this, arguments ); + }, + + /** + * Inserts the dialog into the frame. + * + * @return {void} + */ + createDialog: function() { + var $dialog = $( wp.template( 'media-library-settings-dialog' )( this.prepare() ) ); + + this.controller.$el.append( $dialog ); + + this.dialog = $dialog[0]; + this.status = $dialog.find( '.media-library-settings__status' )[0]; + this.checkbox = $dialog.find( '.media-library-settings__checkbox' )[0]; + + $dialog.on( 'change', '.media-library-settings__checkbox', _.bind( this.updateInfiniteScrolling, this ) ); + + /* + * In the media modal, `wp.media.view.Modal` closes on Escape and + * `wp.media.view.FocusManager` constrains Tab. Neither must run while the + * dialog is open: it handles both itself, and the rest of the modal is inert. + */ + $dialog.on( 'keydown', function( event ) { + event.stopPropagation(); + } ); + }, + + /** + * Opens the dialog. + * + * @return {void} + */ + open: function() { + if ( ! this.dialog ) { + this.createDialog(); + } + + if ( ! this.isSaving ) { + this.checkbox.checked = !! settings.librarySettings.infiniteScrolling; + this.setStatus( '' ); + } + + this.dialog.showModal(); + }, + + /** + * Whether a `media_library_infinite_scrolling` filter callback overrides the + * personal option. + * + * @type {boolean} + */ + isFiltered: !! settings.librarySettings && !! settings.librarySettings.isFiltered, + + /** + * Saves the "Infinite scrolling" personal option for the current user. + * + * @return {void} + */ + updateInfiniteScrolling: function() { + if ( ! this.isSaving ) { + this.save(); + } + }, + + /** + * Sends the state of the checkbox to the server, then whatever it was toggled + * to in the meantime. + * + * @return {void} + */ + save: function() { + var view = this, + enabled = this.checkbox.checked; + + this.isSaving = true; + + this.setStatus( __( 'Saving…' ) ); + + wp.ajax.post( 'set-media-library-settings', { + _ajax_nonce: settings.librarySettings.nonce, + infinite_scrolling: enabled ? 'true' : 'false' + } ).done( function() { + view.isSaving = false; + + settings.librarySettings.infiniteScrolling = enabled ? 1 : 0; + + if ( enabled !== view.checkbox.checked ) { + view.save(); + return; + } + + /* + * A filter callback takes precedence over the preference, so the Media + * Library keeps the filtered behavior. Otherwise the browser this toggle + * belongs to is updated without a reload. + */ + if ( ! view.isFiltered ) { + settings.infiniteScrolling = enabled ? 1 : 0; + + view.controller.trigger( 'library:infinite-scrolling', enabled ); + } + + view.setStatus( enabled ? + __( 'Infinite scrolling is on.' ) : + __( 'Infinite scrolling is off.' ) + ); + } ).fail( function( response ) { + view.isSaving = false; + + // Put the checkbox back in sync with the stored value. + view.checkbox.checked = !! settings.librarySettings.infiniteScrolling; + + view.setStatus( ( response && response.message ) || __( 'The setting could not be saved.' ) ); + } ); + }, + + /** + * Updates the message below the controls. + * + * @param {string} message The message to display. An empty string clears it. + * @return {void} + */ + setStatus: function( message ) { + this.status.textContent = message; + } +}); + +module.exports = LibrarySettings; diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 3ad60f95766e3..73190a24f92fd 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -80,6 +80,7 @@ 'closed-postboxes', 'hidden-columns', 'update-welcome-panel', + 'set-media-library-settings', 'menu-get-metabox', 'wp-link-ajax', 'menu-locations-save', diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 980702d31fb80..ac3fa49a7ad90 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1121,17 +1121,14 @@ th.action-links { .wp-filter .search-form input[type="search"] { width: 280px; max-width: 100%; + min-height: 32px; + padding: 0 8px; } .wp-filter .search-form select { margin: 0; } -.wp-filter .search-form input[type="search"] { - min-height: 32px; - padding: 0 8px; -} - .wp-filter .search-form select, .wp-filter .filter-items select { min-height: 32px; diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index c51751940a976..2895af02e1305 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -1891,6 +1891,29 @@ function wp_ajax_update_welcome_panel() { wp_die( 1 ); } +/** + * Handles saving the Media Library settings for the current user via AJAX. + * + * @since 7.1.0 + */ +function wp_ajax_set_media_library_settings() { + check_ajax_referer( 'media-library-settings' ); + + if ( ! current_user_can( 'upload_files' ) ) { + wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to edit this setting.' ) ) ); + } + + if ( ! isset( $_POST['infinite_scrolling'] ) ) { + wp_send_json_error( array( 'message' => __( 'The setting could not be saved.' ) ) ); + } + + $infinite_scrolling = 'true' === wp_unslash( $_POST['infinite_scrolling'] ) ? 'true' : 'false'; + + update_user_meta( get_current_user_id(), 'infinite_scrolling', $infinite_scrolling ); + + wp_send_json_success( array( 'infiniteScrolling' => 'true' === $infinite_scrolling ) ); +} + /** * Handles for retrieving menu meta boxes via AJAX. * diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index 6748a50f00c57..f8bacb2a86d3a 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -984,6 +984,94 @@ select#media-attachment-filters ~ select#media-attachment-date-filters { display: block; } +/** + * Media Library settings + */ +.wp-core-ui .button.media-library-settings__toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + padding: 0; + position: absolute; + right: 0; +} + +.wp-core-ui .button.media-library-settings__toggle .dashicons { + line-height: 1; +} + +.media-library-settings__dialog { + width: 320px; + max-width: calc(100vw - 32px); + padding: 0; + border: 1px solid #c3c4c7; +} + +.media-library-settings__dialog::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +.media-library-settings__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 8px 8px 16px; + border-bottom: 1px solid #dcdcde; +} + +.media-library-settings__header h2 { + margin: 0; +} + +.media-library-settings__body { + padding: 16px; +} + +.media-library-settings__field { + margin: 0; +} + +.media-library-settings__body .description { + margin: 4px 0 0; + padding-left: 1.5rem; +} + +.media-library-settings__status { + margin: 8px 0 0; + min-height: 2.4em; + color: #3c434a; +} + +.wp-core-ui .media-library-settings__close.button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + border: none; +} + +.wp-core-ui .media-library-settings__close.button .dashicons { + line-height: 1; +} + +@media screen and (max-width: 900px) { + .wp-core-ui .button.media-library-settings__toggle { + min-width: 40px; + min-height: 40px; + width: 40px; + } +} + +@media screen and (max-width: 782px) { + .wp-core-ui .button.media-library-settings__toggle { + position: static; + float: right; + margin-top: 1.125rem; + } +} + /** * Attachments */ @@ -1293,6 +1381,7 @@ select#media-attachment-filters ~ select#media-attachment-date-filters { .attachments-browser .media-toolbar-primary { max-width: calc( 33% - 20px ); + padding-right: 40px; } .mode-grid .attachments-browser .media-toolbar-primary { @@ -1363,6 +1452,11 @@ select#media-attachment-filters ~ select#media-attachment-date-filters { background: #dcdcde; } +/* Kept in the DOM when infinite scrolling is turned back on, so it can be reused. */ +.attachments-browser:not(.has-load-more) .load-more-wrapper { + display: none; +} + .load-more-wrapper { clear: both; display: flex; @@ -2885,6 +2979,7 @@ select#media-attachment-filters ~ select#media-attachment-date-filters { .mode-grid .attachments-browser .media-toolbar-primary { display: grid; grid-template-columns: auto 1fr; + padding-right: 0; } .mode-grid .attachments-browser .media-toolbar-primary input[type="search"] { @@ -2919,6 +3014,10 @@ select#media-attachment-filters ~ select#media-attachment-date-filters { bottom: 0; opacity: 0.6; } + + .media-library-settings__body .description { + padding-left: 2.125rem; + } } /* Responsive on portrait and landscape */ diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 460cf3b3020e5..ff39abf046e56 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -344,6 +344,50 @@ function wp_print_media_templates() { + + + + + +