From 55e1b4c982d3427831a10ffcbf5253088261844d Mon Sep 17 00:00:00 2001 From: Helen Hou-Sandi Date: Thu, 6 Mar 2014 22:54:32 +0000 Subject: [PATCH] At long last, a first pass at bringing the image editor into the media modal. props gcorne, DH-Shredder, tomauger. see #21811. git-svn-id: https://develop.svn.wordpress.org/trunk@27445 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/js/image-edit.js | 54 +++++- src/wp-includes/css/media-views.css | 20 ++ src/wp-includes/js/media-editor.js | 13 +- src/wp-includes/js/media-models.js | 1 + src/wp-includes/js/media-views.js | 272 ++++++++++++++++++++++------ src/wp-includes/media-template.php | 8 + src/wp-includes/media.php | 5 + src/wp-includes/script-loader.php | 2 +- src/wp-includes/version.php | 2 +- 9 files changed, 312 insertions(+), 65 deletions(-) diff --git a/src/wp-admin/js/image-edit.js b/src/wp-admin/js/image-edit.js index 9eaf51bf88..af908d553f 100644 --- a/src/wp-admin/js/image-edit.js +++ b/src/wp-admin/js/image-edit.js @@ -5,6 +5,7 @@ var imageEdit = window.imageEdit = { iasapi : {}, hold : {}, postid : '', + _view : false, intval : function(f) { return f | 0; @@ -241,11 +242,18 @@ var imageEdit = window.imageEdit = { $.post(ajaxurl, data, function(r) { $('#image-editor-' + postid).empty().append(r); t.toggleEditor(postid, 0); + // refresh the attachment model so that changes propagate + if ( this._view ) { + this._view.refresh(); + } }); }, save : function(postid, nonce) { - var data, target = this.getTarget(postid), history = this.filterHistory(postid, 0); + var data, + target = this.getTarget(postid), + history = this.filterHistory(postid, 0), + self = this; if ( '' === history ) { return false; @@ -283,11 +291,17 @@ var imageEdit = window.imageEdit = { $('#imgedit-response-' + postid).html('

' + ret.msg + '

