/*globals wp, _, jQuery */ /** * wp.media.view.Attachments * * @class * @augments wp.media.View * @augments wp.Backbone.View * @augments Backbone.View */ var View = wp.media.View, $ = jQuery, Attachments; Attachments = View.extend({ tagName: 'ul', className: 'attachments', attributes: { tabIndex: -1 }, initialize: function() { this.el.id = _.uniqueId('__attachments-view-'); _.defaults( this.options, { refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, refreshThreshold: 3, AttachmentView: wp.media.view.Attachment, sortable: false, resize: true, idealColumnWidth: $( window ).width() < 640 ? 135 : 150 }); this._viewsByCid = {}; this.$window = $( window ); this.resizeEvent = 'resize.media-modal-columns'; this.collection.on( 'add', function( attachment ) { this.views.add( this.createAttachmentView( attachment ), { at: this.collection.indexOf( attachment ) }); }, this ); this.collection.on( 'remove', function( attachment ) { var view = this._viewsByCid[ attachment.cid ]; delete this._viewsByCid[ attachment.cid ]; if ( view ) { view.remove(); } }, this ); this.collection.on( 'reset', this.render, this ); this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); // Throttle the scroll handler and bind this. this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); this.options.scrollElement = this.options.scrollElement || this.el; $( this.options.scrollElement ).on( 'scroll', this.scroll ); this.initSortable(); _.bindAll( this, 'setColumns' ); if ( this.options.resize ) { this.on( 'ready', this.bindEvents ); this.controller.on( 'open', this.setColumns ); // Call this.setColumns() after this view has been rendered in the DOM so // attachments get proper width applied. _.defer( this.setColumns, this ); } }, bindEvents: function() { this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); }, attachmentFocus: function() { this.$( 'li:first' ).focus(); }, restoreFocus: function() { this.$( 'li.selected:first' ).focus(); }, arrowEvent: function( event ) { var attachments = this.$el.children( 'li' ), perRow = this.columns, index = attachments.filter( ':focus' ).index(), row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); if ( index === -1 ) { return; } // Left arrow if ( 37 === event.keyCode ) { if ( 0 === index ) { return; } attachments.eq( index - 1 ).focus(); } // Up arrow if ( 38 === event.keyCode ) { if ( 1 === row ) { return; } attachments.eq( index - perRow ).focus(); } // Right arrow if ( 39 === event.keyCode ) { if ( attachments.length === index ) { return; } attachments.eq( index + 1 ).focus(); } // Down arrow if ( 40 === event.keyCode ) { if ( Math.ceil( attachments.length / perRow ) === row ) { return; } attachments.eq( index + perRow ).focus(); } }, dispose: function() { this.collection.props.off( null, null, this ); if ( this.options.resize ) { this.$window.off( this.resizeEvent ); } /** * call 'dispose' directly on the parent class */ View.prototype.dispose.apply( this, arguments ); }, setColumns: function() { var prev = this.columns, width = this.$el.width(); if ( width ) { this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; if ( ! prev || prev !== this.columns ) { this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); } } }, initSortable: function() { var collection = this.collection; if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { return; } this.$el.sortable( _.extend({ // If the `collection` has a `comparator`, disable sorting. disabled: !! collection.comparator, // Change the position of the attachment as soon as the // mouse pointer overlaps a thumbnail. tolerance: 'pointer', // Record the initial `index` of the dragged model. start: function( event, ui ) { ui.item.data('sortableIndexStart', ui.item.index()); }, // Update the model's index in the collection. // Do so silently, as the view is already accurate. update: function( event, ui ) { var model = collection.at( ui.item.data('sortableIndexStart') ), comparator = collection.comparator; // Temporarily disable the comparator to prevent `add` // from re-sorting. delete collection.comparator; // Silently shift the model to its new index. collection.remove( model, { silent: true }); collection.add( model, { silent: true, at: ui.item.index() }); // Restore the comparator. collection.comparator = comparator; // Fire the `reset` event to ensure other collections sync. collection.trigger( 'reset', collection ); // If the collection is sorted by menu order, // update the menu order. collection.saveMenuOrder(); } }, this.options.sortable ) ); // If the `orderby` property is changed on the `collection`, // check to see if we have a `comparator`. If so, disable sorting. collection.props.on( 'change:orderby', function() { this.$el.sortable( 'option', 'disabled', !! collection.comparator ); }, this ); this.collection.props.on( 'change:orderby', this.refreshSortable, this ); this.refreshSortable(); }, refreshSortable: function() { if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { return; } // If the `collection` has a `comparator`, disable sorting. var collection = this.collection, orderby = collection.props.get('orderby'), enabled = 'menuOrder' === orderby || ! collection.comparator; this.$el.sortable( 'option', 'disabled', ! enabled ); }, /** * @param {wp.media.model.Attachment} attachment * @returns {wp.media.View} */ createAttachmentView: function( attachment ) { var view = new this.options.AttachmentView({ controller: this.controller, model: attachment, collection: this.collection, selection: this.options.selection }); return this._viewsByCid[ attachment.cid ] = view; }, prepare: function() { // Create all of the Attachment views, and replace // the list in a single DOM operation. if ( this.collection.length ) { this.views.set( this.collection.map( this.createAttachmentView, this ) ); // If there are no elements, clear the views and load some. } else { this.views.unset(); this.collection.more().done( this.scroll ); } }, ready: function() { // Trigger the scroll event to check if we're within the // threshold to query for additional attachments. this.scroll(); }, scroll: function() { var view = this, el = this.options.scrollElement, scrollTop = el.scrollTop, toolbar; // The scroll event occurs on the document, but the element // that should be checked is the document body. if ( el === document ) { el = document.body; scrollTop = $(document).scrollTop(); } if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { return; } toolbar = this.views.parent.toolbar; // Show the spinner only if we are close to the bottom. if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { toolbar.get('spinner').show(); } if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { this.collection.more().done(function() { view.scroll(); toolbar.get('spinner').hide(); }); } } }); module.exports = Attachments;