From 0dd940e0c1890402bce8de610c436710c82d0f24 Mon Sep 17 00:00:00 2001 From: Daryl Koopersmith Date: Wed, 26 Sep 2012 14:12:54 +0000 Subject: [PATCH] First pass on TinyMCE attachment in-editor UI. * Adds in-editor UI for image attachments. Most of this UI should be able to migrate to all images in a future commit. * Removes the wpeditimage TinyMCE plugin from the default plugins array. * Add `wp.media.fit`, a helper method to constrain dimensions based upon a maximum width and/or height. * Add html attribute parsing and string generation utilities (currently stored in mce-views). * Calling `remove` on a TinyMCE views now ensures that the the parent and references are removed as well. * Fixes a bug where we weren't sending the full array of results to matches in wp.mce.view. see #21390, #21812, #21813. git-svn-id: https://develop.svn.wordpress.org/trunk@22012 602fd350-edb4-49c9-b593-d223f7449a82 --- wp-includes/class-wp-editor.php | 2 +- wp-includes/js/mce-view.js | 230 +++++++++++++++++- wp-includes/js/media-models.js | 39 +++ .../advanced/skins/wp_theme/content.css | 44 ++++ wp-includes/media.php | 12 + wp-includes/script-loader.php | 5 +- 6 files changed, 322 insertions(+), 10 deletions(-) diff --git a/wp-includes/class-wp-editor.php b/wp-includes/class-wp-editor.php index c781e0f2e7..f710d2282c 100644 --- a/wp-includes/class-wp-editor.php +++ b/wp-includes/class-wp-editor.php @@ -191,7 +191,7 @@ final class _WP_Editors { self::$baseurl = includes_url('js/tinymce'); self::$mce_locale = $mce_locale = ( '' == get_locale() ) ? 'en' : strtolower( substr(get_locale(), 0, 2) ); // only ISO 639-1 $no_captions = (bool) apply_filters( 'disable_captions', '' ); - $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wpeditimage', 'wpgallery', 'wplink', 'wpdialogs', 'wpview' ); + $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wpgallery', 'wplink', 'wpdialogs', 'wpview' ); $first_run = true; $ext_plugins = ''; diff --git a/wp-includes/js/mce-view.js b/wp-includes/js/mce-view.js index a42641f246..2cdc11330a 100644 --- a/wp-includes/js/mce-view.js +++ b/wp-includes/js/mce-view.js @@ -39,7 +39,7 @@ if ( typeof wp === 'undefined' ) content: match[0], options: { original: match[0], - results: _.toArray( arguments ) + results: match } }; } @@ -92,18 +92,52 @@ if ( typeof wp === 'undefined' ) // * `text` is a method that accepts an instance of the `view` // constructor and transforms it into a text representation. add: function( id, options ) { + var parent, remove, base, properties; + // Fetch the parent view or the default options. - var parent = options.extend ? wp.mce.view.get( options.extend ) : wp.mce.view.defaults; + parent = options.extend ? wp.mce.view.get( options.extend ) : wp.mce.view.defaults; // Extend the `options` object with the parent's properties. _.defaults( options, parent ); options.id = id; - // If the `view` provided was an object, automatically create - // a new `Backbone.View` constructor, using the parent's `view` - // constructor as a base. - if ( ! _.isFunction( options.view ) ) - options.view = parent.view.extend( options.view ); + // Create properties used to enhance the view for use in TinyMCE. + properties = { + // Ensure the wrapper element and references to the view are + // removed. Otherwise, removed views could randomly restore. + remove: function() { + delete instances[ this.el.id ]; + this.$el.parent().remove(); + + // Trigger the inherited `remove` method. + if ( remove ) + remove.apply( this, arguments ); + + return this; + } + }; + + // If the `view` provided was an object, use the parent's + // `view` constructor as a base. If a `view` constructor + // was provided, treat that as the base. + if ( _.isFunction( options.view ) ) { + base = options.view; + } else { + base = parent.view; + remove = options.view.remove; + _.defaults( properties, options.view ); + } + + // If there's a `remove` method on the `base` view that wasn't + // created by this method, inherit it. + if ( ! remove && ! base._mceview ) + remove = base.prototype.remove; + + // Automatically create the new `Backbone.View` constructor. + options.view = base.extend( properties, { + // Flag that the new view has been created by `wp.mce.view`. + _mceview: true + }); views[ id ] = options; }, @@ -234,7 +268,187 @@ if ( typeof wp === 'undefined' ) return instance && view ? view.text( instance ) : ''; }); - } + }, + + // Link any localized strings. + l10n: _.isUndefined( _wpMceViewL10n ) ? {} : _wpMceViewL10n }; }(jQuery)); + +// Default TinyMCE Views +// --------------------- +(function($){ + var mceview = wp.mce.view, + attrs; + + wp.html = _.extend( wp.html || {}, { + // ### Parse HTML attributes. + // + // Converts `content` to a set of parsed HTML attributes. + // Utilizes `wp.shortcode.attrs( content )`, which is a valid superset of + // the HTML attribute specification. Reformats the attributes into an + // object that contains the `attrs` with `key:value` mapping, and a record + // of the attributes that were entered using `empty` attribute syntax (i.e. + // with no value). + attrs: function( content ) { + var result, attrs; + + // If `content` ends in a slash, strip it. + if ( '/' === content[ content.length - 1 ] ) + content = content.slice( 0, -1 ); + + result = wp.shortcode.attrs( content ); + attrs = result.named; + + _.each( result.numeric, function( key ) { + if ( /\s/.test( key ) ) + return; + + attrs[ key ] = ''; + }); + + return attrs; + }, + + string: function( options ) { + var text = '<' + options.tag, + content = options.content || ''; + + _.each( options.attrs, function( value, attr ) { + text += ' ' + attr; + + // Use empty attribute notation where possible. + if ( '' === value ) + return; + + // Convert boolean values to strings. + if ( _.isBoolean( value ) ) + value = value ? 'true' : 'false'; + + text += '="' + value + '"'; + }); + + // Return the result if it is a self-closing tag. + if ( options.single ) + return text + ' />'; + + // Complete the opening tag. + text += '>'; + + // If `content` is an object, recursively call this function. + text += _.isObject( content ) ? wp.html.string( content ) : content; + + return text + ''; + } + }); + + mceview.add( 'attachment', { + pattern: new RegExp( '(?:]*)>)?]*class=(?:"[^"]*|\'[^\']*)\\bwp-image-(\\d+)[^>]*)>(?:)?' ), + + text: function( instance ) { + var img = _.clone( instance.img ), + classes = img['class'].split(/\s+/), + options; + + // Update `img` classes. + if ( instance.align ) + classes.push( 'align' + instance.align ); + + if ( instance.size ) + classes.push( 'size-' + instance.size ); + + classes.push( 'wp-image-' + instance.model.id ); + + img['class'] = _.compact( classes ).join(' '); + + // Generate `img` tag options. + options = { + tag: 'img', + attrs: img, + single: true + }; + + // Generate the `a` element options, if they exist. + if ( instance.anchor ) { + options = { + tag: 'a', + attrs: instance.anchor, + content: options + }; + } + + return wp.html.string( options ); + }, + + view: { + className: 'editor-attachment', + template: media.template('editor-attachment'), + + events: { + 'click .close': 'remove' + }, + + initialize: function() { + var view = this, + results = this.options.results, + id = results[3], + className; + + this.model = wp.media.model.Attachment.get( id ); + + if ( results[1] ) + this.anchor = wp.html.attrs( results[1] ); + + this.img = wp.html.attrs( results[2] ); + className = this.img['class']; + + // Strip ID class. + className = className.replace( /(?:^|\s)wp-image-\d+/, '' ); + + // Calculate thumbnail `size` and remove class. + className = className.replace( /(?:^|\s)size-(\S+)/, function( match, size ) { + view.size = size; + return ''; + }); + + // Calculate `align` and remove class. + className = className.replace( /(?:^|\s)align(left|center|right|none)(?:\s|$)/, function( match, align ) { + view.align = align; + return ''; + }); + + this.img['class'] = className; + + this.$el.addClass('spinner'); + this.model.fetch().done( _.bind( this.render, this ) ); + }, + + render: function() { + var attachment = this.model.toJSON(), + options; + + // If we don't have the attachment data, bail. + if ( ! attachment.url ) + return; + + options = { + url: 'image' === attachment.type ? attachment.url : attachment.icon, + uploading: attachment.uploading + }; + + _.extend( options, wp.media.fit({ + width: attachment.width, + height: attachment.height, + maxWidth: mceview.l10n.contentWidth + }) ); + + // Use the specified size if it exists. + if ( this.size && attachment.sizes && attachment.sizes[ this.size ] ) + _.extend( options, _.pick( attachment.sizes[ this.size ], 'url', 'width', 'height' ) ); + + this.$el.html( this.template( options ) ); + } + } + }); +}(jQuery)); \ No newline at end of file diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index db103639b0..8f5a412a6a 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -121,6 +121,45 @@ if ( typeof wp === 'undefined' ) deferred.rejectWith( this, arguments ); }); }).promise(); + }, + + // Scales a set of dimensions to fit within bounding dimensions. + fit: function( dimensions ) { + var width = dimensions.width, + height = dimensions.height, + maxWidth = dimensions.maxWidth, + maxHeight = dimensions.maxHeight, + constraint; + + // Compare ratios between the two values to determine which + // max to constrain by. If a max value doesn't exist, then the + // opposite side is the constraint. + if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { + constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; + } else if ( _.isUndefined( maxHeight ) ) { + constraint = 'width'; + } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { + constraint = 'height'; + } + + // If the value of the constrained side is larger than the max, + // then scale the values. Otherwise return the originals; they fit. + if ( 'width' === constraint && width > maxWidth ) { + return { + width : maxWidth, + height: maxWidth * height / width + }; + } else if ( 'height' === constraint && height > maxHeight ) { + return { + width : maxHeight * width / height, + height: maxHeight + }; + } else { + return { + width : width, + height: height + }; + } } }); diff --git a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css index 807cf9b05d..8d6fd1e44a 100644 --- a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css +++ b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css @@ -146,3 +146,47 @@ div.wp-view-wrap, div.wp-view { display: inline-block; } + +.spinner { + background: #fff url("../../../../../../../wp-admin/images/wpspin_light.gif") no-repeat center center; + border: 1px solid #dfdfdf; + margin-top: 10px; + margin-right: 10px; +} + +.editor-attachment { + position: relative; + padding: 5px; +} + +.editor-attachment, +.editor-attachment img { + min-height: 100px; + min-width: 100px; +} + +.editor-attachment img { + display: block; + border: 0; + padding: 0; + margin: 0; +} + +.close { + display: none; + position: absolute; + top: 0; + right: 0; + height: 26px; + width: 26px; + font-size: 26px; + line-height: 22px; + text-align: center; + cursor: pointer; + color: #464646; + background: #fff; +} + +.editor-attachment:hover .close { + display: block; +} \ No newline at end of file diff --git a/wp-includes/media.php b/wp-includes/media.php index eebb27b269..662678aae1 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1414,5 +1414,17 @@ function wp_print_media_templates( $attachment ) { + + add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 ); - $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'backbone', 'jquery' ), false, 1 ); + $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'media-models' ), false, 1 ); + did_action( 'init' ) && $scripts->localize( 'mce-view', '_wpMceViewL10n', array( + 'contentWidth' => isset( $GLOBALS['content_width'] ) ? $GLOBALS['content_width'] : 800, + ) ); if ( is_admin() ) { $scripts->add( 'ajaxcat', "/wp-admin/js/cat$suffix.js", array( 'wp-lists' ) );