// Responsible for managing the ui for a collection of attachments. Works in // conjunction with `AttachmentView`, `Attachment`, `AttachmentCollection` // and specialized Handlebars helper `attachmentComposer`. // A JSON description of the collection is written to the element’s // data-computed-value, and subsequently read by Slices when saving the Page. // // This shouldn’t be instantiated directly. // Instead, use `{{attachmentComposer}}` like this: // // {{#attachmentComposer myAttachments}} // // {{/attachmentComposer}} // slices.AttachmentComposerView = Backbone.View.extend({ DROP_THRESHOLD: 15, views: {}, // internal view cache events: { 'click [data-action="library"]' : 'openAssetDrawer', 'click [data-action="remove"]' : 'removeClicked' }, template: Handlebars.compile( '
' + ' ' ), className: 'attachment-composer', broadcastChanges: true, // Initialize the view. There are a few steps here, so read on. initialize: function() { _.bindAll(this); // If this.options.collection is just a simple array, we need to // instantiate and AttachmentCollection. this.collection = new slices.AttachmentCollection(this.options.collection); this.collection.bind('add' , this.addAttachment); this.collection.bind('remove' , this.removeAttachment); this.collection.bind('change' , this.update); this.collection.bind('reset' , this.update); // Listen out for asset drags and drops. $(window).on('assets:dragStarted', this.onAssetDragStarted); if (this.options.autoAttach) { // Defer the attachment of the real view element. _.defer(this.attach); } }, // Placeholder element to render into later. placeholder: function() { return Handlebars.compile('')(this); }, render: function() { this.broadcastChanges = false; $(this.el).html(this.template(this)); this.collection.each(this.addAttachment); this.makeSortable(); this.makeUploader(); this.update(); this.broadcastChanges = true; return this; }, // Replace our placeholder element with this.el. attach: function() { $('#placeholder-' + this.id).replaceWith(this.el); this.render(); }, // Add ui and references for an attachment. addAttachment: function(attachment, collection, options) { var view = new slices.AttachmentView({ fields: this.options.fields, model: attachment }); if (options.index < collection.length - 1) { view.$el.insertBefore(this.$('.attachment-list').children()[options.index]); } else { this.$('.attachment-list').append(view.el); } view.render(); this.views[attachment.cid] = view; this.update(); }, // Remove ui and references for an attachment. removeAttachment: function(attachment) { var view = this.views[attachment.cid]; view.remove(); delete this.views[attachment.cid]; if (attachment.file) this.uploader.removeFile(attachment.file); this.update(); }, // Write a JSON representation of the collection into data-computed-value // on this.el. Slices picks up on the computed-value when saving the page. // Ignores any items with a null asset_id, which are likely to be // failed uploads. update: function() { var value = this.collection.toJSON(), $el = $(this.el); value = _.reject(value, function(a) { return a.asset_id == null }); $el.data('computed-value', value); $el[this.collection.isEmpty() ? 'removeClass' : 'addClass']('not-empty'); if (this.broadcastChanges) $el.trigger('change'); }, // Infers a view and asset from just-clicked button and attempts to remove // the asset from the collection. Will prevent action if upload is in // progress - not ideal, but Plupload doesn't support cancelling an // in-progress uploader. removeClicked: function(e) { var button = $(e.target), view = button.parent('li'), attachment = view.data('model'); this.collection.remove(attachment); }, // Shows the asset library. openAssetDrawer: function(e) { e.preventDefault(); e.stopImmediatePropagation(); slices.assetDrawer().open({ step: this.assetDrawerStep }); }, assetDrawerStep: function() { var el = $(this.el), bottom = el.offset().top + el.outerHeight() + 30, drawerTop = $(slices.assetDrawer().el).offset().top; if (bottom > drawerTop) { var body = $('body'); body.scrollTop(body.scrollTop() + (bottom - drawerTop)); } }, // Returns the Attachment view responsible for given File. viewForFile: function(file) { return this.views[file.attachment.cid]; }, // Make items sortable using jQuery UI sortable plugin. makeSortable: function() { this.$('.attachment-list').sortable({ handle: '.attachment-thumb', scroll: false, beforeStart: _.bind(function(e, ui) { this.attachmentList().freezeHeight(); window.autoscroll.start(); }, this), stop: _.bind(function(e, ui) { this.attachmentList().thawHeight(); window.autoscroll.stop(); }, this), update: this.updateOnSort }); }, // Update collection to match the visible order of item elements. // We avoid jQuery.map here, because it returns some sort of weird // jquery object, rather than an array. updateOnSort: function() { var newOrder = _.map(this.$('.attachment-list li').get(), function(li) { return $(li).data('model'); }); this.collection.reset(newOrder); }, // Create an uploader instance and bind up its callbacks. makeUploader: function() { this.uploader = new slices.Uploader({ button : this.$('[data-action="upload"]'), drop : this.el }); this.uploader.bind('filesAdded', this.onFilesAdded); this.uploader.bind('fileUploaded', this.onFileUploaded); }, // When files are added to the upload queue we create corresponding // attachment objects and add them to the collection. onFilesAdded: function(event) { var files = event.files, uploader = event.uploader; _(files).each(function(file) { var a = new slices.Attachment({ asset: new slices.Asset({ file: file }) }); // This is clearly a code-smell, but it lets us easily look-up // the attachment-view for this file when events occur. file.attachment = a; // These bits are fine. this.collection.add(a); this.updateFileStatus(file); }, this); this.uploader.start(); }, // This looks weird, I know, but really all we’re doing is taking the // response from our upload to /assets and feeding it into our // attachment model. onFileUploaded: function(event) { var file = event.file, response = event.response, attachment = file.attachment, asset = attachment.get('asset'); // Update the attachment model with just the new asset_id and update // the underlying asset with all the new info. This could do with // refactoring to make it better reveal its intent. attachment.set({ asset_id: response.id }); asset.set(response); // Finally complete upload progress display and transition to thumbnail. this.viewForFile(file).updateFileAndComplete(file); // Need to update when uploads complete, so data('value') correctly // reflects all these attachments. this.update(); // Send the signal to any asset library views. $(window).trigger('assets:uploadCompleted'); }, // Tell the appropriate attachment object to update against the file. // This needs to be deferred, for reasons mentioned above. updateFileStatus: function(file) { this.viewForFile(file).updateFile(file); }, // The following methods implement the asset-receiver interface. // See slices.AssetLibraryView for details. onAssetDragStarted: function(e, library) { library.registerReceiver(this); }, withinBounds: function(x, y) { var offset = $(this.el).offset(), top = offset.top - this.DROP_THRESHOLD, left = offset.left - this.DROP_THRESHOLD, bottom = top + $(this.el).height() + (this.DROP_THRESHOLD * 2), right = left + $(this.el).width() + (this.DROP_THRESHOLD * 2); return x >= left && x <= right && y >= top && y <= bottom; }, cursor: function() { return this._cursor = this._cursor || $('