Make WordPress Core

Ticket #26959: 26959-06.patch

File 26959-06.patch, 35.6 KB (added by gcorne, 11 years ago)
  • src/wp-includes/class-wp-editor.php

    diff --git src/wp-includes/class-wp-editor.php src/wp-includes/class-wp-editor.php
    index b02b1ad..a49b776 100644
    final class _WP_Editors { 
    239239                                                'fullscreen',
    240240                                                'wordpress',
    241241                                                'wpeditimage',
    242                                                 'wpgallery',
    243242                                                'wplink',
    244243                                                'wpdialogs',
     244                                                'wpview'
    245245                                        ) ) );
    246246
    247247                                        if ( ( $key = array_search( 'spellchecker', $plugins ) ) !== false ) {
    final class _WP_Editors { 
    501501                if ( self::$has_medialib ) {
    502502                        add_thickbox();
    503503                        wp_enqueue_script('media-upload');
     504
     505                        if ( self::$has_tinymce )
     506                                wp_enqueue_script('mce-view');
    504507                }
     508
    505509        }
    506510
    507511        public static function wp_mce_translation() {
  • src/wp-includes/js/mce-view.js

    diff --git src/wp-includes/js/mce-view.js src/wp-includes/js/mce-view.js
    index 912c4c7..0881680 100644
     
     1/* global tinymce */
     2
    13// Ensure the global `wp` object exists.
    24window.wp = window.wp || {};
    35
    46(function($){
    57        var views = {},
    6                 instances = {};
     8                instances = {},
     9                media = wp.media,
     10                viewOptions = ['encodedText'];
    711
    812        // Create the `wp.mce` object if necessary.
    913        wp.mce = wp.mce || {};
    1014
    11         // wp.mce.view
    12         // -----------
    13         // A set of utilities that simplifies adding custom UI within a TinyMCE editor.
    14         // At its core, it serves as a series of converters, transforming text to a
    15         // custom UI, and back again.
    16         wp.mce.view = {
    17                 // ### defaults
    18                 defaults: {
    19                         // The default properties used for objects with the `pattern` key in
    20                         // `wp.mce.view.add()`.
    21                         pattern: {
    22                                 view: Backbone.View,
    23                                 text: function( instance ) {
    24                                         return instance.options.original;
    25                                 },
    26 
    27                                 toView: function( content ) {
    28                                         if ( ! this.pattern )
    29                                                 return;
    30 
    31                                         this.pattern.lastIndex = 0;
    32                                         var match = this.pattern.exec( content );
    33 
    34                                         if ( ! match )
    35                                                 return;
    36 
    37                                         return {
    38                                                 index:   match.index,
    39                                                 content: match[0],
    40                                                 options: {
    41                                                         original: match[0],
    42                                                         results:  match
    43                                                 }
    44                                         };
    45                                 }
    46                         },
    47 
    48                         // The default properties used for objects with the `shortcode` key in
    49                         // `wp.mce.view.add()`.
    50                         shortcode: {
    51                                 view: Backbone.View,
    52                                 text: function( instance ) {
    53                                         return instance.options.shortcode.string();
    54                                 },
    55 
    56                                 toView: function( content ) {
    57                                         var match = wp.shortcode.next( this.shortcode, content );
    58 
    59                                         if ( ! match )
    60                                                 return;
    61 
    62                                         return {
    63                                                 index:   match.index,
    64                                                 content: match.content,
    65                                                 options: {
    66                                                         shortcode: match.shortcode
    67                                                 }
    68                                         };
    69                                 }
    70                         }
    71                 },
     15        /**
     16         * wp.mce.View
     17         *
     18         * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
     19         * that the TinyMCE View is not tied to a particular DOM node.
     20         */
     21        wp.mce.View = function( options ) {
     22                options || (options = {});
     23                _.extend(this, _.pick(options, viewOptions));
     24                this.initialize.apply(this, arguments);
     25        };
    7226
    73                 // ### add( id, options )
    74                 // Registers a new TinyMCE view.
    75                 //
    76                 // Accepts a unique `id` and an `options` object.
    77                 //
    78                 // `options` accepts the following properties:
    79                 //
    80                 // * `pattern` is the regular expression used to scan the content and
    81                 // detect matching views.
    82                 //
    83                 // * `view` is a `Backbone.View` constructor. If a plain object is
    84                 // provided, it will automatically extend the parent constructor
    85                 // (usually `Backbone.View`). Views are instantiated when the `pattern`
    86                 // is successfully matched. The instance's `options` object is provided
    87                 // with the `original` matched value, the match `results` including
    88                 // capture groups, and the `viewType`, which is the constructor's `id`.
    89                 //
    90                 // * `extend` an existing view by passing in its `id`. The current
    91                 // view will inherit all properties from the parent view, and if
    92                 // `view` is set to a plain object, it will extend the parent `view`
    93                 // constructor.
    94                 //
    95                 // * `text` is a method that accepts an instance of the `view`
    96                 // constructor and transforms it into a text representation.
    97                 add: function( id, options ) {
    98                         var parent, remove, base, properties;
    99 
    100                         // Fetch the parent view or the default options.
    101                         if ( options.extend )
    102                                 parent = wp.mce.view.get( options.extend );
    103                         else if ( options.shortcode )
    104                                 parent = wp.mce.view.defaults.shortcode;
    105                         else
    106                                 parent = wp.mce.view.defaults.pattern;
    107 
    108                         // Extend the `options` object with the parent's properties.
    109                         _.defaults( options, parent );
    110                         options.id = id;
    111 
    112                         // Create properties used to enhance the view for use in TinyMCE.
    113                         properties = {
    114                                 // Ensure the wrapper element and references to the view are
    115                                 // removed. Otherwise, removed views could randomly restore.
    116                                 remove: function() {
    117                                         delete instances[ this.el.id ];
    118                                         this.$el.parent().remove();
    119 
    120                                         // Trigger the inherited `remove` method.
    121                                         if ( remove )
    122                                                 remove.apply( this, arguments );
    123 
    124                                         return this;
     27        _.extend( wp.mce.View.prototype, {
     28                initialize: function() {},
     29                html: function() {},
     30                render: function() {
     31                        var html = this.getHtml();
     32                        // Search all tinymce editor instances and update the placeholders
     33                        _.each( tinymce.editors, function( editor ) {
     34                                var doc;
     35                                if ( editor.plugins.wpview ) {
     36                                        doc = editor.getDoc();
     37                                        $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).html( html );
    12538                                }
    126                         };
    127 
    128                         // If the `view` provided was an object, use the parent's
    129                         // `view` constructor as a base. If a `view` constructor
    130                         // was provided, treat that as the base.
    131                         if ( _.isFunction( options.view ) ) {
    132                                 base = options.view;
    133                         } else {
    134                                 base   = parent.view;
    135                                 remove = options.view.remove;
    136                                 _.defaults( properties, options.view );
    137                         }
    138 
    139                         // If there's a `remove` method on the `base` view that wasn't
    140                         // created by this method, inherit it.
    141                         if ( ! remove && ! base._mceview )
    142                                 remove = base.prototype.remove;
    143 
    144                         // Automatically create the new `Backbone.View` constructor.
    145                         options.view = base.extend( properties, {
    146                                 // Flag that the new view has been created by `wp.mce.view`.
    147                                 _mceview: true
    148                         });
    149 
    150                         views[ id ] = options;
     39                        }, this );
     40                }
     41        } );
     42
     43        // take advantage of the Backbone extend method
     44        wp.mce.View.extend = Backbone.View.extend;
     45
     46        /**
     47         * wp.mce.views
     48         *
     49         * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
     50         * At its core, it serves as a series of converters, transforming text to a
     51         * custom UI, and back again.
     52         */
     53        wp.mce.views = {
     54
     55                /**
     56                 * wp.mce.views.register( type, view )
     57                 *
     58                 * Registers a new TinyMCE view.
     59                 *
     60                 * @param type
     61                 * @param constructor
     62                 *
     63                 */
     64                register: function( type, constructor ) {
     65                        views[ type ] = constructor;
    15166                },
    15267
    153                 // ### get( id )
    154                 // Returns a TinyMCE view options object.
    155                 get: function( id ) {
    156                         return views[ id ];
     68                /**
     69                 * wp.mce.views.get( id )
     70                 *
     71                 * Returns a TinyMCE view constructor.
     72                 */
     73                get: function( type ) {
     74                        return views[ type ];
    15775                },
    15876
    159                 // ### remove( id )
    160                 // Unregisters a TinyMCE view.
    161                 remove: function( id ) {
    162                         delete views[ id ];
     77                /**
     78                 * wp.mce.views.unregister( type )
     79                 *
     80                 * Unregisters a TinyMCE view.
     81                 */
     82                unregister: function( type ) {
     83                        delete views[ type ];
    16384                },
    16485
    165                 // ### toViews( content )
    166                 // Scans a `content` string for each view's pattern, replacing any
    167                 // matches with wrapper elements, and creates a new view instance for
    168                 // every match.
    169                 //
    170                 // To render the views, call `wp.mce.view.render( scope )`.
     86                /**
     87                 * toViews( content )
     88                 * Scans a `content` string for each view's pattern, replacing any
     89                 * matches with wrapper elements, and creates a new instance for
     90                 * every match, which triggers the related data to be fetched.
     91                 *
     92                 */
    17193                toViews: function( content ) {
    17294                        var pieces = [ { content: content } ],
    17395                                current;
    window.wp = window.wp || {}; 
    190112                                        // and slicing the string as we go.
    191113                                        while ( remaining && (result = view.toView( remaining )) ) {
    192114                                                // Any text before the match becomes an unprocessed piece.
    193                                                 if ( result.index )
     115                                                if ( result.index ) {
    194116                                                        pieces.push({ content: remaining.substring( 0, result.index ) });
     117                                                }
    195118
    196119                                                // Add the processed piece for the match.
    197120                                                pieces.push({
    198                                                         content:   wp.mce.view.toView( viewType, result.options ),
     121                                                        content: wp.mce.views.toView( viewType, result.content, result.options ),
    199122                                                        processed: true
    200123                                                });
    201124
    window.wp = window.wp || {}; 
    205128
    206129                                        // There are no additional matches. If any content remains,
    207130                                        // add it as an unprocessed piece.
    208                                         if ( remaining )
     131                                        if ( remaining ) {
    209132                                                pieces.push({ content: remaining });
     133                                        }
    210134                                });
    211135                        });
    212136
    213137                        return _.pluck( pieces, 'content' ).join('');
    214138                },
    215139
    216                 toView: function( viewType, options ) {
    217                         var view = wp.mce.view.get( viewType ),
    218                                 instance, id;
    219 
    220                         if ( ! view )
    221                                 return '';
    222 
    223                         // Create a new view instance.
    224                         instance = new view.view( _.extend( options || {}, {
    225                                 viewType: viewType
    226                         }) );
    227 
    228                         // Use the view's `id` if it already exists. Otherwise,
    229                         // create a new `id`.
    230                         id = instance.el.id = instance.el.id || _.uniqueId('__wpmce-');
    231                         instances[ id ] = instance;
     140                /**
     141                 * Create a placeholder for a particular view type
     142                 *
     143                 * @param viewType
     144                 * @param text
     145                 * @param options
     146                 *
     147                 */
     148                toView: function( viewType, text, options ) {
     149                        var view = wp.mce.views.get( viewType ),
     150                                encodedText = window.encodeURIComponent( text ),
     151                                instance, viewOptions;
     152
     153
     154                        if ( ! view ) {
     155                                return text;
     156                        }
    232157
    233                         // Create a dummy `$wrapper` property to allow `$wrapper` to be
    234                         // called in the view's `render` method without a conditional.
    235                         instance.$wrapper = $();
     158                        if ( ! wp.mce.views.getInstance( encodedText ) ) {
     159                                viewOptions = options;
     160                                viewOptions.encodedText = encodedText;
     161                                instance = new view.View( viewOptions );
     162                                instances[ encodedText ] = instance;
     163                        }
    236164
    237165                        return wp.html.string({
    238                                 // If the view is a span, wrap it in a span.
    239                                 tag: 'span' === instance.tagName ? 'span' : 'div',
     166                                tag: 'div',
    240167
    241168                                attrs: {
    242                                         'class':           'wp-view-wrap wp-view-type-' + viewType,
    243                                         'data-wp-view':    id,
    244                                         'contenteditable': false
    245                                 }
    246                         });
    247                 },
    248 
    249                 // ### render( scope )
    250                 // Renders any view instances inside a DOM node `scope`.
    251                 //
    252                 // View instances are detected by the presence of wrapper elements.
    253                 // To generate wrapper elements, pass your content through
    254                 // `wp.mce.view.toViews( content )`.
    255                 render: function( scope ) {
    256                         $( '.wp-view-wrap', scope ).each( function() {
    257                                 var wrapper = $(this),
    258                                         view = wp.mce.view.instance( this );
    259 
    260                                 if ( ! view )
    261                                         return;
     169                                        'class': 'wpview-wrap wpview-type-' + viewType,
     170                                        'data-wpview-text': encodedText,
     171                                        'data-wpview-type': viewType,
     172                                        'contenteditable': 'false'
     173                                },
    262174
    263                                 // Link the real wrapper to the view.
    264                                 view.$wrapper = wrapper;
    265                                 // Render the view.
    266                                 view.render();
    267                                 // Detach the view element to ensure events are not unbound.
    268                                 view.$el.detach();
    269 
    270                                 // Empty the wrapper, attach the view element to the wrapper,
    271                                 // and add an ending marker to the wrapper to help regexes
    272                                 // scan the HTML string.
    273                                 wrapper.empty().append( view.el ).append('<span data-wp-view-end class="wp-view-end"></span>');
     175                                content: '\u00a0'
    274176                        });
    275177                },
    276178
    277                 // ### toText( content )
    278                 // Scans an HTML `content` string and replaces any view instances with
    279                 // their respective text representations.
    280                 toText: function( content ) {
    281                         return content.replace( /<(?:div|span)[^>]+data-wp-view="([^"]+)"[^>]*>.*?<span[^>]+data-wp-view-end[^>]*><\/span><\/(?:div|span)>/g, function( match, id ) {
    282                                 var instance = instances[ id ],
    283                                         view;
    284 
    285                                 if ( instance )
    286                                         view = wp.mce.view.get( instance.options.viewType );
     179                /**
     180                 * Refresh views after an update is made
     181                 *
     182                 * @param view {object} being refreshed
     183                 * @param text {string} textual representation of the view
     184                 */
     185                refreshView: function( view, text ) {
     186                        var encodedText = window.encodeURIComponent( text ),
     187                                viewOptions,
     188                                result, instance;
     189
     190                        instance = wp.mce.views.getInstance( encodedText );
     191
     192                        if ( ! instance ) {
     193                                result = view.toView( text );
     194                                viewOptions = result.options;
     195                                viewOptions.encodedText = encodedText;
     196                                instance = new view.View( viewOptions );
     197                                instances[ encodedText ] = instance;
     198                        }
    287199
    288                                 return instance && view ? view.text( instance ) : '';
    289                         });
     200                        wp.mce.views.render();
    290201                },
    291202
    292                 // ### Remove internal TinyMCE attributes.
    293                 removeInternalAttrs: function( attrs ) {
    294                         var result = {};
    295                         _.each( attrs, function( value, attr ) {
    296                                 if ( -1 === attr.indexOf('data-mce') )
    297                                         result[ attr ] = value;
    298                         });
    299                         return result;
     203                getInstance: function( encodedText ) {
     204                        return instances[ encodedText ];
    300205                },
    301206
    302                 // ### Parse an attribute string and removes internal TinyMCE attributes.
    303                 attrs: function( content ) {
    304                         return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
     207                /**
     208                 * render( scope )
     209                 *
     210                 * Renders any view instances inside a DOM node `scope`.
     211                 *
     212                 * View instances are detected by the presence of wrapper elements.
     213                 * To generate wrapper elements, pass your content through
     214                 * `wp.mce.view.toViews( content )`.
     215                 */
     216                render: function() {
     217                        _.each( instances, function( instance ) {
     218                                instance.render();
     219                        } );
    305220                },
    306221
    307                 // ### instance( scope )
    308                 //
    309                 // Accepts a MCE view wrapper `node` (i.e. a node with the
    310                 // `wp-view-wrap` class).
    311                 instance: function( node ) {
    312                         var id = $( node ).data('wp-view');
     222                edit: function( node ) {
     223                        var viewType = $( node ).data('wpview-type'),
     224                                view = wp.mce.views.get( viewType );
    313225
    314                         if ( id )
    315                                 return instances[ id ];
    316                 },
     226                        if ( view ) {
     227                                view.edit( node );
     228                        }
     229                }
     230        };
    317231
    318                 // ### Select a view.
    319                 //
    320                 // Accepts a MCE view wrapper `node` (i.e. a node with the
    321                 // `wp-view-wrap` class).
    322                 select: function( node ) {
    323                         var $node = $(node);
     232        wp.mce.gallery = {
     233                shortcode: 'gallery',
     234                toView:  function( content ) {
     235                        var match = wp.shortcode.next( this.shortcode, content );
    324236
    325                         // Bail if node is already selected.
    326                         if ( $node.hasClass('selected') )
     237                        if ( ! match ) {
    327238                                return;
     239                        }
    328240
    329                         $node.addClass('selected');
    330                         $( node.firstChild ).trigger('select');
     241                        return {
     242                                index:   match.index,
     243                                content: match.content,
     244                                options: {
     245                                        shortcode: match.shortcode
     246                                }
     247                        };
    331248                },
     249                View: wp.mce.View.extend({
     250                        className: 'editor-gallery',
     251                        template:  media.template('editor-gallery'),
     252
     253                        // The fallback post ID to use as a parent for galleries that don't
     254                        // specify the `ids` or `include` parameters.
     255                        //
     256                        // Uses the hidden input on the edit posts page by default.
     257                        postID: $('#post_ID').val(),
     258
     259                        initialize: function( options ) {
     260                                this.shortcode = options.shortcode;
     261                                this.fetch();
     262                        },
    332263
    333                 // ### Deselect a view.
    334                 //
    335                 // Accepts a MCE view wrapper `node` (i.e. a node with the
    336                 // `wp-view-wrap` class).
    337                 deselect: function( node ) {
    338                         var $node = $(node);
     264                        fetch: function() {
     265                                this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
     266                                this.attachments.more().done( _.bind( this.render, this ) );
     267                        },
    339268
    340                         // Bail if node is already selected.
    341                         if ( ! $node.hasClass('selected') )
    342                                 return;
     269                        getHtml: function() {
     270                                var attrs = this.shortcode.attrs.named,
     271                                        options;
     272
     273                                if ( ! this.attachments.length ) {
     274                                        return;
     275                                }
     276
     277                                options = {
     278                                        attachments: this.attachments.toJSON(),
     279                                        columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3
     280                                };
    343281
    344                         $node.removeClass('selected');
    345                         $( node.firstChild ).trigger('deselect');
     282                                return this.template( options );
     283
     284                        }
     285                }),
     286
     287                edit: function( node ) {
     288                        var gallery = wp.media.gallery,
     289                                self = this,
     290                                frame, data;
     291
     292                        data = window.decodeURIComponent( $( node ).data('wpview-text') );
     293                        frame = gallery.edit( data );
     294
     295                        frame.state('gallery-edit').on( 'update', function( selection ) {
     296                                var shortcode = gallery.shortcode( selection ).string();
     297                                $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
     298                                wp.mce.views.refreshView( self, shortcode );
     299                                frame.detach();
     300                        });
    346301                }
    347         };
    348302
    349 }(jQuery));
    350  No newline at end of file
     303        };
     304        wp.mce.views.register( 'gallery', wp.mce.gallery );
     305}(jQuery));
  • src/wp-includes/js/tinymce/plugins/wpview/plugin.js

    diff --git src/wp-includes/js/tinymce/plugins/wpview/plugin.js src/wp-includes/js/tinymce/plugins/wpview/plugin.js
    index 0c56ecb..a6b2640 100644
     
    22/**
    33 * WordPress View plugin.
    44 */
    5 
    6 (function() {
    7         var VK = tinymce.VK,
     5tinymce.PluginManager.add( 'wpview', function( editor ) {
     6        var selected,
     7                VK = tinymce.util.VK,
    88                TreeWalker = tinymce.dom.TreeWalker,
    9                 selected;
    10 
    11         tinymce.create('tinymce.plugins.wpView', {
    12                 init : function( editor ) {
    13                         var wpView = this;
     9                toRemove = false;
    1410
    15                         // Check if the `wp.mce` API exists.
    16                         if ( typeof wp === 'undefined' || ! wp.mce ) {
    17                                 return;
     11        function getParentView( node ) {
     12                while ( node && node.nodeName !== 'BODY' ) {
     13                        if ( isView( node ) ) {
     14                                return node;
    1815                        }
    1916
    20                         editor.on( 'PreInit', function() {
    21                                 // Add elements so we can set `contenteditable` to false.
    22                                 editor.schema.addValidElements('div[*],span[*]');
    23                         });
    24 
    25                         // When the editor's content changes, scan the new content for
    26                         // matching view patterns, and transform the matches into
    27                         // view wrappers. Since the editor's DOM is outdated at this point,
    28                         // we'll wait to render the views.
    29                         editor.on( 'BeforeSetContent', function( e ) {
    30                                 if ( ! e.content ) {
    31                                         return;
    32                                 }
     17                        node = node.parentNode;
     18                }
     19        }
     20
     21        function isView( node ) {
     22                return node && /\bwpview-wrap\b/.test( node.className );
     23        }
     24
     25        function createPadNode() {
     26                return editor.dom.create( 'p', { 'data-wpview-pad': 1 },
     27                        ( tinymce.Env.ie && tinymce.Env.ie < 11 ) ? '' : '<br data-mce-bogus="1" />' );
     28        }
     29
     30        /**
     31         * Get the text/shortcode string for a view.
     32         *
     33         * @param view The view wrapper's HTML id or node
     34         * @returns string The text/shoercode string of the view
     35         */
     36        function getViewText( view ) {
     37                view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
     38
     39                if ( view ) {
     40                        return window.decodeURIComponent( editor.dom.getAttrib( view, 'data-wpview-text' ) || '' );
     41                }
     42                return '';
     43        }
     44
     45        /**
     46         * Set the view's original text/shortcode string
     47         *
     48         * @param view The view wrapper's HTML id or node
     49         * @param text The text string to be set
     50         */
     51        function setViewText( view, text ) {
     52                view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
     53
     54                if ( view ) {
     55                        editor.dom.setAttrib( view, 'data-wpview-text', window.encodeURIComponent( text || '' ) );
     56                        return true;
     57                }
     58                return false;
     59        }
    3360
    34                                 e.content = wp.mce.view.toViews( e.content );
    35                         });
    36 
    37                         // When the editor's content has been updated and the DOM has been
    38                         // processed, render the views in the document.
    39                         editor.on( 'SetContent', function() {
    40                                 wp.mce.view.render( editor.getDoc() );
    41                         });
    42 
    43                         editor.on( 'init', function() {
    44                                 var selection = editor.selection;
    45                                 // When a view is selected, ensure content that is being pasted
    46                                 // or inserted is added to a text node (instead of the view).
    47                                 editor.on( 'BeforeSetContent', function() {
    48                                         var walker, target,
    49                                                 view = wpView.getParentView( selection.getNode() );
    50 
    51                                         // If the selection is not within a view, bail.
    52                                         if ( ! view ) {
    53                                                 return;
    54                                         }
     61        function _stop( event ) {
     62                event.stopPropagation();
     63        }
    5564
    56                                         // If there are no additional nodes or the next node is a
    57                                         // view, create a text node after the current view.
    58                                         if ( ! view.nextSibling || wpView.isView( view.nextSibling ) ) {
    59                                                 target = editor.getDoc().createTextNode('');
    60                                                 editor.dom.insertAfter( target, view );
     65        function select( viewNode ) {
     66                var clipboard,
     67                        dom = editor.dom;
    6168
    62                                         // Otherwise, find the next text node.
    63                                         } else {
    64                                                 walker = new TreeWalker( view.nextSibling, view.nextSibling );
    65                                                 target = walker.next();
    66                                         }
     69                // Bail if node is already selected.
     70                if ( viewNode === selected ) {
     71                        return;
     72                }
    6773
    68                                         // Select the `target` text node.
    69                                         selection.select( target );
    70                                         selection.collapse( true );
    71                                 });
    72 
    73                                 // When the selection's content changes, scan any new content
    74                                 // for matching views and immediately render them.
    75                                 //
    76                                 // Runs on paste and on inserting nodes/html.
    77                                 editor.on( 'SetContent', function( e ) {
    78                                         if ( ! e.context ) {
    79                                                 return;
    80                                         }
     74                deselect();
     75                selected = viewNode;
     76                dom.addClass( viewNode, 'selected' );
    8177
    82                                         var node = selection.getNode();
     78                clipboard = dom.create( 'div', {
     79                        'class': 'wpview-clipboard',
     80                        'contenteditable': 'true'
     81                }, getViewText( viewNode ) );
    8382
    84                                         if ( ! node.innerHTML ) {
    85                                                 return;
    86                                         }
     83                viewNode.appendChild( clipboard );
    8784
    88                                         node.innerHTML = wp.mce.view.toViews( node.innerHTML );
    89                                         wp.mce.view.render( node );
    90                                 });
    91                         });
     85                // Both of the following are necessary to prevent manipulating the selection/focus
     86                editor.dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop );
     87                editor.dom.bind( selected, 'beforedeactivate focusin focusout', _stop );
    9288
    93                         // When the editor's contents are being accessed as a string,
    94                         // transform any views back to their text representations.
    95                         editor.on( 'PostProcess', function( e ) {
    96                                 if ( ( ! e.get && ! e.save ) || ! e.content ) {
    97                                         return;
    98                                 }
     89                // select the hidden div
     90                editor.selection.select( clipboard, true );
     91        }
    9992
    100                                 e.content = wp.mce.view.toText( e.content );
    101                         });
     93        /**
     94         * Deselect a selected view and remove clipboard
     95         */
     96        function deselect() {
     97                var clipboard,
     98                        dom = editor.dom;
    10299
    103                         // Triggers when the selection is changed.
    104                         // Add the event handler to the top of the stack.
    105                         editor.on( 'NodeChange', function( e ) {
    106                                 var view = wpView.getParentView( e.element );
     100                if ( selected ) {
     101                        clipboard = editor.dom.select( '.wpview-clipboard', selected )[0];
     102                        dom.unbind( clipboard );
     103                        dom.remove( clipboard );
    107104
    108                                 // Update the selected view.
    109                                 if ( view ) {
    110                                         wpView.select( view );
     105                        dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop );
     106                        dom.removeClass( selected, 'selected' );
    111107
    112                                         // Prevent the selection from propagating to other plugins.
    113                                         return false;
     108                        editor.selection.select( selected.nextSibling );
     109                        editor.selection.collapse();
    114110
    115                                 // If we've clicked off of the selected view, deselect it.
    116                                 } else {
    117                                         wpView.deselect();
    118                                 }
    119                         });
     111                }
    120112
    121                         editor.on( 'keydown', function( event ) {
    122                                 var keyCode = event.keyCode,
    123                                         view, instance;
     113                selected = null;
     114        }
    124115
    125                                 // If a view isn't selected, let the event go on its merry way.
    126                                 if ( ! selected ) {
    127                                         return;
    128                                 }
     116        // Check if the `wp.mce` API exists.
     117        if ( typeof wp === 'undefined' || ! wp.mce ) {
     118                return;
     119        }
    129120
    130                                 // If the caret is not within the selected view, deselect the
    131                                 // view and bail.
    132                                 view = wpView.getParentView( editor.selection.getNode() );
    133                                 if ( view !== selected ) {
    134                                         wpView.deselect();
    135                                         return;
    136                                 }
     121        editor.on( 'BeforeAddUndo', function( event ) {
     122                if ( selected && ! toRemove ) {
     123                        event.preventDefault();
     124                }
     125        });
    137126
    138                                 // If delete or backspace is pressed, delete the view.
    139                                 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
    140                                         if ( (instance = wp.mce.view.instance( selected )) ) {
    141                                                 instance.remove();
    142                                                 wpView.deselect();
     127        // When the editor's content changes, scan the new content for
     128        // matching view patterns, and transform the matches into
     129        // view wrappers.
     130        editor.on( 'BeforeSetContent', function( e ) {
     131                if ( ! e.content ) {
     132                        return;
     133                }
     134
     135                e.content = wp.mce.views.toViews( e.content );
     136        });
     137
     138        // When the editor's content has been updated and the DOM has been
     139        // processed, render the views in the document.
     140        editor.on( 'SetContent', function( event ) {
     141                var body, padNode;
     142
     143                wp.mce.views.render();
     144
     145                // Add padding <p> if the noneditable node is last
     146                if ( event.load || ! event.set ) {
     147                        body = editor.getBody();
     148
     149                        if ( isView( body.lastChild ) ) {
     150                                padNode = createPadNode();
     151                                body.appendChild( padNode );
     152                                editor.selection.setCursorLocation( padNode, 0 );
     153                        }
     154                }
     155
     156        //      refreshEmptyContentNode();
     157        });
     158
     159        // Detect mouse down events that are adjacent to a view when a view is the first view or the last view
     160        editor.on( 'click', function( event ) {
     161                var body = editor.getBody(),
     162                        doc = editor.getDoc(),
     163                        scrollTop = doc.documentElement.scrollTop || body.scrollTop || 0,
     164                        x, y, firstNode, lastNode, padNode;
     165
     166                if ( event.target.nodeName === 'HTML' && ! event.metaKey && ! event.ctrlKey ) {
     167                        firstNode = body.firstChild;
     168                        lastNode = body.lastChild;
     169                        x = event.clientX;
     170                        y = event.clientY;
     171
     172                        if ( isView( firstNode ) && ( ( x < firstNode.offsetLeft && y < ( firstNode.offsetHeight - scrollTop ) ) ||
     173                                y < firstNode.offsetTop ) ) {
     174                                // detect events above or to the left of the first view
     175
     176                                padNode = createPadNode();
     177                                body.insertBefore( padNode, firstNode );
     178                        } else if ( isView( lastNode ) && ( x > ( lastNode.offsetLeft + lastNode.offsetWidth ) ||
     179                                ( ( scrollTop + y ) - ( lastNode.offsetTop + lastNode.offsetHeight ) ) > 0 ) ) {
     180                                // detect events to the right and below the last view
     181
     182                                padNode = createPadNode();
     183                                body.appendChild( padNode );
     184                        }
     185
     186                        if ( padNode ) {
     187                                editor.selection.setCursorLocation( padNode, 0 );
     188                        }
     189                }
     190        });
     191
     192        editor.on( 'init', function() {
     193                var selection = editor.selection;
     194                // When a view is selected, ensure content that is being pasted
     195                // or inserted is added to a text node (instead of the view).
     196                editor.on( 'BeforeSetContent', function() {
     197                        var walker, target,
     198                                view = getParentView( selection.getNode() );
     199
     200                        // If the selection is not within a view, bail.
     201                        if ( ! view ) {
     202                                return;
     203                        }
     204
     205                        if ( ! view.nextSibling || isView( view.nextSibling ) ) {
     206                                // If there are no additional nodes or the next node is a
     207                                // view, create a text node after the current view.
     208                                target = editor.getDoc().createTextNode('');
     209                                editor.dom.insertAfter( target, view );
     210                        } else {
     211                                // Otherwise, find the next text node.
     212                                walker = new TreeWalker( view.nextSibling, view.nextSibling );
     213                                target = walker.next();
     214                        }
     215
     216                        // Select the `target` text node.
     217                        selection.select( target );
     218                        selection.collapse( true );
     219                });
     220
     221                // When the selection's content changes, scan any new content
     222                // for matching views.
     223                //
     224                // Runs on paste and on inserting nodes/html.
     225                editor.on( 'SetContent', function( e ) {
     226                        if ( ! e.context ) {
     227                                return;
     228                        }
     229
     230                        var node = selection.getNode();
     231
     232                        if ( ! node.innerHTML ) {
     233                                return;
     234                        }
     235
     236                        node.innerHTML = wp.mce.views.toViews( node.innerHTML );
     237                });
     238
     239                editor.dom.bind( editor.getBody(), 'mousedown mouseup click', function( event ) {
     240                        var view = getParentView( event.target );
     241
     242                        // Contain clicks inside the view wrapper
     243                        if ( view ) {
     244                                event.stopPropagation();
     245
     246                                if ( event.type === 'click' ) {
     247                                        if ( ! event.metaKey && ! event.ctrlKey ) {
     248                                                if ( editor.dom.hasClass( event.target, 'edit' ) ) {
     249                                                        wp.mce.views.edit( view );
     250                                                } else if ( editor.dom.hasClass( event.target, 'remove' ) ) {
     251                                                        editor.dom.remove( view );
     252                                                }
    143253                                        }
    144254                                }
    145 
    146                                 // Let keypresses that involve the command or control keys through.
    147                                 // Also, let any of the F# keys through.
    148                                 if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
    149                                         return;
     255                                select( view );
     256                                // returning false stops the ugly bars from appearing in IE11 and stops the view being selected a a range in FF
     257                                // unfortunately, it also inhibits the dragging fo views to a new location
     258                                return false;
     259                        } else {
     260                                if ( event.type === 'click' ) {
     261                                        deselect();
    150262                                }
     263                        }
     264                });
    151265
    152                                 event.preventDefault();
    153                         });
    154                 },
     266        });
    155267
    156                 getParentView : function( node ) {
    157                         while ( node ) {
    158                                 if ( this.isView( node ) ) {
    159                                         return node;
    160                                 }
     268        editor.on( 'PreProcess', function( event ) {
     269                var dom = editor.dom;
    161270
    162                                 node = node.parentNode;
     271                // Remove empty padding nodes
     272                tinymce.each( dom.select( 'p[data-wpview-pad]', event.node ), function( node ) {
     273                        if ( dom.isEmpty( node ) ) {
     274                                dom.remove( node );
     275                        } else {
     276                                dom.setAttrib( node, 'data-wpview-pad', null );
     277                        }
     278                });
     279
     280                // Replace the wpview node with the wpview string/shortcode?
     281                tinymce.each( dom.select( 'div[data-wpview-text]', event.node ), function( node ) {
     282                        // Empty the wrap node
     283                /*      while ( node.firstChild ) {
     284                                node.removeChild( node.firstChild );
     285                        }*/
     286
     287                        if ( 'textContent' in node ) {
     288                                node.textContent = '';
     289                        } else {
     290                                node.innerText = '';
    163291                        }
    164                 },
    165292
    166                 isView : function( node ) {
    167                         return (/(?:^|\s)wp-view-wrap(?:\s|$)/).test( node.className );
    168                 },
     293                        // TODO: that makes all views into block tags (as we use <div>).
     294                        // Can use 'PostProcess' and toText() instead.
     295                        dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node );
     296                });
     297    });
    169298
    170                 select : function( view ) {
    171                         if ( view === selected ) {
    172                                 return;
     299        editor.on( 'keydown', function( event ) {
     300                var keyCode = event.keyCode,
     301                        view;
     302
     303                // If a view isn't selected, let the event go on its merry way.
     304                if ( ! selected ) {
     305                        return;
     306                }
     307
     308                // Let keypresses that involve the command or control keys through.
     309                // Also, let any of the F# keys through.
     310                if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
     311                        if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) {
     312                                toRemove = selected;
    173313                        }
     314                        return;
     315                }
    174316
    175                         this.deselect();
    176                         selected = view;
    177                         wp.mce.view.select( selected );
    178                 },
     317                // If the caret is not within the selected view, deselect the
     318                // view and bail.
     319                view = getParentView( editor.selection.getNode() );
    179320
    180                 deselect : function() {
    181                         if ( selected ) {
    182                                 wp.mce.view.deselect( selected );
     321                if ( view !== selected ) {
     322                        deselect();
     323                        return;
     324                }
     325
     326                // If delete or backspace is pressed, delete the view.
     327                if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
     328                        editor.dom.remove( selected );
     329                }
     330
     331                event.preventDefault();
     332        });
     333
     334        editor.on( 'keyup', function( event ) {
     335                var padNode,
     336                        keyCode = event.keyCode,
     337                        body = editor.getBody(),
     338                        range;
     339
     340                if ( toRemove ) {
     341                        editor.dom.remove( toRemove );
     342                        toRemove = false;
     343                }
     344
     345                if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
     346                        // Make sure there is padding if the last element is a view
     347                        if ( isView( body.lastChild ) ) {
     348                                padNode = createPadNode();
     349                                body.appendChild( padNode );
     350
     351                                if ( body.childNodes.length === 2 ) {
     352                                        editor.selection.setCursorLocation( padNode, 0 );
     353                                }
    183354                        }
    184355
    185                         selected = null;
     356                        range = editor.selection.getRng();
     357
     358                        // Allow an initial element in the document to be removed when it is before a view
     359                        if ( body.firstChild === range.startContainer && range.collapsed === true &&
     360                                        isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) {
     361
     362                                editor.dom.remove( range.startContainer );
     363                        }
    186364                }
    187365        });
    188366
    189         // Register plugin
    190         tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView );
    191 })();
     367        return {
     368                getViewText: getViewText,
     369                setViewText: setViewText
     370        };
     371});
  • src/wp-includes/js/tinymce/skins/wordpress/wp-content.css

    diff --git src/wp-includes/js/tinymce/skins/wordpress/wp-content.css src/wp-includes/js/tinymce/skins/wordpress/wp-content.css
    index 95f1f6c..afff0c8 100644
    img::selection { 
    198198        outline: 0;
    199199}
    200200
     201
     202/**
     203 * WP Views
     204 */
     205
     206/* IE hasLayout. Needed for all IE incl. 11 (ugh, not again!!) */
     207.wpview-wrap {
     208        width: 99.99%;
     209        position: relative;
     210}
     211
     212/* delegate the handling of the selection to the wpview tinymce plugin */
     213.wpview-wrap,
     214.wpview-wrap * {
     215        -moz-user-select: none;
     216        -webkit-user-select: none;
     217        -ms-user-select: none;
     218        user-select: none;
     219}
     220
     221/* hide the shortcode content, but allow the content to still be selected */
     222.wpview-wrap .wpview-clipboard {
     223        position: absolute;
     224        top: 0;
     225        left: 0;
     226        z-index: -1;
     227        clip: rect(1px, 1px, 1px, 1px);
     228        overflow: hidden;
     229        outline: 0;
     230}
     231
     232/**
     233 * Gallery preview
     234 */
     235.wpview-type-gallery {
     236    position: relative;
     237    padding: 0 0 12px;
     238    margin-bottom: 16px;
     239        cursor: pointer;
     240}
     241
     242 .wpview-type-gallery:after {
     243    content: '';
     244    display: block;
     245    height: 0;
     246    clear: both;
     247    visibility: hidden;
     248}
     249
     250 .wpview-type-gallery.selected {
     251        background-color: #efefef;
     252}
     253
     254.wpview-type-gallery .toolbar {
     255    position: absolute;
     256    top: 0;
     257    left: 0;
     258    background-color: #333;
     259    color: white;
     260    padding: 4px;
     261        display: none;
     262}
     263
     264.wpview-type-gallery.selected .toolbar {
     265        display: block;
     266}
     267
     268.wpview-type-gallery .toolbar span {
     269        cursor: pointer;
     270}
     271
     272.gallery img[data-mce-selected]:focus {
     273        outline: none;
     274}
     275
     276.gallery a {
     277        cursor: default;
     278}
     279
     280.gallery {
     281        margin: auto;
     282    line-height: 1;
     283}
     284
     285.gallery .gallery-item {
     286        float: left;
     287        margin: 10px 0 0 0;
     288        text-align: center;
     289}
     290
     291.gallery .gallery-caption,
     292.gallery .gallery-icon {
     293        margin: 0;
     294}
     295
     296.gallery-columns-1 .gallery-item {
     297        width: 99%;
     298}
     299
     300.gallery-columns-2 .gallery-item {
     301        width: 49.5%;
     302}
     303
     304.gallery-columns-3 .gallery-item {
     305        width: 33%;
     306}
     307
     308.gallery-columns-4 .gallery-item {
     309        width: 24.75%;
     310}
     311
     312.gallery-columns-5 .gallery-item {
     313        width: 19.825%;
     314}
     315
     316.gallery-columns-6 .gallery-item {
     317        width: 16%;
     318}
     319
     320.gallery-columns-7 .gallery-item {
     321        width: 14%;
     322}
     323
     324.gallery-columns-8 .gallery-item {
     325        width: 12%;
     326}
     327
     328.gallery-columns-9 .gallery-item {
     329        width: 11%;
     330}
     331
     332.gallery img {
     333        border: 1px solid #cfcfcf;
     334}
     335
    201336img.wp-oembed {
    202337        border: 1px dashed #888;
    203338        background: #f7f5f2 url(images/embedded.png) no-repeat scroll center center;
  • src/wp-includes/media-template.php

    diff --git src/wp-includes/media-template.php src/wp-includes/media-template.php
    index 9078b34..345d41e 100644
    function wp_print_media_templates() { 
    650650        </script>
    651651        <?php
    652652
     653                //TODO: do we want to deal with the fact that the elements used for gallery items are filterable and can be overriden via shortcode attributes
     654                // do we want to deal with the difference between display and edit context at all? (e.g. wptexturize() being applied to the caption.
     655        ?>
     656
     657        <script type="text/html" id="tmpl-editor-gallery">
     658                <div class="toolbar">
     659                        <div class="dashicons dashicons-format-gallery edit"></div>
     660                        <div class="dashicons dashicons-no-alt remove"></div>
     661                </div>
     662                <div class="gallery gallery-columns-{{{ data.columns }}}">
     663                        <# _.each( data.attachments, function( attachment, index ) { #>
     664                                <dl class="gallery-item">
     665                                        <dt class="gallery-icon">
     666                                                <?php // TODO: need to figure out the best way to make sure that we have thumbnails ?>
     667                                                <img src="{{{ attachment.sizes.thumbnail.url }}}" />
     668                                        </dt>
     669                                        <dd class="wp-caption-text gallery-caption">
     670                                                {{ attachment.caption }}
     671                                        </dd>
     672                                </dl>
     673                                <?php // this is kind silly, but copied from the gallery shortcode. Maybe it should be removed ?>
     674                                <# if ( index % data.columns === data.columns - 1 ) { #>
     675                                        <br style="clear: both;">
     676                                <# } #>
     677
     678                        <# } ); #>
     679                </div>
     680        </script>
     681        <?php
     682
    653683        /**
    654684         * Prints the media manager custom media templates.
    655685         *