Make WordPress Core

Ticket #26959: 26959-05.patch

File 26959-05.patch, 35.1 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 004ec91..6d4c6dc 100644
    final class _WP_Editors { 
    235235                                                'fullscreen',
    236236                                                'wordpress',
    237237                                                'wpeditimage',
    238                                                 'wpgallery',
    239238                                                'wplink',
    240239                                                'wpdialogs',
     240                                                'wpview'
    241241                                        ) ) );
    242242
    243243                                        if ( ( $key = array_search( 'spellchecker', $plugins ) ) !== false ) {
    final class _WP_Editors { 
    497497                if ( self::$has_medialib ) {
    498498                        add_thickbox();
    499499                        wp_enqueue_script('media-upload');
     500
     501                        if ( self::$has_tinymce )
     502                                wp_enqueue_script('mce-view');
    500503                }
     504
    501505        }
    502506
    503507        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..9448714 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                 },
    72 
    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;
    125                                 }
    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                         });
     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 intended to be short-lived. Once the view is rendered, the View is destroyed rather than
     20         * being attached to the DOM and listening for either DOM event or Backbone events.
     21         */
     22        wp.mce.View = function( options ) {
     23                options || (options = {});
     24                _.extend(this, _.pick(options, viewOptions));
     25                this.initialize.apply(this, arguments);
     26        };
    14927
    150                         views[ id ] = options;
     28        _.extend( wp.mce.View.prototype, {
     29                initialize: function() {},
     30                render: function() {}
     31        } );
     32
     33        // take advantage of the Backbone extend method
     34        wp.mce.View.extend = Backbone.View.extend;
     35
     36        /**
     37         * wp.mce.views
     38         *
     39         * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
     40         * At its core, it serves as a series of converters, transforming text to a
     41         * custom UI, and back again.
     42         */
     43        wp.mce.views = {
     44
     45                /**
     46                 * wp.mce.views.register( type, view )
     47                 *
     48                 * Registers a new TinyMCE view.
     49                 *
     50                 * @param type
     51                 * @param constructor
     52                 *
     53                 */
     54                register: function( type, constructor ) {
     55                        views[ type ] = constructor;
    15156                },
    15257
    153                 // ### get( id )
    154                 // Returns a TinyMCE view options object.
    155                 get: function( id ) {
    156                         return views[ id ];
     58                /**
     59                 * wp.mce.views.get( id )
     60                 *
     61                 * Returns a TinyMCE view constructor.
     62                 */
     63                get: function( type ) {
     64                        return views[ type ];
    15765                },
    15866
    159                 // ### remove( id )
    160                 // Unregisters a TinyMCE view.
    161                 remove: function( id ) {
    162                         delete views[ id ];
     67                /**
     68                 * wp.mce.views.unregister( type )
     69                 *
     70                 * Unregisters a TinyMCE view.
     71                 */
     72                unregister: function( type ) {
     73                        delete views[ type ];
    16374                },
    16475
    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 )`.
     76                /**
     77                 * toViews( content )
     78                 * Scans a `content` string for each view's pattern, replacing any
     79                 * matches with wrapper elements, and creates a new instance for
     80                 * every match, which triggers the related data to be fetched.
     81                 *
     82                 */
    17183                toViews: function( content ) {
    17284                        var pieces = [ { content: content } ],
    17385                                current;
    window.wp = window.wp || {}; 
    190102                                        // and slicing the string as we go.
    191103                                        while ( remaining && (result = view.toView( remaining )) ) {
    192104                                                // Any text before the match becomes an unprocessed piece.
    193                                                 if ( result.index )
     105                                                if ( result.index ) {
    194106                                                        pieces.push({ content: remaining.substring( 0, result.index ) });
     107                                                }
    195108
    196109                                                // Add the processed piece for the match.
    197110                                                pieces.push({
    198                                                         content:   wp.mce.view.toView( viewType, result.options ),
     111                                                        content: wp.mce.views.toView( viewType, result.content, result.options ),
    199112                                                        processed: true
    200113                                                });
    201114
    window.wp = window.wp || {}; 
    205118
    206119                                        // There are no additional matches. If any content remains,
    207120                                        // add it as an unprocessed piece.
    208                                         if ( remaining )
     121                                        if ( remaining ) {
    209122                                                pieces.push({ content: remaining });
     123                                        }
    210124                                });
    211125                        });
    212126
    213127                        return _.pluck( pieces, 'content' ).join('');
    214128                },
    215129
    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;
     130                /**
     131                 * Create a placeholder for a particular view type
     132                 *
     133                 * @param viewType
     134                 * @param text
     135                 * @param options
     136                 *
     137                 */
     138                toView: function( viewType, text, options ) {
     139                        var view = wp.mce.views.get( viewType ),
     140                                encodedText = window.encodeURIComponent( text ),
     141                                instance, viewOptions;
     142
     143
     144                        if ( ! view ) {
     145                                return text;
     146                        }
    232147
    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 = $();
     148                        if ( ! wp.mce.views.getInstance( encodedText ) ) {
     149                                viewOptions = options;
     150                                viewOptions.encodedText = encodedText;
     151                                instance = new view.View( viewOptions );
     152                                instances[ encodedText ] = instance;
     153                        }
    236154
    237155                        return wp.html.string({
    238                                 // If the view is a span, wrap it in a span.
    239                                 tag: 'span' === instance.tagName ? 'span' : 'div',
     156                                tag: 'div',
    240157
    241158                                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;
     159                                        'class': 'wpview-wrap wpview-type-' + viewType,
     160                                        'data-wpview-text': encodedText,
     161                                        'data-wpview-type': viewType,
     162                                        'contenteditable': 'false'
     163                                },
    262164
    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>');
     165                                content: '\u00a0'
    274166                        });
    275167                },
    276168
    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 );
    287 
    288                                 return instance && view ? view.text( instance ) : '';
    289                         });
     169                /**
     170                 * Refresh view after an update is made
     171                 *
     172                 * @param view {object} being refreshed
     173                 * @param text {string} textual representation of the view
     174                 */
     175                refreshView: function( view, text ) {
     176                        var encodedText = window.encodeURIComponent( text ),
     177                                viewOptions,
     178                                result, instance;
     179
     180                        if ( ! wp.mce.views.getInstance( encodedText ) ) {
     181                                result = view.toView( text );
     182                                viewOptions = result.options;
     183                                viewOptions.encodedText = encodedText;
     184                                instance = new view.View( viewOptions );
     185                                instances[ encodedText ] = instance;
     186                        }
    290187                },
    291188
    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;
     189                getInstance: function( encodedText ) {
     190                        return instances[ encodedText ];
    300191                },
    301192
    302                 // ### Parse an attribute string and removes internal TinyMCE attributes.
    303                 attrs: function( content ) {
    304                         return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
     193                /**
     194                 * render( scope )
     195                 *
     196                 * Renders any view instances inside a DOM node `scope`.
     197                 *
     198                 * View instances are detected by the presence of wrapper elements.
     199                 * To generate wrapper elements, pass your content through
     200                 * `wp.mce.view.toViews( content )`.
     201                 */
     202                render: function() {
     203                        _.each( instances, function( instance ) {
     204                                instance.render();
     205                        } );
    305206                },
    306207
    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');
     208                edit: function( node ) {
     209                        var viewType = $( node ).data('wpview-type'),
     210                                view = wp.mce.views.get( viewType );
    313211
    314                         if ( id )
    315                                 return instances[ id ];
    316                 },
     212                        if ( view ) {
     213                                view.edit( node );
     214                        }
     215                }
     216        };
    317217
    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);
     218        wp.mce.gallery = {
     219                shortcode: 'gallery',
     220                toView:  function( content ) {
     221                        var match = wp.shortcode.next( this.shortcode, content );
    324222
    325                         // Bail if node is already selected.
    326                         if ( $node.hasClass('selected') )
     223                        if ( ! match ) {
    327224                                return;
     225                        }
    328226
    329                         $node.addClass('selected');
    330                         $( node.firstChild ).trigger('select');
     227                        return {
     228                                index:   match.index,
     229                                content: match.content,
     230                                options: {
     231                                        shortcode: match.shortcode
     232                                }
     233                        };
    331234                },
     235                View: wp.mce.View.extend({
     236                        className: 'editor-gallery',
     237                        template:  media.template('editor-gallery'),
     238
     239                        // The fallback post ID to use as a parent for galleries that don't
     240                        // specify the `ids` or `include` parameters.
     241                        //
     242                        // Uses the hidden input on the edit posts page by default.
     243                        postID: $('#post_ID').val(),
     244
     245                        initialize: function( options ) {
     246                                this.shortcode = options.shortcode;
     247                                this.fetch();
     248                        },
    332249
    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);
     250                        fetch: function() {
     251                                this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
     252                                this.attachments.more().done( _.bind( this.render, this ) );
     253                        },
    339254
    340                         // Bail if node is already selected.
    341                         if ( ! $node.hasClass('selected') )
    342                                 return;
     255                        render: function() {
     256                                var attrs = this.shortcode.attrs.named,
     257                                        options,
     258                                        html;
     259
     260                                if ( ! this.attachments.length ) {
     261                                        return;
     262                                }
     263
     264                                options = {
     265                                        attachments: this.attachments.toJSON(),
     266                                        columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3
     267                                };
     268
     269                                html = this.template( options );
    343270
    344                         $node.removeClass('selected');
    345                         $( node.firstChild ).trigger('deselect');
     271                                // Search all tinymce editor instances and update the placeholders
     272                                _.each( tinymce.editors, function( editor ) {
     273                                        var doc;
     274                                        if ( editor.plugins.wpview ) {
     275                                                doc = editor.getDoc();
     276                                                $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).html( html );
     277                                        }
     278                                }, this );
     279                        }
     280                }),
     281
     282                edit: function( node ) {
     283                        var gallery = wp.media.gallery,
     284                                self = this,
     285                                frame, data;
     286
     287                        data = window.decodeURIComponent( $( node ).data('wpview-text') );
     288                        frame = gallery.edit( data );
     289
     290                        frame.state('gallery-edit').on( 'update', function( selection ) {
     291                                var shortcode = gallery.shortcode( selection ).string();
     292                                $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
     293                                wp.mce.views.refreshView( self, shortcode );
     294                                frame.detach();
     295                        });
    346296                }
    347         };
    348297
    349 }(jQuery));
    350  No newline at end of file
     298        };
     299        wp.mce.views.register( 'gallery', wp.mce.gallery );
     300}(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..8f4293a 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        // @arg view can be either the view wrapper's HTML id or node
     32        /**
     33         * Get the text/shortcode string for a view.
     34         *
     35         * @param view The view wrapper's HTML id or node
     36         * @returns string The text/shoercode string of the view
     37         */
     38        function getViewText( view ) {
     39                view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
     40
     41                if ( view ) {
     42                        return window.decodeURIComponent( editor.dom.getAttrib( view, 'data-wpview-text' ) || '' );
     43                }
     44                return '';
     45        }
     46
     47        /**
     48         * Set the view's original text/shortcode string
     49         *
     50         * @param view The view wrapper's HTML id or node
     51         * @param text The text string to be set
     52         */
     53        function setViewText( view, text ) {
     54                view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
     55
     56                if ( view ) {
     57                        editor.dom.setAttrib( view, 'data-wpview-text', window.encodeURIComponent( text || '' ) );
     58                        return true;
     59                }
     60                return false;
     61        }
    3362
    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                                         }
     63        function _stop( event ) {
     64                event.stopPropagation();
     65        }
    5566
    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 );
     67        function select( viewNode ) {
     68                var clipboard,
     69                        dom = editor.dom;
    6170
    62                                         // Otherwise, find the next text node.
    63                                         } else {
    64                                                 walker = new TreeWalker( view.nextSibling, view.nextSibling );
    65                                                 target = walker.next();
    66                                         }
     71                // Bail if node is already selected.
     72                if ( viewNode === selected ) {
     73                        return;
     74                }
    6775
    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                                         }
     76                deselect();
     77                selected = viewNode;
     78                dom.addClass( viewNode, 'selected' );
    8179
    82                                         var node = selection.getNode();
     80                clipboard = dom.create( 'div', {
     81                        'class': 'wpview-clipboard',
     82                        'contenteditable': 'true'
     83                }, getViewText( viewNode ) );
    8384
    84                                         if ( ! node.innerHTML ) {
    85                                                 return;
    86                                         }
     85                viewNode.appendChild( clipboard );
    8786
    88                                         node.innerHTML = wp.mce.view.toViews( node.innerHTML );
    89                                         wp.mce.view.render( node );
    90                                 });
    91                         });
     87                // Both of the following are necessary to prevent manipulating the selection/focus
     88                editor.dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop );
     89                editor.dom.bind( selected, 'beforedeactivate focusin focusout', _stop );
    9290
    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                                 }
     91                // select the hidden div
     92                editor.selection.select( clipboard, true );
     93        }
    9994
    100                                 e.content = wp.mce.view.toText( e.content );
    101                         });
     95        // ### Deselect a view.
     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();
    143                                         }
    144                                 }
     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                }
    145134
    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;
    150                                 }
     135                e.content = wp.mce.views.toViews( e.content );
     136        });
    151137
    152                                 event.preventDefault();
    153                         });
    154                 },
     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;
    155142
    156                 getParentView : function( node ) {
    157                         while ( node ) {
    158                                 if ( this.isView( node ) ) {
    159                                         return node;
    160                                 }
     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                }
    161155
    162                                 node = node.parentNode;
     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 );
    163184                        }
    164                 },
    165185
    166                 isView : function( node ) {
    167                         return (/(?:^|\s)wp-view-wrap(?:\s|$)/).test( node.className );
    168                 },
     186                        if ( padNode ) {
     187                                editor.selection.setCursorLocation( padNode, 0 );
     188                        }
     189                }
     190        });
    169191
    170                 select : function( view ) {
    171                         if ( view === selected ) {
     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 ) {
    172202                                return;
    173203                        }
    174204
    175                         this.deselect();
    176                         selected = view;
    177                         wp.mce.view.select( selected );
    178                 },
     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                        }
    179215
    180                 deselect : function() {
    181                         if ( selected ) {
    182                                 wp.mce.view.deselect( selected );
     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                                                } else {
     253                                                        select( view );
     254                                                }
     255                                        }
     256                                }
     257                        } else {
     258                                if ( event.type === 'click' ) {
     259                                        deselect();
     260                                }
    183261                        }
     262                });
     263
     264        });
     265
     266        editor.on( 'PreProcess', function( event ) {
     267                var dom = editor.dom;
     268
     269                // Remove empty padding nodes
     270                tinymce.each( dom.select( 'p[data-wpview-pad]', event.node ), function( node ) {
     271                        if ( dom.isEmpty( node ) ) {
     272                                dom.remove( node );
     273                        } else {
     274                                dom.setAttrib( node, 'data-wpview-pad', null );
     275                        }
     276                });
     277
     278                // Replace the wpview node with the wpview string/shortcode?
     279                tinymce.each( dom.select( 'div[data-wpview-text]', event.node ), function( node ) {
     280                        // Empty the wrap node
     281                /*      while ( node.firstChild ) {
     282                                node.removeChild( node.firstChild );
     283                        }*/
     284
     285                        if ( 'textContent' in node ) {
     286                                node.textContent = '';
     287                        } else {
     288                                node.innerText = '';
     289                        }
     290
     291                        // TODO: that makes all views into block tags (as we use <div>).
     292                        // Can use 'PostProcess' and toText() instead.
     293                        dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node );
     294                });
     295    });
     296
     297        editor.on( 'keydown', function( event ) {
     298                var keyCode = event.keyCode,
     299                        view;
    184300
    185                         selected = null;
     301                // If a view isn't selected, let the event go on its merry way.
     302                if ( ! selected ) {
     303                        return;
     304                }
     305
     306                // Let keypresses that involve the command or control keys through.
     307                // Also, let any of the F# keys through.
     308                if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
     309                        if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) {
     310                                toRemove = selected;
     311                        }
     312                        return;
     313                }
     314
     315                // If the caret is not within the selected view, deselect the
     316                // view and bail.
     317                view = getParentView( editor.selection.getNode() );
     318
     319                if ( view !== selected ) {
     320                        deselect();
     321                        return;
     322                }
     323
     324                // If delete or backspace is pressed, delete the view.
     325                if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
     326                        editor.dom.remove( selected );
     327                }
     328
     329                event.preventDefault();
     330        });
     331
     332        editor.on( 'keyup', function( event ) {
     333                var padNode,
     334                        keyCode = event.keyCode,
     335                        body = editor.getBody();
     336
     337                if ( toRemove ) {
     338                        editor.dom.remove( toRemove );
     339                        toRemove = false;
     340                }
     341
     342                // Make sure there is padding if the last element is a view
     343                if ( ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) && isView( body.lastChild ) ) {
     344                        padNode = createPadNode();
     345                        body.appendChild( padNode );
     346
     347                        if ( body.childNodes.length === 2 ) {
     348                                editor.selection.setCursorLocation( padNode, 0 );
     349                        }
    186350                }
    187351        });
    188352
    189         // Register plugin
    190         tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView );
    191 })();
     353        return {
     354                getViewText: getViewText,
     355                setViewText: setViewText
     356        };
     357});
  • 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 0026e14..2f0309b 100644
    img::selection { 
    195195        outline: 0;
    196196}
    197197
     198
     199/**
     200 * WP Views
     201 */
     202
     203/* IE hasLayout. Needed for all IE incl. 11 (ugh, not again!!) */
     204.wpview-wrap {
     205        width: 99.99%;
     206        position: relative;
     207}
     208
     209/* delegate the handling of the selection to the wpview tinymce plugin */
     210.wpview-wrap,
     211.wpview-wrap * {
     212        -moz-user-select: none;
     213        -webkit-user-select: none;
     214        -ms-user-select: none;
     215        user-select: none;
     216}
     217
     218/* hide the shortcode content, but allow the content to still be selected */
     219.wpview-wrap .wpview-clipboard {
     220        position: absolute;
     221        top: 0;
     222        left: 0;
     223        z-index: -1;
     224        clip: rect(1px, 1px, 1px, 1px);
     225        overflow: hidden;
     226        outline: 0;
     227}
     228
     229/**
     230 * Gallery preview
     231 */
     232.wpview-type-gallery {
     233    position: relative;
     234    padding: 0 0 12px;
     235    margin-bottom: 16px;
     236        cursor: pointer;
     237}
     238
     239 .wpview-type-gallery:after {
     240    content: '';
     241    display: block;
     242    height: 0;
     243    clear: both;
     244    visibility: hidden;
     245}
     246
     247 .wpview-type-gallery.selected {
     248        background-color: #efefef;
     249}
     250
     251.wpview-type-gallery .toolbar {
     252    position: absolute;
     253    top: 0;
     254    left: 0;
     255    background-color: #333;
     256    color: white;
     257    padding: 4px;
     258        display: none;
     259}
     260
     261.wpview-type-gallery.selected .toolbar {
     262        display: block;
     263}
     264
     265.wpview-type-gallery .toolbar span {
     266        cursor: pointer;
     267}
     268
     269.gallery img[data-mce-selected]:focus {
     270        outline: none;
     271}
     272
     273.gallery a {
     274        cursor: default;
     275}
     276
     277.gallery {
     278        margin: auto;
     279    line-height: 1;
     280}
     281
     282.gallery .gallery-item {
     283        float: left;
     284        margin: 10px 0 0 0;
     285        text-align: center;
     286}
     287
     288.gallery .gallery-caption,
     289.gallery .gallery-icon {
     290        margin: 0;
     291}
     292
     293.gallery-columns-1 .gallery-item {
     294        width: 99%;
     295}
     296
     297.gallery-columns-2 .gallery-item {
     298        width: 49.5%;
     299}
     300
     301.gallery-columns-3 .gallery-item {
     302        width: 33%;
     303}
     304
     305.gallery-columns-4 .gallery-item {
     306        width: 24.75%;
     307}
     308
     309.gallery-columns-5 .gallery-item {
     310        width: 19.825%;
     311}
     312
     313.gallery-columns-6 .gallery-item {
     314        width: 16%;
     315}
     316
     317.gallery-columns-7 .gallery-item {
     318        width: 14%;
     319}
     320
     321.gallery-columns-8 .gallery-item {
     322        width: 12%;
     323}
     324
     325.gallery-columns-9 .gallery-item {
     326        width: 11%;
     327}
     328
     329.gallery img {
     330        border: 1px solid #cfcfcf;
     331}
     332
    198333img.wp-oembed {
    199334        border: 1px dashed #888;
    200335        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 91216ed..a6ee6f7 100644
    function wp_print_media_templates() { 
    644644        </script>
    645645        <?php
    646646
     647                //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
     648                // do we want to deal with the difference between display and edit context at all? (e.g. wptexturize() being applied to the caption.
     649        ?>
     650
     651        <script type="text/html" id="tmpl-editor-gallery">
     652                <div class="toolbar">
     653                        <div class="dashicons dashicons-format-gallery edit"></div>
     654                        <div class="dashicons dashicons-no-alt remove"></div>
     655                </div>
     656                <div class="gallery gallery-columns-{{{ data.columns }}}">
     657                        <# _.each( data.attachments, function( attachment, index ) { #>
     658                                <dl class="gallery-item">
     659                                        <dt class="gallery-icon">
     660                                                <?php // TODO: need to figure out the best way to make sure that we have thumbnails ?>
     661                                                <img src="{{{ attachment.sizes.thumbnail.url }}}" />
     662                                        </dt>
     663                                        <dd class="wp-caption-text gallery-caption">
     664                                                {{ attachment.caption }}
     665                                        </dd>
     666                                </dl>
     667                                <?php // this is kind silly, but copied from the gallery shortcode. Maybe it should be removed ?>
     668                                <# if ( index % data.columns === data.columns - 1 ) { #>
     669                                        <br style="clear: both;">
     670                                <# } #>
     671
     672                        <# } ); #>
     673                </div>
     674        </script>
     675        <?php
     676
    647677        /**
    648678         * Prints the media manager custom media templates.
    649679         *