<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[27050] trunk/src/wp-includes: Introduce Edit Image (single mode) in the media modal and use it to edit images inserted in the editor.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://core.trac.wordpress.org/changeset/27050">27050</a></dd>
<dt>Author</dt> <dd>azaozz</dd>
<dt>Date</dt> <dd>2014-01-28 21:16:42 +0000 (Tue, 28 Jan 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Introduce Edit Image (single mode) in the media modal and use it to edit images inserted in the editor. Adds new feature: replace an image in the editor. Props gcorne, see <a href="http://core.trac.wordpress.org/ticket/24409">#24409</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpeditorphp">trunk/src/wp-includes/class-wp-editor.php</a></li>
<li><a href="#trunksrcwpincludescssmediaviewscss">trunk/src/wp-includes/css/media-views.css</a></li>
<li><a href="#trunksrcwpincludesjsmediamodelsjs">trunk/src/wp-includes/js/media-models.js</a></li>
<li><a href="#trunksrcwpincludesjsmediaviewsjs">trunk/src/wp-includes/js/media-views.js</a></li>
<li><a href="#trunksrcwpincludesjstinymcepluginswpeditimagepluginjs">trunk/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js</a></li>
<li><a href="#trunksrcwpincludesmediatemplatephp">trunk/src/wp-includes/media-template.php</a></li>
<li><a href="#trunksrcwpincludesmediaphp">trunk/src/wp-includes/media.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpeditorphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/class-wp-editor.php (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/class-wp-editor.php        2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/class-wp-editor.php   2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -228,7 +228,6 @@
</span><span class="cx">                                          'paste',
</span><span class="cx">                                          'tabfocus',
</span><span class="cx">                                          'textcolor',
</span><del>-                                               'image',
</del><span class="cx">                                           'fullscreen',
</span><span class="cx">                                          'wordpress',
</span><span class="cx">                                          'wpeditimage',
</span></span></pre></div>
<a id="trunksrcwpincludescssmediaviewscss"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/css/media-views.css (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/css/media-views.css        2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/css/media-views.css   2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -86,6 +86,10 @@
</span><span class="cx">    color: #a9a9a9;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.media-frame .hidden {
+       display: none;
+}
+
</ins><span class="cx"> /* Enable draggable on IE10 touch events until it's rolled into jQuery UI core */
</span><span class="cx"> .ui-sortable,
</span><span class="cx"> .ui-draggable {
</span><span class="lines">@@ -1411,7 +1415,7 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><del>- * Embed from URL
</del><ins>+ * Embed from URL and Image Details
</ins><span class="cx">  */
</span><span class="cx"> .embed-url {
</span><span class="cx">  display: block;
</span><span class="lines">@@ -1452,6 +1456,10 @@
</span><span class="cx">  overflow: auto;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.image-details .embed-image-settings {
+       top: 0;
+}
+
</ins><span class="cx"> .media-embed .thumbnail {
</span><span class="cx">  max-width: 100%;
</span><span class="cx">  max-height: 200px;
</span><span class="lines">@@ -1484,6 +1492,10 @@
</span><span class="cx">  clear: both;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.media-embed .setting .hidden {
+       display: none;
+}
+
</ins><span class="cx"> .media-embed .setting span {
</span><span class="cx">  display: block;
</span><span class="cx">  width: 200px;
</span><span class="lines">@@ -1926,4 +1938,4 @@
</span><span class="cx">          .media-frame .spinner {
</span><span class="cx">                  background-image: url('../images/spinner-2x.gif');
</span><span class="cx">          }
</span><del>-}
</del><span class="cx">\ No newline at end of file
</span><ins>+}
</ins></span></pre></div>
<a id="trunksrcwpincludesjsmediamodelsjs"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/js/media-models.js (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/js/media-models.js 2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/js/media-models.js    2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -2,7 +2,7 @@
</span><span class="cx"> window.wp = window.wp || {};
</span><span class="cx"> 
</span><span class="cx"> (function($){
</span><del>-       var Attachment, Attachments, Query, compare, l10n, media;
</del><ins>+        var Attachment, Attachments, Query, PostImage, compare, l10n, media;
</ins><span class="cx"> 
</span><span class="cx">  /**
</span><span class="cx">   * wp.media( attributes )
</span><span class="lines">@@ -30,6 +30,8 @@
</span><span class="cx">                  frame = new MediaFrame.Select( attributes );
</span><span class="cx">          } else if ( 'post' === attributes.frame && MediaFrame.Post ) {
</span><span class="cx">                  frame = new MediaFrame.Post( attributes );
</span><ins>+               } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) {
+                       frame = new MediaFrame.ImageDetails( attributes );
</ins><span class="cx">           }
</span><span class="cx"> 
</span><span class="cx">          delete attributes.frame;
</span><span class="lines">@@ -340,6 +342,121 @@
</span><span class="cx">  });
</span><span class="cx"> 
</span><span class="cx">  /**
</span><ins>+        * wp.media.model.Attachment
+        *
+        * @constructor
+        * @augments Backbone.Model
+        *
+        **/
+       PostImage = media.model.PostImage = Backbone.Model.extend({
+
+               initialize: function( attributes ) {
+                       this.attachment = false;
+
+                       if ( attributes.attachment_id ) {
+                               this.attachment = media.model.Attachment.get( attributes.attachment_id );
+                               this.dfd = this.attachment.fetch();
+                               this.bindAttachmentListeners();
+                       }
+
+                       // keep url in sync with changes to the type of link
+                       this.on( 'change:link', this.updateLinkUrl, this );
+                       this.on( 'change:size', this.updateSize, this );
+
+                       this.setLinkTypeFromUrl();
+
+               },
+
+               bindAttachmentListeners: function() {
+                       this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl );
+               },
+
+               changeAttachment: function( attachment, props ) {
+                       this.stopListening( this.attachment );
+                       this.attachment = attachment;
+                       this.bindAttachmentListeners();
+
+                       this.set( 'attachment_id', this.attachment.get( 'id' ) );
+                       this.set( 'caption', this.attachment.get( 'caption' ) );
+                       this.set( 'alt', this.attachment.get( 'alt' ) );
+                       this.set( 'size', props.get( 'size' ) );
+                       this.set( 'align', props.get( 'align' ) );
+                       this.set( 'link', props.get( 'link' ) );
+                       this.updateLinkUrl();
+                       this.updateSize();
+               },
+
+               setLinkTypeFromUrl: function() {
+                       var linkUrl = this.get( 'linkUrl' ),
+                               type;
+
+                       if ( ! linkUrl ) {
+                               this.set( 'link', 'none' );
+                               return;
+                       }
+
+                       // default to custom if there is a linkUrl
+                       type = 'custom';
+
+                       if ( this.attachment ) {
+                               if ( this.attachment.get( 'url' ) === linkUrl ) {
+                                       type = 'file';
+                               } else if ( this.attachment.get( 'link' ) === linkUrl ) {
+                                       type = 'post';
+                               }
+                       } else {
+                               if ( this.get( 'url' ) === linkUrl ) {
+                                       type = 'file';
+                               }
+                       }
+
+                       this.set( 'link', type );
+
+               },
+
+
+               updateLinkUrl: function() {
+                       var link = this.get( 'link' ),
+                               url;
+
+                       switch( link ) {
+                               case 'file':
+                                       if ( this.attachment ) {
+                                               url = this.attachment.get( 'url' );
+                                       } else {
+                                               url = this.get( 'url' );
+                                       }
+                                       this.set( 'linkUrl', url );
+                                       break;
+                               case 'post':
+                                       this.set( 'linkUrl', this.attachment.get( 'link' ) );
+                                       break;
+                               case 'none':
+                                       this.set( 'linkUrl', '' );
+                                       break;
+
+                       }
+
+               },
+
+               updateSize: function() {
+                       var size;
+
+                       if ( ! this.attachment ) {
+                               return;
+                       }
+
+                       size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ];
+                       this.set( 'url', size.url );
+                       this.set( 'width', size.width );
+                       this.set( 'height', size.height );
+
+               }
+
+
+       });
+
+       /**
</ins><span class="cx">    * wp.media.model.Attachments
</span><span class="cx">   *
</span><span class="cx">   * @constructor
</span><span class="lines">@@ -1170,4 +1287,4 @@
</span><span class="cx">          window.wp = null;
</span><span class="cx">  });
</span><span class="cx"> 
</span><del>-}(jQuery));
</del><span class="cx">\ No newline at end of file
</span><ins>+}(jQuery));
</ins></span></pre></div>
<a id="trunksrcwpincludesjsmediaviewsjs"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/js/media-views.js (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/js/media-views.js  2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/js/media-views.js     2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -980,7 +980,108 @@
</span><span class="cx">          }
</span><span class="cx">  });
</span><span class="cx"> 
</span><ins>+
+       media.controller.ImageDetails = media.controller.State.extend({
+
+               defaults: _.defaults({
+                       id: 'image-details',
+                       toolbar: 'image-details',
+                       title: l10n.imageDetailsTitle,
+                       content: 'image-details',
+                       menu: 'image-details',
+                       router: false,
+                       attachment: false,
+                       priority: 60,
+                       editing: false
+               }, media.controller.Library.prototype.defaults ),
+
+               initialize: function( options ) {
+                       this.image = options.image;
+                       media.controller.State.prototype.initialize.apply( this, arguments );
+               }
+       });
+
</ins><span class="cx">   /**
</span><ins>+        * wp.media.controller.ReplaceImage
+        *
+        * Replace a selected single image
+        *
+        **/
+       media.controller.ReplaceImage = media.controller.Library.extend({
+               defaults: _.defaults({
+                       id:         'replace-image',
+                       filterable: 'uploaded',
+                       multiple:   false,
+                       toolbar:    'replace',
+                       title:      l10n.replaceImageTitle,
+                       priority:   60,
+                       syncSelection: false
+               }, media.controller.Library.prototype.defaults ),
+
+               initialize: function( options ) {
+                       var library, comparator;
+
+                       this.image = options.image;
+
+                       // If we haven't been provided a `library`, create a `Selection`.
+                       if ( ! this.get('library') ) {
+                               this.set( 'library', media.query({ type: 'image' }) );
+                       }
+                       /**
+                        * call 'initialize' directly on the parent class
+                        */
+                       media.controller.Library.prototype.initialize.apply( this, arguments );
+
+                       library    = this.get('library');
+                       comparator = library.comparator;
+
+                       // Overload the library's comparator to push items that are not in
+                       // the mirrored query to the front of the aggregate collection.
+                       library.comparator = function( a, b ) {
+                               var aInQuery = !! this.mirroring.get( a.cid ),
+                                       bInQuery = !! this.mirroring.get( b.cid );
+
+                               if ( ! aInQuery && bInQuery ) {
+                                       return -1;
+                               } else if ( aInQuery && ! bInQuery ) {
+                                       return 1;
+                               } else {
+                                       return comparator.apply( this, arguments );
+                               }
+                       };
+
+                       // Add all items in the selection to the library, so any featured
+                       // images that are not initially loaded still appear.
+                       library.observe( this.get('selection') );
+               },
+
+               activate: function() {
+                       this.updateSelection();
+                       /**
+                        * call 'activate' directly on the parent class
+                        */
+                       media.controller.Library.prototype.activate.apply( this, arguments );
+               },
+
+               deactivate: function() {
+                       /**
+                        * call 'deactivate' directly on the parent class
+                        */
+                       media.controller.Library.prototype.deactivate.apply( this, arguments );
+               },
+
+               updateSelection: function() {
+                       var selection = this.get('selection'),
+                               attachment = this.image.attachment;
+
+                       selection.reset( attachment ? [ attachment ] : [] );
+
+               }
+
+
+       });
+
+       /**
</ins><span class="cx">    * wp.media.controller.Embed
</span><span class="cx">   *
</span><span class="cx">   * @constructor
</span><span class="lines">@@ -1924,8 +2025,157 @@
</span><span class="cx">                          }
</span><span class="cx">                  }) );
</span><span class="cx">          }
</span><ins>+
</ins><span class="cx">   });
</span><span class="cx"> 
</span><ins>+       media.view.MediaFrame.ImageDetails = media.view.MediaFrame.Select.extend({
+               defaults: {
+                       id:      'image',
+                       url:     '',
+                       menu:    'image-details',
+                       content: 'image-details',
+                       toolbar: 'image-details',
+                       type:    'link',
+                       title:    l10n.imageDetailsTitle,
+                       priority: 120
+               },
+
+               initialize: function( options ) {
+                       this.image = new media.model.PostImage( options.metadata );
+                       this.options.selection = new media.model.Selection( this.image.attachment, { multiple: false } );
+                       media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
+               },
+
+               bindHandlers: function() {
+                       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( 'menu:render:image-details', this.renderMenu, this );
+                       this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this );
+                       // override the select toolbar
+                       this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this );
+               },
+
+               createStates: function() {
+                       this.states.add([
+                               new media.controller.ImageDetails({
+                                       image: this.image,
+                                       editable: false,
+                                       menu: 'image-details'
+                               }),
+                               new media.controller.ReplaceImage({
+                                       id: 'replace-image',
+                                       library:   media.query( { type: 'image' } ),
+                                       image: this.image,
+                                       multiple:  false,
+                                       title:     l10n.imageReplaceTitle,
+                                       menu: 'image-details',
+                                       toolbar: 'replace',
+                                       priority:  80,
+                                       displaySettings: true
+                               })
+                       ]);
+               },
+
+               renderImageDetailsContent: function() {
+                       var view = new media.view.ImageDetails({
+                               controller: this,
+                               model: this.state().image,
+                               attachment: this.state().image.attachment
+                       }).render();
+
+                       this.content.set( view );
+
+               },
+
+               renderMenu: function( view ) {
+                       var lastState = this.lastState(),
+                               previous = lastState && lastState.id,
+                               frame = this;
+
+                       view.set({
+                               cancel: {
+                                       text:     l10n.imageDetailsCancel,
+                                       priority: 20,
+                                       click:    function() {
+                                               if ( previous ) {
+                                                       frame.setState( previous );
+                                               } else {
+                                                       frame.close();
+                                               }
+                                       }
+                               },
+                               separateCancel: new media.View({
+                                       className: 'separator',
+                                       priority: 40
+                               })
+                       });
+
+               },
+
+               renderImageDetailsToolbar: function() {
+                       this.toolbar.set( new media.view.Toolbar({
+                               controller: this,
+                               items: {
+                                       select: {
+                                               style:    'primary',
+                                               text:     l10n.update,
+                                               priority: 80,
+
+                                               click: function() {
+                                                       var controller = this.controller,
+                                                               state = controller.state();
+
+                                                       controller.close();
+
+                                                       // not sure if we want to use wp.media.string.image which will create a shortcode or
+                                                       // perhaps wp.html.string to at least to build the <img />
+                                                       state.trigger( 'update', controller.image.toJSON() );
+
+                                                       // Restore and reset the default state.
+                                                       controller.setState( controller.options.state );
+                                                       controller.reset();
+                                               }
+                                       }
+                               }
+                       }) );
+               },
+
+               renderReplaceImageToolbar: function() {
+                       this.toolbar.set( new media.view.Toolbar({
+                               controller: this,
+                               items: {
+                                       replace: {
+                                               style:    'primary',
+                                               text:     l10n.replace,
+                                               priority: 80,
+
+                                               click: function() {
+                                                       var controller = this.controller,
+                                                               state = controller.state(),
+                                                               selection = state.get( 'selection' ),
+                                                               attachment = selection.single();
+
+                                                       controller.close();
+
+                                                       controller.image.changeAttachment( attachment, state.display( attachment ) );
+
+                                                       // not sure if we want to use wp.media.string.image which will create a shortcode or
+                                                       // perhaps wp.html.string to at least to build the <img />
+                                                       state.trigger( 'replace', controller.image.toJSON() );
+
+                                                       // Restore and reset the default state.
+                                                       controller.setState( controller.options.state );
+                                                       controller.reset();
+                                               }
+                                       }
+                               }
+                       }) );
+               }
+
+       });
+
+
</ins><span class="cx">   /**
</span><span class="cx">   * wp.media.view.Modal
</span><span class="cx">   *
</span><span class="lines">@@ -4555,7 +4805,7 @@
</span><span class="cx">                          attachment = this.options.attachment;
</span><span class="cx"> 
</span><span class="cx">                  if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
</span><del>-                               $input.hide();
</del><ins>+                                $input.addClass( 'hidden' );
</ins><span class="cx">                           return;
</span><span class="cx">                  }
</span><span class="cx"> 
</span><span class="lines">@@ -4571,7 +4821,7 @@
</span><span class="cx">                          $input.prop( 'readonly', 'custom' !== linkTo );
</span><span class="cx">                  }
</span><span class="cx"> 
</span><del>-                       $input.show();
</del><ins>+                        $input.removeClass( 'hidden' );
</ins><span class="cx"> 
</span><span class="cx">                  // If the input is visible, focus and select its contents.
</span><span class="cx">                  if ( $input.is(':visible') ) {
</span><span class="lines">@@ -4932,4 +5182,50 @@
</span><span class="cx">                  this.$('img').attr( 'src', this.model.get('url') );
</span><span class="cx">          }
</span><span class="cx">  });
</span><del>-}(jQuery));
</del><span class="cx">\ No newline at end of file
</span><ins>+
+       media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({
+               className: 'image-details',
+               template:  media.template('image-details'),
+
+               initialize: function() {
+                       // used in AttachmentDisplay.prototype.updateLinkTo
+                       this.options.attachment = this.model.attachment;
+                       media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
+               },
+
+               prepare: function() {
+                       var attachment = false;
+
+                       if ( this.model.attachment ) {
+                               attachment = this.model.attachment.toJSON();
+                       }
+                       return _.defaults({
+                               model: this.model.toJSON(),
+                               attachment: attachment
+                       }, this.options );
+               },
+
+
+               render: function() {
+                       var self = this,
+                               args = arguments;
+                       if ( this.model.attachment && 'pending' === this.model.dfd.state() ) {
+                               // should instead show a spinner when the attachment is new and then add a listener that updates on change
+                               this.model.dfd.done( function() {
+                                       media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
+                                       self.resetFocus();
+                               } );
+                       } else {
+                               media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
+                               setTimeout( function() { self.resetFocus(); }, 10 );
+                       }
+
+                       return this;
+               },
+
+               resetFocus: function() {
+                       this.$( '.caption textarea' ).focus();
+                       this.$( '.embed-image-settings' ).scrollTop( 0 );
+               }
+       });
+}(jQuery));
</ins></span></pre></div>
<a id="trunksrcwpincludesjstinymcepluginswpeditimagepluginjs"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js   2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js      2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -101,6 +101,176 @@
</span><span class="cx">          });
</span><span class="cx">  }
</span><span class="cx"> 
</span><ins>+       function extractImageData( imageNode ) {
+               var classes, metadata, captionBlock, caption;
+
+               // default attributes
+               metadata = {
+                       attachment_id: false,
+                       url: false,
+                       height: '',
+                       width: '',
+                       size: 'none',
+                       caption: '',
+                       alt: '',
+                       align: 'none',
+                       link: false,
+                       linkUrl: ''
+               };
+
+               metadata.url = editor.dom.getAttrib( imageNode, 'src' );
+               metadata.alt = editor.dom.getAttrib( imageNode, 'alt' );
+               metadata.width = parseInt( editor.dom.getAttrib( imageNode, 'width' ), 10 );
+               metadata.height = parseInt( editor.dom.getAttrib( imageNode, 'height' ), 10 );
+
+               //TODO: probably should capture attributes on both the <img /> and the <a /> so that they can be restored when the image and/or caption are updated
+               // maybe use getAttribs()
+
+               // extract meta data from classes (candidate for turning into a method)
+               classes = imageNode.className.split( ' ' );
+               tinymce.each( classes, function( name ) {
+
+                       if ( /^wp-image/.test( name ) ) {
+                               metadata.attachment_id = parseInt( name.replace( 'wp-image-', '' ), 10 );
+                       }
+
+                       if ( /^align/.test( name ) ) {
+                               metadata.align = name.replace( 'align', '' );
+                       }
+
+                       if ( /^size/.test( name ) ) {
+                               metadata.size = name.replace( 'size-', '' );
+                       }
+               } );
+
+
+               // extract caption
+               captionBlock = editor.dom.getParents( imageNode, '.wp-caption' );
+
+               if ( captionBlock.length ) {
+                       captionBlock = captionBlock[0];
+
+                       classes = captionBlock.className.split( ' ' );
+                       tinymce.each( classes, function( name ) {
+                               if ( /^align/.test( name ) ) {
+                                       metadata.align = name.replace( 'align', '' );
+                               }
+                       } );
+                       caption = editor.dom.select( 'dd.wp-caption-dd', captionBlock );
+                       if ( caption.length ) {
+                               caption = caption[0];
+                               // need to do some more thinking about this
+                               metadata.caption = editor.serializer.serialize( caption )
+                                       .replace( /<br[^>]*>/g, '$&\n' ).replace( /^<p>/, '' ).replace( /<\/p>$/, '' );
+
+                       }
+               }
+
+               // extract linkTo
+               if ( imageNode.parentNode.nodeName === 'A' ) {
+                       metadata.linkUrl = editor.dom.getAttrib( imageNode.parentNode, 'href' );
+               }
+
+               return metadata;
+
+       }
+
+       function updateImage( imageNode, imageData ) {
+               var className, width, node, html, captionNode, nodeToReplace, uid;
+
+               if ( imageData.caption ) {
+
+                       html = createImageAndLink( imageData, 'html' );
+
+                       width = imageData.width + 10;
+                       className = 'align' + imageData.align;
+
+                       //TODO: shouldn't add the id attribute if it isn't an attachment
+
+                       // should create a new function for genrating the caption markup
+                       html =  '<dl id="'+ imageData.attachment_id +'" class="wp-caption '+ className +'" style="width: '+ width +'px">' +
+                               '<dt class="wp-caption-dt">'+ html + '</dt><dd class="wp-caption-dd">'+ imageData.caption +'</dd></dl>';
+
+                       node = editor.dom.create( 'div', { 'class': 'mceTemp', draggable: 'true' }, html );
+               } else {
+                       node = createImageAndLink( imageData, 'node' );
+               }
+
+               nodeToReplace = imageNode;
+
+               captionNode = editor.dom.getParent( imageNode, '.mceTemp' );
+
+               if ( captionNode ) {
+                       nodeToReplace = captionNode;
+               } else {
+                       if ( imageNode.parentNode.nodeName === 'A' ) {
+                               nodeToReplace = imageNode.parentNode;
+                       }
+               }
+               // uniqueId isn't super exciting, so maybe we want to use something else
+               uid = editor.dom.uniqueId( 'wp_' );
+               editor.dom.setAttrib( node, 'data-wp-replace-id', uid );
+               editor.dom.replace( node, nodeToReplace );
+
+               // find the updated node
+               node = editor.dom.select( '[data-wp-replace-id="' + uid + '"]' )[0];
+
+               editor.dom.setAttrib( node, 'data-wp-replace-id', '' );
+
+               if ( node.nodeName === 'IMG' ) {
+                       editor.selection.select( node );
+               } else {
+                       editor.selection.select( editor.dom.select( 'img', node )[0] );
+               }
+               editor.nodeChanged();
+
+       }
+
+       function createImageAndLink( imageData, mode ) {
+               var classes = [],
+                       props;
+
+               mode = mode ? mode : 'node';
+
+
+               if ( ! imageData.caption ) {
+                       classes.push( 'align' + imageData.align );
+               }
+
+               if ( imageData.attachment_id ) {
+                       classes.push( 'wp-image-' + imageData.attachment_id );
+                       if ( imageData.size ) {
+                               classes.push( 'size-' + imageData.size );
+                       }
+               }
+
+               props = {
+                       src: imageData.url,
+                       width: imageData.width,
+                       height: imageData.height,
+                       alt: imageData.alt
+               };
+
+               if ( classes.length ) {
+                       props['class'] = classes.join( ' ' );
+               }
+
+               if ( imageData.linkUrl ) {
+                       if ( mode === 'node' ) {
+                               return editor.dom.create( 'a', { href: imageData.linkUrl }, editor.dom.createHTML( 'img', props ) );
+                       } else if ( mode === 'html' ) {
+                               return editor.dom.createHTML( 'a', { href: imageData.linkUrl }, editor.dom.createHTML( 'img', props ) );
+                       }
+               } else {
+                       if ( mode === 'node' ) {
+                               return editor.dom.create( 'img', props );
+                       } else if ( mode === 'html' ) {
+                               return editor.dom.createHTML( 'img', props );
+                       }
+
+               }
+       }
+
</ins><span class="cx">   editor.on( 'init', function() {
</span><span class="cx">          var dom = editor.dom;
</span><span class="cx"> 
</span><span class="lines">@@ -452,6 +622,40 @@
</span><span class="cx">          }
</span><span class="cx">  });
</span><span class="cx"> 
</span><ins>+       editor.on( 'mousedown', function( e ) {
+               var imageNode, frame, callback;
+               if ( e.target.nodeName === 'IMG' && editor.selection.getNode() === e.target ) {
+                       // Don't trigger on right-click
+                       if ( e.button !== 2 ) {
+
+                               // Don't attempt to edit placeholders
+                               if ( editor.dom.hasClass( e.target, 'mceItem' ) || '1' === editor.dom.getAttrib( e.target, 'data-mce-placeholder' ) ) {
+                                       return;
+                               }
+
+                               imageNode = e.target;
+
+                               frame = wp.media({
+                                       frame: 'image',
+                                       state: 'image-details',
+                                       metadata: extractImageData( imageNode )
+                               } );
+
+                               callback = function( imageData ) {
+                                       updateImage( imageNode, imageData );
+                                       editor.focus();
+                               };
+
+                               frame.state('image-details').on( 'update', callback );
+                               frame.state('replace-image').on( 'replace', callback );
+
+                               frame.open();
+
+
+                       }
+               }
+       } );
+
</ins><span class="cx">   editor.wpSetImgCaption = function( content ) {
</span><span class="cx">          return parseShortcode( content );
</span><span class="cx">  };
</span></span></pre></div>
<a id="trunksrcwpincludesmediatemplatephp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/media-template.php (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/media-template.php 2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/media-template.php    2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -500,6 +500,117 @@
</span><span class="cx">                  }
</span><span class="cx">          </style>
</span><span class="cx">  </script>
</span><ins>+
+       <script type="text/html" id="tmpl-image-details">
+               <?php // reusing .media-embed to pick up the styles for now ?>
+               <div class="media-embed">
+                       <div class="embed-image-settings">
+                               <div class="thumbnail">
+                                       <img src="{{ data.model.url }}" draggable="false" />
+                               </div>
+
+                               <div class="setting url">
+                                       <?php // might want to make the url editable if it isn't an attachment ?>
+                                       <input type="text" disabled="disabled" value="{{ data.model.url }}" />
+                               </div>
+
+                               <?php
+                               /** This filter is documented in wp-admin/includes/media.php */
+                               if ( ! apply_filters( 'disable_captions', '' ) ) : ?>
+                                       <label class="setting caption">
+                                               <span><?php _e('Caption'); ?></span>
+                                               <textarea data-setting="caption">{{ data.model.caption }}</textarea>
+                                       </label>
+                               <?php endif; ?>
+
+                               <label class="setting alt-text">
+                                       <span><?php _e('Alt Text'); ?></span>
+                                       <input type="text" data-setting="alt" value="{{ data.model.alt }}" />
+                               </label>
+
+                               <div class="setting align">
+                                       <span><?php _e('Align'); ?></span>
+                                       <div class="button-group button-large" data-setting="align">
+                                               <button class="button" value="left">
+                                                       <?php esc_attr_e('Left'); ?>
+                                               </button>
+                                               <button class="button" value="center">
+                                                       <?php esc_attr_e('Center'); ?>
+                                               </button>
+                                               <button class="button" value="right">
+                                                       <?php esc_attr_e('Right'); ?>
+                                               </button>
+                                               <button class="button active" value="none">
+                                                       <?php esc_attr_e('None'); ?>
+                                               </button>
+                                       </div>
+                               </div>
+                               <div class="setting link-to">
+                               <span><?php _e('Link To'); ?></span>
+
+                               <# if ( data.attachment ) { #>
+                                       <div class="button-group button-large" data-setting="link">
+                                               <button class="button" value="file">
+                                                       <?php esc_attr_e('Media File'); ?>
+                                               </button>
+                                               <button class="button" value="post">
+                                                       <?php esc_attr_e('Attachment Page'); ?>
+                                               </button>
+                                               <button class="button" value="custom">
+                                                       <?php esc_attr_e('Custom URL'); ?>
+                                               </button>
+                                               <button class="button active" value="none">
+                                                       <?php esc_attr_e('None'); ?>
+                                               </button>
+                                       </div>
+                                       <input type="text" class="link-to-custom" data-setting="linkUrl" />
+
+                               <# } else { #>
+                                       <div class="button-group button-large" data-setting="link">
+                                               <button class="button" value="file">
+                                                       <?php esc_attr_e('Image URL'); ?>
+                                               </button>
+                                               <button class="button" value="custom">
+                                                       <?php esc_attr_e('Custom URL'); ?>
+                                               </button>
+                                               <button class="button active" value="none">
+                                                       <?php esc_attr_e('None'); ?>
+                                               </button>
+                                       </div>
+                                       <input type="text" class="link-to-custom" data-setting="linkUrl" />
+
+                               <# } #>
+                               </div>
+
+                               <# if ( data.attachment ) { #>
+                                       <div class="setting size">
+                                               <span><?php _e('Size'); ?></span>
+                                               <div class="button-group button-large" data-setting="size">
+                                               <?php
+                                                       /** This filter is documented in wp-admin/includes/media.php */
+                                                       $sizes = apply_filters( 'image_size_names_choose', array(
+                                                               'thumbnail' => __('Thumbnail'),
+                                                               'medium'    => __('Medium'),
+                                                               'large'     => __('Large'),
+                                                               'full'      => __('Full Size'),
+                                                       ) );
+
+                                                       foreach ( $sizes as $value => $name ) : ?>
+                                                               <#
+                                                               var size = data.attachment.sizes['<?php echo esc_js( $value ); ?>'];
+                                                               if ( size ) { #>
+                                                                       <button class="button" value="<?php echo esc_attr( $value ); ?>">
+                                                                               <?php echo esc_html( $name ); ?>
+                                                                               </button>
+                                                               <# } #>
+                                                       <?php endforeach; ?>
+                                               </div>
+                                       </div>
+                               <# } #>
+                               </div>
+                       </div>
+               </div>
+       </script>
</ins><span class="cx">   <?php
</span><span class="cx"> 
</span><span class="cx">  /**
</span></span></pre></div>
<a id="trunksrcwpincludesmediaphp"></a>
<div class="modfile"><h4>Modified: trunk/src/wp-includes/media.php (27049 => 27050)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/src/wp-includes/media.php  2014-01-28 08:25:00 UTC (rev 27049)
+++ trunk/src/wp-includes/media.php     2014-01-28 21:16:42 UTC (rev 27050)
</span><span class="lines">@@ -1967,6 +1967,8 @@
</span><span class="cx">          'search'      => __( 'Search' ),
</span><span class="cx">          'select'      => __( 'Select' ),
</span><span class="cx">          'cancel'      => __( 'Cancel' ),
</span><ins>+               'update'      => __( 'Update' ),
+               'replace'     => __( 'Replace' ),
</ins><span class="cx">           /* translators: This is a would-be plural string used in the media manager.
</span><span class="cx">             If there is not a word you can use in your language to avoid issues with the
</span><span class="cx">             lack of plural support here, turn it into "selected: %d" then translate it.
</span><span class="lines">@@ -2005,6 +2007,12 @@
</span><span class="cx">          'addToGallery'       => __( 'Add to gallery' ),
</span><span class="cx">          'addToGalleryTitle'  => __( 'Add to Gallery' ),
</span><span class="cx">          'reverseOrder'       => __( 'Reverse order' ),
</span><ins>+
+
+               // Edit Image
+               'imageDetailsTitle'     => __( 'Image Details' ),
+               'imageReplaceTitle'     => __( 'Replace Image' ),
+               'imageDetailsCancel'     => __( 'Cancel Edit' )
</ins><span class="cx">   );
</span><span class="cx"> 
</span><span class="cx">  $settings = apply_filters( 'media_view_settings', $settings, $post );
</span></span></pre>
</div>
</div>

</body>
</html>