'); } - imageEdit.close(postid); + if ( self._view ) { + self._view.save(); + } else { + imageEdit.close(postid); + } }); }, - open : function(postid, nonce) { + open : function( postid, nonce, view ) { + this._view = view; + var data, elem = $('#image-editor-' + postid), head = $('#media-head-' + postid), btn = $('#imgedit-open-btn-' + postid), spin = btn.siblings('.spinner'); @@ -319,8 +333,10 @@ var imageEdit = window.imageEdit = { }, initCrop : function(postid, image, parent) { - var t = this, selW = $('#imgedit-sel-width-' + postid), - selH = $('#imgedit-sel-height-' + postid); + var t = this, + selW = $('#imgedit-sel-width-' + postid), + selH = $('#imgedit-sel-height-' + postid), + $img; t.iasapi = $(image).imgAreaSelect({ parent: parent, @@ -330,7 +346,13 @@ var imageEdit = window.imageEdit = { minWidth: 3, minHeight: 3, - onInit: function() { + onInit: function( img ) { + // Ensure that the imgareaselect wrapper elements are position:absolute + // (even if we're in a position:fixed modal) + $img = $( img ); + $img.next().css( 'position', 'absolute' ) + .nextAll( '.imgareaselect-outer' ).css( 'position', 'absolute' ); + parent.children().mousedown(function(e){ var ratio = false, sel, defRatio; @@ -397,10 +419,22 @@ var imageEdit = window.imageEdit = { this.iasapi = {}; this.hold = {}; - $('#image-editor-' + postid).fadeOut('fast', function() { - $('#media-head-' + postid).fadeIn('fast'); - $(this).empty(); - }); + + // If we've loaded the editor in the context of a Media Modal, then switch to the previous view, + // whatever that might have been. + if ( this._view ){ + this._view.back(); + } + + // In case we are not accessing the image editor in the context of a View, close the editor the old-skool way + else { + $('#image-editor-' + postid).fadeOut('fast', function() { + $('#media-head-' + postid).fadeIn('fast'); + $(this).empty(); + }); + } + + }, notsaved : function(postid) { diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index b653be51e5..3e9a5125e5 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -1431,6 +1431,22 @@ overflow: hidden; } +/** + * Image Editor + */ + +.media-frame .image-editor { + padding: 16px; +} + +.media-frame .imgedit-wrap table td { + vertical-align: top; + padding-top: 0; +} + +.media-frame .imgedit-wrap table td.imgedit-settings { + width: 250px; +} /** * Embed from URL and Image Details */ @@ -1490,6 +1506,10 @@ display: block; } +.media-embed .edit-attachment { + margin-left: 10px; +} + .media-embed .thumbnail:after { content: ''; display: block; diff --git a/src/wp-includes/js/media-editor.js b/src/wp-includes/js/media-editor.js index a0cb52ea03..f065c0e8b0 100644 --- a/src/wp-includes/js/media-editor.js +++ b/src/wp-includes/js/media-editor.js @@ -698,7 +698,7 @@ this._frame = wp.media({ state: 'featured-image', - states: [ new wp.media.controller.FeaturedImage() ] + states: [ new wp.media.controller.FeaturedImage() , new wp.media.controller.EditImage() ] }); this._frame.on( 'toolbar:create:featured-image', function( toolbar ) { @@ -710,6 +710,17 @@ }); }, this._frame ); + this._frame.on( 'content:render:edit-image', function() { + var selection = this.state('featured-image').get('selection'), + view = new wp.media.view.EditImage( { model: selection.single(), controller: this } ).render(); + + this.content.set( view ); + + // after bringing in the frame, load the actual editor via an ajax call + view.loadEditor(); + + }, this._frame ); + this._frame.state('featured-image').on( 'select', this.select ); return this._frame; }, diff --git a/src/wp-includes/js/media-models.js b/src/wp-includes/js/media-models.js index adbbca249f..ea3fed8093 100644 --- a/src/wp-includes/js/media-models.js +++ b/src/wp-includes/js/media-models.js @@ -371,6 +371,7 @@ window.wp = window.wp || {}; bindAttachmentListeners: function() { this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl ); + this.listenTo( this.attachment, 'change', this.updateSize ); }, changeAttachment: function( attachment, props ) { diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js index 1a9328d1ef..375ad42c05 100644 --- a/src/wp-includes/js/media-views.js +++ b/src/wp-includes/js/media-views.js @@ -483,6 +483,54 @@ }; }); + media.selectionSync = { + syncSelection: function() { + var selection = this.get('selection'), + manager = this.frame._selection; + + if ( ! this.get('syncSelection') || ! manager || ! selection ) { + return; + } + + // If the selection supports multiple items, validate the stored + // attachments based on the new selection's conditions. Record + // the attachments that are not included; we'll maintain a + // reference to those. Other attachments are considered in flux. + if ( selection.multiple ) { + selection.reset( [], { silent: true }); + selection.validateAll( manager.attachments ); + manager.difference = _.difference( manager.attachments.models, selection.models ); + } + + // Sync the selection's single item with the master. + selection.single( manager.single ); + }, + + /** + * Record the currently active attachments, which is a combination + * of the selection's attachments and the set of selected + * attachments that this specific selection considered invalid. + * Reset the difference and record the single attachment. + */ + recordSelection: function() { + var selection = this.get('selection'), + manager = this.frame._selection; + + if ( ! this.get('syncSelection') || ! manager || ! selection ) { + return; + } + + if ( selection.multiple ) { + manager.attachments.reset( selection.toArray().concat( manager.difference ) ); + manager.difference = []; + } else { + manager.attachments.add( selection.toArray() ); + } + + manager.single = selection._single; + } + }; + /** * wp.media.controller.Library * @@ -635,51 +683,6 @@ return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); }, - syncSelection: function() { - var selection = this.get('selection'), - manager = this.frame._selection; - - if ( ! this.get('syncSelection') || ! manager || ! selection ) { - return; - } - - // If the selection supports multiple items, validate the stored - // attachments based on the new selection's conditions. Record - // the attachments that are not included; we'll maintain a - // reference to those. Other attachments are considered in flux. - if ( selection.multiple ) { - selection.reset( [], { silent: true }); - selection.validateAll( manager.attachments ); - manager.difference = _.difference( manager.attachments.models, selection.models ); - } - - // Sync the selection's single item with the master. - selection.single( manager.single ); - }, - - /** - * Record the currently active attachments, which is a combination - * of the selection's attachments and the set of selected - * attachments that this specific selection considered invalid. - * Reset the difference and record the single attachment. - */ - recordSelection: function() { - var selection = this.get('selection'), - manager = this.frame._selection; - - if ( ! this.get('syncSelection') || ! manager || ! selection ) { - return; - } - - if ( selection.multiple ) { - manager.attachments.reset( selection.toArray().concat( manager.difference ) ); - manager.difference = []; - } else { - manager.attachments.add( selection.toArray() ); - } - - manager.single = selection._single; - }, /** * If the state is active, no items are selected, and the current @@ -734,6 +737,8 @@ } }); + _.extend( media.controller.Library.prototype, media.selectionSync ); + /** * wp.media.controller.ImageDetails * @@ -989,7 +994,7 @@ toolbar: 'featured-image', title: l10n.setFeaturedImageTitle, priority: 60, - syncSelection: false + syncSelection: true }, media.controller.Library.prototype.defaults ), initialize: function() { @@ -1070,7 +1075,7 @@ toolbar: 'replace', title: l10n.replaceImageTitle, priority: 60, - syncSelection: false + syncSelection: true }, media.controller.Library.prototype.defaults ), initialize: function( options ) { @@ -1120,6 +1125,63 @@ } }); + /** + * wp.media.controller.EditImage + * + * @constructor + * @augments wp.media.controller.State + * @augments Backbone.Model + */ + media.controller.EditImage = media.controller.State.extend({ + defaults: { + id: 'edit-image', + url: '', + menu: false, + toolbar: 'edit-image', + title: l10n.editImage, + content: 'edit-image', + syncSelection: true + }, + + activate: function() { + if ( ! this.get('selection') ) { + this.set( 'selection', new media.model.Selection() ); + } + this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); + this.syncSelection(); + }, + + deactivate: function() { + this.stopListening( this.frame ); + }, + + toolbar: function() { + var frame = this.frame, + lastState = frame.lastState(), + previous = lastState && lastState.id; + + frame.toolbar.set( new media.view.Toolbar({ + controller: frame, + items: { + back: { + style: 'primary', + text: l10n.back, + priority: 20, + click: function() { + if ( previous ) { + frame.setState( previous ); + } else { + frame.close(); + } + } + } + } + }) ); + } + }); + + _.extend( media.controller.EditImage.prototype, media.selectionSync ); + /** * wp.media.controller.ReplaceVideo * @@ -1928,6 +1990,8 @@ // Embed states. new media.controller.Embed(), + new media.controller.EditImage( { selection: options.selection } ), + // Gallery states. new media.controller.CollectionEdit({ type: 'image', @@ -2043,6 +2107,7 @@ content: { 'embed': 'embedContent', + 'edit-image': 'editImageContent', 'edit-selection': 'editSelectionContent' }, @@ -2195,6 +2260,17 @@ this.content.set( view ); }, + editImageContent: function() { + var selection = this.state().get('selection'), + view = new media.view.EditImage( { model: selection.single(), controller: this } ).render(); + + this.content.set( view ); + + // after creating the wrapper view, load the actual editor via an ajax call + view.loadEditor(); + + }, + // Toolbars /** @@ -2537,6 +2613,7 @@ media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); this.on( 'menu:create:image-details', this.createMenu, this ); this.on( 'content:render:image-details', this.renderImageDetailsContent, this ); + this.on( 'content:render:edit-image', this.editImageContent, this ); this.on( 'menu:render:image-details', this.renderMenu, this ); this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this ); // override the select toolbar @@ -2560,7 +2637,11 @@ toolbar: 'replace', priority: 80, displaySettings: true - }) + }), + new media.controller.EditImage( { + image: this.image, + selection: this.options.selection + } ) ]); }, @@ -2575,6 +2656,32 @@ }, + editImageContent: function() { + var state = this.state(), + attachment = state.get('image').attachment, + model, + view; + + if ( ! attachment ) { + return; + } + + model = state.get('selection').single(); + + if ( ! model ) { + model = attachment; + } + + view = new media.view.EditImage( { model: model, controller: this } ).render(); + + this.content.set( view ); + + // after bringing in the frame, load the actual editor via an ajax call + view.loadEditor(); + + }, + + renderMenu: function( view ) { var lastState = this.lastState(), previous = lastState && lastState.id, @@ -5864,8 +5971,9 @@ } }, - editAttachment: function() { - this.$el.addClass('needs-refresh'); + editAttachment: function( event ) { + event.preventDefault(); + this.controller.setState( 'edit-image' ); }, /** * @param {Object} event @@ -5875,6 +5983,7 @@ event.preventDefault(); this.model.fetch(); } + }); /** @@ -6158,10 +6267,15 @@ media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({ className: 'image-details', template: media.template('image-details'), - + events: _.defaults( media.view.Settings.AttachmentDisplay.prototype.events, { + 'click .edit-attachment': 'editAttachment' + } ), initialize: function() { // used in AttachmentDisplay.prototype.updateLinkTo this.options.attachment = this.model.attachment; + if ( this.model.attachment ) { + this.listenTo( this.model.attachment, 'change:url', this.updateUrl ); + } media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments ); }, @@ -6177,7 +6291,6 @@ }, this.options ); }, - render: function() { var self = this, args = arguments; @@ -6198,9 +6311,64 @@ resetFocus: function() { this.$( '.caption textarea' ).focus(); this.$( '.embed-image-settings' ).scrollTop( 0 ); + }, + + updateUrl: function() { + this.$( '.thumbnail img' ).attr( 'src', this.model.get('url' ) ); + this.$( '.url' ).val( this.model.get('url' ) ); + }, + + editAttachment: function( event ) { + event.preventDefault(); + this.controller.setState( 'edit-image' ); } }); + + media.view.EditImage = media.View.extend({ + + className: 'image-editor', + template: media.template('image-editor'), + + initialize: function( options ) { + this.editor = window.imageEdit; + this.controller = options.controller; + media.View.prototype.initialize.apply( this, arguments ); + }, + + prepare: function() { + return this.model.toJSON(); + }, + + render: function() { + media.View.prototype.render.apply( this, arguments ); + return this; + }, + + loadEditor: function() { + this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); + }, + + back: function() { + var lastState = this.controller.lastState(); + this.controller.setState( lastState ); + }, + + refresh: function() { + this.model.fetch(); + }, + + save: function() { + var self = this, + lastState = this.controller.lastState(); + + this.model.fetch().done( function() { + self.controller.setState( lastState ); + }); + } + + }); + /** * wp.media.view.AudioDetails * diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 8fcbb0831e..54b9780559 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -559,6 +559,9 @@ function wp_print_media_templates() {
+ <# if ( data.attachment ) { #> + + <# } #>
@@ -649,6 +652,11 @@ function wp_print_media_templates() {
+ +