Ticket #50105: 50105.2.diff
File 50105.2.diff, 18.6 KB (added by , 5 years ago) |
---|
-
src/js/media/models/attachments.js
355 355 hasMore: function() { 356 356 return this.mirroring ? this.mirroring.hasMore() : false; 357 357 }, 358 358 359 /** 360 * Holds the total number of attachments. 361 * 362 * @since 5.5.0 363 */ 364 totalAttachments: 0, 365 366 /** 367 * Gets the total number of attachments. 368 * 369 * @since 5.5.0 370 * 371 * @return {number} The total number of attachments. 372 */ 373 getTotalAttachments: function() { 374 return this.mirroring ? this.mirroring.totalAttachments : 0; 375 }, 376 377 /** 359 378 * A custom AJAX-response parser. 360 379 * 361 380 * See trac ticket #24753 362 381 * 363 * @param {Object|Array} resp The raw response Object/Array. 382 * Called automatically by Backbone whenever a collection's models are returned 383 * by the server, in fetch. The default implementation is a no-op, simply 384 * passing through the JSON response. We override this to add attributes to 385 * the collection items. 386 * 387 * Since WordPress 5.5, the response returns the attachments under `response.attachments` 388 * and `response.totalAttachments` holds the total number of attachments found. 389 * 390 * @param {Object|Array} response The raw response Object/Array. 364 391 * @param {Object} xhr 365 392 * @return {Array} The array of model attributes to be added to the collection 366 393 */ 367 parse: function( resp , xhr ) {368 if ( ! _.isArray( resp ) ) {369 resp = [resp];394 parse: function( response, xhr ) { 395 if ( ! _.isArray( response.attachments ) ) { 396 response = [response.attachments]; 370 397 } 371 398 372 return _.map( resp, function( attrs ) { 399 this.totalAttachments = parseInt( response.totalAttachments, 10 ); 400 401 return _.map( response.attachments, function( attrs ) { 373 402 var id, attachment, newAttributes; 374 403 375 404 if ( attrs instanceof Backbone.Model ) { -
src/js/media/models/query.js
112 112 options = options || {}; 113 113 options.remove = false; 114 114 115 return this._more = this.fetch( options ).done( function( resp ) { 116 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 115 return this._more = this.fetch( options ).done( function( response ) { 116 // Since WordPress 5.5, the response returns the attachments under `response.attachments`. 117 var attachments = response.attachments; 118 119 if ( _.isEmpty( attachments ) || -1 === this.args.posts_per_page || attachments.length < this.args.posts_per_page ) { 117 120 query._hasMore = false; 118 121 } 119 122 }); -
src/js/media/views/attachments/browser.js
2 2 mediaTrash = wp.media.view.settings.mediaTrash, 3 3 l10n = wp.media.view.l10n, 4 4 $ = jQuery, 5 AttachmentsBrowser; 5 AttachmentsBrowser, 6 infiniteScrolling = wp.media.view.settings.infiniteScrolling, 7 __ = wp.i18n.__, 8 sprintf = wp.i18n.sprintf; 6 9 7 10 /** 8 11 * wp.media.view.AttachmentsBrowser … … 68 71 this.createUploader(); 69 72 } 70 73 71 72 74 // Add a heading before the attachments list. 73 75 this.createAttachmentsHeading(); 74 76 75 // Create the list of attachments.76 this.createAttachments ();77 // Create the attachments wrapper view. 78 this.createAttachmentsWrapperView(); 77 79 80 if ( ! infiniteScrolling ) { 81 this.$el.addClass( 'has-load-more' ); 82 this.createLoadMoreView(); 83 } 84 78 85 // For accessibility reasons, place the normal sidebar after the attachments, see ticket #36909. 79 86 if ( this.options.sidebar && 'errors' !== this.options.sidebar ) { 80 87 this.createSidebar(); … … 92 99 93 100 this.collection.on( 'add remove reset', this.updateContent, this ); 94 101 102 if ( ! infiniteScrolling ) { 103 this.collection.on( 'add remove reset', this.updateLoadMoreView, this ); 104 } 105 95 106 // The non-cached or cached attachments query has completed. 96 107 this.collection.on( 'attachments:received', this.announceSearchResults, this ); 97 108 }, … … 106 117 * @return {void} 107 118 */ 108 119 announceSearchResults: _.debounce( function() { 109 var count; 120 var count, 121 /* translators: Accessibility text. %d: Number of attachments found in a search. */ 122 mediaFoundHasMoreResultsMessage = __( 'Number of media items displayed: %d. Click load more for more results.' ); 110 123 124 if ( infiniteScrolling ) { 125 /* translators: Accessibility text. %d: Number of attachments found in a search. */ 126 mediaFoundHasMoreResultsMessage = __( 'Number of media items displayed: %d. Scroll the page for more results.' ); 127 } 128 111 129 if ( this.collection.mirroring.args.s ) { 112 130 count = this.collection.length; 113 131 … … 117 135 } 118 136 119 137 if ( this.collection.hasMore() ) { 120 wp.a11y.speak( l10n.mediaFoundHasMoreResults.replace( '%d', count ) );138 wp.a11y.speak( mediaFoundHasMoreResultsMessage.replace( '%d', count ) ); 121 139 return; 122 140 } 123 141 … … 392 410 noItemsView; 393 411 394 412 if ( this.controller.isModeActive( 'grid' ) ) { 413 // Usually the media library. 395 414 noItemsView = view.attachmentsNoResults; 396 415 } else { 416 // Usually the media modal. 397 417 noItemsView = view.uploader; 398 418 } 399 419 … … 433 453 } 434 454 }, 435 455 456 /** 457 * Creates the Attachments wrapper view. 458 * 459 * @since 5.5.0 460 * 461 * @return {void} 462 */ 463 createAttachmentsWrapperView: function() { 464 this.attachmentsWrapper = new wp.media.View( { 465 className: 'attachments-wrapper' 466 } ); 467 468 // Create the list of attachments. 469 this.views.add( this.attachmentsWrapper ); 470 this.createAttachments(); 471 }, 472 436 473 createAttachments: function() { 437 474 this.attachments = new wp.media.view.Attachments({ 438 475 controller: this.controller, … … 451 488 this.controller.on( 'attachment:keydown:arrow', _.bind( this.attachments.arrowEvent, this.attachments ) ); 452 489 this.controller.on( 'attachment:details:shift-tab', _.bind( this.attachments.restoreFocus, this.attachments ) ); 453 490 454 this.views.add( this.attachments );491 this.views.add( '.attachments-wrapper', this.attachments ); 455 492 456 457 493 if ( this.controller.isModeActive( 'grid' ) ) { 458 494 this.attachmentsNoResults = new View({ 459 495 controller: this.controller, … … 467 503 } 468 504 }, 469 505 506 /** 507 * Creates the load more button and attachments counter view. 508 * 509 * @since 5.5.0 510 * 511 * @return {void} 512 */ 513 createLoadMoreView: function() { 514 var view = this; 515 516 this.loadMoreWrapper = new View( { 517 controller: this.controller, 518 className: 'load-more-wrapper' 519 } ); 520 521 this.loadMoreCount = new View( { 522 controller: this.controller, 523 tagName: 'p', 524 className: 'load-more-count hidden' 525 } ); 526 527 this.loadMoreButton = new wp.media.view.Button( { 528 text: __( 'Load more' ), 529 className: 'load-more hidden', 530 style: 'primary', 531 size: 'hero', 532 click: function() { 533 view.loadMoreAttachments(); 534 } 535 } ); 536 537 this.loadMoreSpinner = new wp.media.view.Spinner(); 538 539 this.loadMoreJumpToFirst = new wp.media.view.Button( { 540 text: __( 'Jump to first new item' ), 541 className: 'load-more-jump visually-hidden', 542 size: '', 543 click: function() { 544 view.jumpToFirstAddedItem(); 545 } 546 } ); 547 548 this.views.add( '.attachments-wrapper', this.loadMoreWrapper ); 549 this.views.add( '.load-more-wrapper', this.loadMoreCount ); 550 this.views.add( '.load-more-wrapper', this.loadMoreButton ); 551 this.views.add( '.load-more-wrapper', this.loadMoreSpinner ); 552 this.views.add( '.load-more-wrapper', this.loadMoreJumpToFirst ); 553 }, 554 555 /** 556 * Updates the Load More view. This function is debounced because the 557 * collection updates multiple times at the add, remove, and reset events. 558 * We need it to run only once, after all attachments are added or removed. 559 * 560 * @since 5.5.0 561 * 562 * @return {void} 563 */ 564 updateLoadMoreView: _.debounce( function() { 565 // Ensure the load more view elements are initially hidden at each update. 566 this.loadMoreButton.$el.addClass( 'hidden' ); 567 this.loadMoreCount.$el.addClass( 'hidden' ); 568 this.loadMoreJumpToFirst.$el.addClass( 'visually-hidden' ); 569 570 if ( ! this.collection.getTotalAttachments() ) { 571 return; 572 } 573 574 if ( this.collection.length ) { 575 this.loadMoreCount.$el.text( 576 /* translators: 1: Number of displayed attachments, 2: Number of total attachments. */ 577 sprintf( 578 __( 'Media items %1$s of %2$s' ), 579 this.collection.length, 580 this.collection.getTotalAttachments() 581 ) 582 ); 583 584 this.loadMoreCount.$el.removeClass( 'hidden' ); 585 } 586 587 /* 588 * Notice that while the collection updates multiple times hasMore() may 589 * return true when it's actually not true. 590 */ 591 if ( this.collection.hasMore() ) { 592 this.loadMoreButton.$el.removeClass( 'hidden' ); 593 } 594 595 // Find the media item to move focus to. The jQuery `eq()` index is zero-based. 596 this.firstAddedMediaItem = this.$el.find( '.attachment' ).eq( this.firstAddedMediaItemIndex ); 597 598 // If there's a media item to move focus to, make the "Jump to" button available on focus. 599 if ( this.firstAddedMediaItem.length ) { 600 this.loadMoreJumpToFirst.$el.removeClass( 'visually-hidden' ); 601 } 602 }, 10 ), 603 604 /** 605 * Loads more attachments. 606 * 607 * @since 5.5.0 608 * 609 * @return {void} 610 */ 611 loadMoreAttachments: function() { 612 var view = this; 613 614 if ( ! this.collection.hasMore() ) { 615 return; 616 } 617 618 /* 619 * The collection index is zero-based while the length counts the actual 620 * amount of items. Thus the length is equivaent to the position of the 621 * first added item. 622 */ 623 this.firstAddedMediaItemIndex = this.collection.length; 624 625 view.loadMoreSpinner.show(); 626 627 this.collection.more().done( function() { 628 // Within done(), `this` is the returned collection. 629 view.loadMoreSpinner.hide(); 630 } ); 631 }, 632 633 /** 634 * Moves focus to the first new added item. . 635 * 636 * @since 5.5.0 637 * 638 * @return {void} 639 */ 640 jumpToFirstAddedItem: function() { 641 // Set focus on first added item. 642 this.firstAddedMediaItem.focus(); 643 }, 644 470 645 createAttachmentsHeading: function() { 471 646 this.attachmentsHeading = new wp.media.view.Heading( { 472 647 text: l10n.attachmentsList, -
src/js/media/views/attachments.js
1 1 var View = wp.media.View, 2 2 $ = jQuery, 3 Attachments; 3 Attachments, 4 infiniteScrolling = wp.media.view.settings.infiniteScrolling; 4 5 5 6 Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{ 6 7 tagName: 'ul', … … 35 36 this.el.id = _.uniqueId('__attachments-view-'); 36 37 37 38 /** 39 * @param infiniteScrolling Whether to enable infinite scrolling or use 40 * the default "load more" button. 38 41 * @param refreshSensitivity The time in milliseconds to throttle the scroll 39 42 * handler. 40 43 * @param refreshThreshold The amount of pixels that should be scrolled before … … 49 52 * calculating the total number of columns. 50 53 */ 51 54 _.defaults( this.options, { 55 infiniteScrolling: infiniteScrolling || false, 52 56 refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, 53 57 refreshThreshold: 3, 54 58 AttachmentView: wp.media.view.Attachment, … … 84 88 85 89 this.controller.on( 'library:selection:add', this.attachmentFocus, this ); 86 90 87 // Throttle the scroll handler and bind this. 88 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 91 if ( this.options.infiniteScrolling ) { 92 // Throttle the scroll handler and bind this. 93 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 89 94 90 this.options.scrollElement = this.options.scrollElement || this.el; 91 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 95 this.options.scrollElement = this.options.scrollElement || this.el; 96 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 97 } 92 98 93 99 this.initSortable(); 94 100 … … 387 393 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 388 394 } else { 389 395 this.views.unset(); 390 this.collection.more().done( this.scroll ); 396 if ( this.options.infiniteScrolling ) { 397 this.collection.more().done( this.scroll ); 398 } 391 399 } 392 400 }, 393 401 … … 400 408 * @return {void} 401 409 */ 402 410 ready: function() { 403 this.scroll(); 411 if ( this.options.infiniteScrolling ) { 412 this.scroll(); 413 } 404 414 }, 405 415 406 416 /** -
src/wp-admin/css/media.css
416 416 417 417 .media-frame.mode-grid, 418 418 .media-frame.mode-grid .media-frame-content, 419 .media-frame.mode-grid .attachments-browser .attachments, 419 .media-frame.mode-grid .attachments-browser:not(.has-load-more) .attachments, 420 .media-frame.mode-grid .attachments-browser.has-load-more .attachments-wrapper, 420 421 .media-frame.mode-grid .uploader-inline-content { 421 422 position: static; 422 423 } … … 494 495 border: 4px dashed #b4b9be; 495 496 } 496 497 497 .media-frame.mode-select .attachments-browser.fixed .attachments { 498 .media-frame.mode-select .attachments-browser.fixed:not(.has-load-more) .attachments, 499 .media-frame.mode-select .attachments-browser.has-load-more.fixed .attachments-wrapper { 498 500 position: relative; 499 501 top: 94px; /* prevent jumping up when the toolbar becomes fixed */ 500 502 padding-bottom: 94px; /* offset for above so the bottom doesn't get cut off */ -
src/wp-admin/includes/ajax-actions.php
2969 2969 $posts = array_map( 'wp_prepare_attachment_for_js', $query->posts ); 2970 2970 $posts = array_filter( $posts ); 2971 2971 2972 wp_send_json_success( $posts ); 2972 $result = array( 2973 'attachments' => $posts, 2974 'totalAttachments' => $query->found_posts, 2975 ); 2976 2977 wp_send_json_success( $result ); 2973 2978 } 2974 2979 2975 2980 /** -
src/wp-includes/css/media-views.css
1170 1170 padding: 2px 8px 8px; 1171 1171 } 1172 1172 1173 .attachments-browser .attachments, 1173 .attachments-browser:not(.has-load-more) .attachments, 1174 .attachments-browser.has-load-more .attachments-wrapper, 1174 1175 .attachments-browser .uploader-inline { 1175 1176 position: absolute; 1176 1177 top: 72px; … … 1249 1250 padding: 2em 0 0 2em; 1250 1251 } 1251 1252 1253 .load-more-wrapper { 1254 clear: both; 1255 display: flex; 1256 flex-wrap: wrap; 1257 align-items: center; 1258 justify-content: center; 1259 padding: 1em 0; 1260 } 1261 1262 .load-more-wrapper .load-more-count { 1263 min-width: 100%; 1264 margin: 0 0 1em; 1265 text-align: center; 1266 } 1267 1268 .load-more-wrapper .load-more { 1269 margin: 0; 1270 } 1271 1272 /* Needs high specificity. */ 1273 .media-frame .load-more-wrapper .load-more + .spinner { 1274 float: none; 1275 margin: 0 -30px 0 10px; 1276 } 1277 1278 /* Reset spinner margin when the button is hidden to avoid horizontal scrollbar. */ 1279 .media-frame .load-more-wrapper .load-more.hidden + .spinner { 1280 margin: 0; 1281 } 1282 1283 /* Force a new row within the flex container. */ 1284 .load-more-wrapper::after { 1285 content: ""; 1286 min-width: 100%; 1287 order: 1; 1288 } 1289 1290 .load-more-wrapper .load-more-jump { 1291 /* Force a new row within the flex container. */ 1292 order: 2; 1293 /* Place the button out of screen. */ 1294 position: relative; 1295 left: -1000em; 1296 margin: 1em 0 0; 1297 } 1298 1299 .load-more-wrapper .load-more-jump.visually-hidden { 1300 visibility: hidden; 1301 } 1302 1303 .load-more-wrapper .load-more-jump:focus { 1304 /* Reveal the button on focus. */ 1305 left: auto; 1306 } 1307 1252 1308 /** 1253 1309 * Progress Bar 1254 1310 */ -
src/wp-includes/media.php
3964 3964 ); 3965 3965 } 3966 3966 3967 /** 3968 * Filters whether the Media Library grid has infinite scrolling. Default `false`. 3969 * 3970 * @since 5.5.0 3971 * 3972 * @param bool $value The filtered value, defaults to `false`. 3973 */ 3974 $infinite_scrolling = apply_filters( 'media_library_infinite_scrolling', false ); 3975 3967 3976 $settings = array( 3968 'tabs' => $tabs,3969 'tabUrl' => add_query_arg( array( 'chromeless' => true ), admin_url( 'media-upload.php' ) ),3970 'mimeTypes' => wp_list_pluck( get_post_mime_types(), 0 ),3977 'tabs' => $tabs, 3978 'tabUrl' => add_query_arg( array( 'chromeless' => true ), admin_url( 'media-upload.php' ) ), 3979 'mimeTypes' => wp_list_pluck( get_post_mime_types(), 0 ), 3971 3980 /** This filter is documented in wp-admin/includes/media.php */ 3972 'captions' => ! apply_filters( 'disable_captions', '' ),3973 'nonce' => array(3981 'captions' => ! apply_filters( 'disable_captions', '' ), 3982 'nonce' => array( 3974 3983 'sendToEditor' => wp_create_nonce( 'media-send-to-editor' ), 3975 3984 ), 3976 'post' => array(3985 'post' => array( 3977 3986 'id' => 0, 3978 3987 ), 3979 'defaultProps' => $props,3980 'attachmentCounts' => array(3988 'defaultProps' => $props, 3989 'attachmentCounts' => array( 3981 3990 'audio' => ( $show_audio_playlist ) ? 1 : 0, 3982 3991 'video' => ( $show_video_playlist ) ? 1 : 0, 3983 3992 ), 3984 'oEmbedProxyUrl' => rest_url( 'oembed/1.0/proxy' ), 3985 'embedExts' => $exts, 3986 'embedMimes' => $ext_mimes, 3987 'contentWidth' => $content_width, 3988 'months' => $months, 3989 'mediaTrash' => MEDIA_TRASH ? 1 : 0, 3993 'oEmbedProxyUrl' => rest_url( 'oembed/1.0/proxy' ), 3994 'embedExts' => $exts, 3995 'embedMimes' => $ext_mimes, 3996 'contentWidth' => $content_width, 3997 'months' => $months, 3998 'mediaTrash' => MEDIA_TRASH ? 1 : 0, 3999 'infiniteScrolling' => ( $infinite_scrolling ) ? 1 : 0, 3990 4000 ); 3991 4001 3992 4002 $post = null; … … 4070 4080 'searchLabel' => __( 'Search' ), 4071 4081 'searchMediaLabel' => __( 'Search Media' ), // Backward compatibility pre-5.3. 4072 4082 'searchMediaPlaceholder' => __( 'Search media items...' ), // Placeholder (no ellipsis), backward compatibility pre-5.3. 4083 /* translators: %d: Number of attachments found in a search. */ 4073 4084 'mediaFound' => __( 'Number of media items found: %d' ), 4074 'mediaFoundHasMoreResults' => __( 'Number of media items displayed: %d. Scroll the page for more results.' ),4075 4085 'noMedia' => __( 'No media items found.' ), 4076 4086 'noMediaTryNewSearch' => __( 'No media items found. Try a different search.' ), 4077 4087