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() {
+
+
+
+
+
+