diff --git a/src/wp-includes/class-wp-editor.php b/src/wp-includes/class-wp-editor.php
index dd59239751..fb36424093 100644
--- a/src/wp-includes/class-wp-editor.php
+++ b/src/wp-includes/class-wp-editor.php
@@ -228,7 +228,6 @@ final class _WP_Editors {
'paste',
'tabfocus',
'textcolor',
- 'image',
'fullscreen',
'wordpress',
'wpeditimage',
diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css
index 7fb7ffaf06..78a48ea274 100644
--- a/src/wp-includes/css/media-views.css
+++ b/src/wp-includes/css/media-views.css
@@ -86,6 +86,10 @@
color: #a9a9a9;
}
+.media-frame .hidden {
+ display: none;
+}
+
/* Enable draggable on IE10 touch events until it's rolled into jQuery UI core */
.ui-sortable,
.ui-draggable {
@@ -1411,7 +1415,7 @@
}
/**
- * Embed from URL
+ * Embed from URL and Image Details
*/
.embed-url {
display: block;
@@ -1452,6 +1456,10 @@
overflow: auto;
}
+.image-details .embed-image-settings {
+ top: 0;
+}
+
.media-embed .thumbnail {
max-width: 100%;
max-height: 200px;
@@ -1484,6 +1492,10 @@
clear: both;
}
+.media-embed .setting .hidden {
+ display: none;
+}
+
.media-embed .setting span {
display: block;
width: 200px;
@@ -1926,4 +1938,4 @@
.media-frame .spinner {
background-image: url('../images/spinner-2x.gif');
}
-}
\ No newline at end of file
+}
diff --git a/src/wp-includes/js/media-models.js b/src/wp-includes/js/media-models.js
index 2144f33d29..6ea947fa41 100644
--- a/src/wp-includes/js/media-models.js
+++ b/src/wp-includes/js/media-models.js
@@ -2,7 +2,7 @@
window.wp = window.wp || {};
(function($){
- var Attachment, Attachments, Query, compare, l10n, media;
+ var Attachment, Attachments, Query, PostImage, compare, l10n, media;
/**
* wp.media( attributes )
@@ -30,6 +30,8 @@ window.wp = window.wp || {};
frame = new MediaFrame.Select( attributes );
} else if ( 'post' === attributes.frame && MediaFrame.Post ) {
frame = new MediaFrame.Post( attributes );
+ } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) {
+ frame = new MediaFrame.ImageDetails( attributes );
}
delete attributes.frame;
@@ -339,6 +341,121 @@ window.wp = window.wp || {};
})
});
+ /**
+ * 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 );
+
+ }
+
+
+ });
+
/**
* wp.media.model.Attachments
*
@@ -1170,4 +1287,4 @@ window.wp = window.wp || {};
window.wp = null;
});
-}(jQuery));
\ No newline at end of file
+}(jQuery));
diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js
index 07b49c916c..3fa9ecd09b 100644
--- a/src/wp-includes/js/media-views.js
+++ b/src/wp-includes/js/media-views.js
@@ -980,6 +980,107 @@
}
});
+
+ 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 );
+ }
+ });
+
+ /**
+ * 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 ] : [] );
+
+ }
+
+
+ });
+
/**
* wp.media.controller.Embed
*
@@ -1924,8 +2025,157 @@
}
}) );
}
+
});
+ 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
+ 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
+ state.trigger( 'replace', controller.image.toJSON() );
+
+ // Restore and reset the default state.
+ controller.setState( controller.options.state );
+ controller.reset();
+ }
+ }
+ }
+ }) );
+ }
+
+ });
+
+
/**
* wp.media.view.Modal
*
@@ -4555,7 +4805,7 @@
attachment = this.options.attachment;
if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
- $input.hide();
+ $input.addClass( 'hidden' );
return;
}
@@ -4571,7 +4821,7 @@
$input.prop( 'readonly', 'custom' !== linkTo );
}
- $input.show();
+ $input.removeClass( 'hidden' );
// If the input is visible, focus and select its contents.
if ( $input.is(':visible') ) {
@@ -4932,4 +5182,50 @@
this.$('img').attr( 'src', this.model.get('url') );
}
});
-}(jQuery));
\ No newline at end of file
+
+ 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));
diff --git a/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js b/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js
index 3f4ca1036e..91392396ff 100644
--- a/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js
+++ b/src/wp-includes/js/tinymce/plugins/wpeditimage/plugin.js
@@ -101,6 +101,176 @@ tinymce.PluginManager.add( 'wpeditimage', function( editor ) {
});
}
+ 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 and the 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( /
]*>/g, '$&\n' ).replace( /^
/, '' ).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 = '
'; + + 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 ); + } + + } + } + editor.on( 'init', function() { var dom = editor.dom; @@ -452,6 +622,40 @@ tinymce.PluginManager.add( 'wpeditimage', function( editor ) { } }); + 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(); + + + } + } + } ); + editor.wpSetImgCaption = function( content ) { return parseShortcode( content ); }; diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 57d1c0087d..71e9c8282d 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -500,6 +500,117 @@ function wp_print_media_templates() { } + + __( 'Search' ), 'select' => __( 'Select' ), 'cancel' => __( 'Cancel' ), + 'update' => __( 'Update' ), + 'replace' => __( 'Replace' ), /* translators: This is a would-be plural string used in the media manager. If there is not a word you can use in your language to avoid issues with the lack of plural support here, turn it into "selected: %d" then translate it. @@ -2005,6 +2007,12 @@ function wp_enqueue_media( $args = array() ) { 'addToGallery' => __( 'Add to gallery' ), 'addToGalleryTitle' => __( 'Add to Gallery' ), 'reverseOrder' => __( 'Reverse order' ), + + + // Edit Image + 'imageDetailsTitle' => __( 'Image Details' ), + 'imageReplaceTitle' => __( 'Replace Image' ), + 'imageDetailsCancel' => __( 'Cancel Edit' ) ); $settings = apply_filters( 'media_view_settings', $settings, $post );