Make WordPress Core

Changeset 27408


Ignore:
Timestamp:
03/05/2014 07:00:18 AM (11 years ago)
Author:
azaozz
Message:

Update mce-view.js and the wpview TinyMCE plugin, and use them to show gallery previews in the Visual editor, props gcorne, see #26959

Location:
trunk/src/wp-includes
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-editor.php

    r27391 r27408  
    243243                        'wplink',
    244244                        'wpdialogs',
     245                        'wpview',
    245246                    ) ) );
    246247
     
    502503            add_thickbox();
    503504            wp_enqueue_script('media-upload');
     505
     506            if ( self::$has_tinymce )
     507                wp_enqueue_script('mce-view');
    504508        }
    505509    }
  • trunk/src/wp-includes/js/mce-view.js

    r22798 r27408  
     1/* global tinymce */
     2
    13// Ensure the global `wp` object exists.
    24window.wp = window.wp || {};
     
    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                     };
     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    };
     26
     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 );
    4538                }
    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             });
    149 
    150             views[ id ] = options;
    151         },
    152 
    153         // ### get( id )
    154         // Returns a TinyMCE view options object.
    155         get: function( id ) {
    156             return views[ id ];
    157         },
    158 
    159         // ### remove( id )
    160         // Unregisters a TinyMCE view.
    161         remove: function( id ) {
    162             delete views[ id ];
    163         },
    164 
    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 )`.
     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;
     66        },
     67
     68        /**
     69         * wp.mce.views.get( id )
     70         *
     71         * Returns a TinyMCE view constructor.
     72         */
     73        get: function( type ) {
     74            return views[ type ];
     75        },
     76
     77        /**
     78         * wp.mce.views.unregister( type )
     79         *
     80         * Unregisters a TinyMCE view.
     81         */
     82        unregister: function( type ) {
     83            delete views[ type ];
     84        },
     85
     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 } ],
     
    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                        });
     
    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            });
     
    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;
    232 
    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 = $();
     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            }
     157
     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                 }
     169                    'class': 'wpview-wrap wpview-type-' + viewType,
     170                    'data-wpview-text': encodedText,
     171                    'data-wpview-type': viewType,
     172                    'contenteditable': 'false'
     173                },
     174
     175                content: '\u00a0'
    246176            });
    247177        },
    248178
    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;
    262 
    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>');
    274             });
    275         },
    276 
    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             });
    290         },
    291 
    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;
    300         },
    301 
    302         // ### Parse an attribute string and removes internal TinyMCE attributes.
    303         attrs: function( content ) {
    304             return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
    305         },
    306 
    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');
    313 
    314             if ( id )
    315                 return instances[ id ];
    316         },
    317 
    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);
    324 
    325             // Bail if node is already selected.
    326             if ( $node.hasClass('selected') )
    327                 return;
    328 
    329             $node.addClass('selected');
    330             $( node.firstChild ).trigger('select');
    331         },
    332 
    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);
    339 
    340             // Bail if node is already selected.
    341             if ( ! $node.hasClass('selected') )
    342                 return;
    343 
    344             $node.removeClass('selected');
    345             $( node.firstChild ).trigger('deselect');
     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            }
     199
     200            wp.mce.views.render();
     201        },
     202
     203        getInstance: function( encodedText ) {
     204            return instances[ encodedText ];
     205        },
     206
     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            } );
     220        },
     221
     222        edit: function( node ) {
     223            var viewType = $( node ).data('wpview-type'),
     224                view = wp.mce.views.get( viewType );
     225
     226            if ( view ) {
     227                view.edit( node );
     228            }
    346229        }
    347230    };
    348231
     232    wp.mce.gallery = {
     233        shortcode: 'gallery',
     234        toView:  function( content ) {
     235            var match = wp.shortcode.next( this.shortcode, content );
     236
     237            if ( ! match ) {
     238                return;
     239            }
     240
     241            return {
     242                index:   match.index,
     243                content: match.content,
     244                options: {
     245                    shortcode: match.shortcode
     246                }
     247            };
     248        },
     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            },
     263
     264            fetch: function() {
     265                this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
     266                this.attachments.more().done( _.bind( this.render, this ) );
     267            },
     268
     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                };
     281
     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            });
     301        }
     302
     303    };
     304    wp.mce.views.register( 'gallery', wp.mce.gallery );
    349305}(jQuery));
  • trunk/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js

    r27276 r27408  
    6060        }
    6161
    62         // Check if the `wp.media.gallery` API exists.
     62        // Check if the `wp.media` API exists.
    6363        if ( typeof wp === 'undefined' || ! wp.media ) {
    6464            return;
     
    167167
    168168    editor.on( 'BeforeSetContent', function( event ) {
    169         event.content = replaceGalleryShortcodes( event.content );
     169        // 'wpview' handles the gallery shortcode when present
     170        if ( ! editor.plugins.wpview ) {
     171            event.content = replaceGalleryShortcodes( event.content );
     172        }
     173
    170174        event.content = replaceAVShortcodes( event.content );
    171175    });
  • trunk/src/wp-includes/js/tinymce/plugins/wpview/plugin.js

    r26880 r27408  
    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;
    14 
    15             // Check if the `wp.mce` API exists.
    16             if ( typeof wp === 'undefined' || ! wp.mce ) {
     9        toRemove = false;
     10
     11    function getParentView( node ) {
     12        while ( node && node.nodeName !== 'BODY' ) {
     13            if ( isView( node ) ) {
     14                return node;
     15            }
     16
     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    }
     60
     61    function _stop( event ) {
     62        event.stopPropagation();
     63    }
     64
     65    function select( viewNode ) {
     66        var clipboard,
     67            dom = editor.dom;
     68
     69        // Bail if node is already selected.
     70        if ( viewNode === selected ) {
     71            return;
     72        }
     73
     74        deselect();
     75        selected = viewNode;
     76        dom.addClass( viewNode, 'selected' );
     77
     78        clipboard = dom.create( 'div', {
     79            'class': 'wpview-clipboard',
     80            'contenteditable': 'true'
     81        }, getViewText( viewNode ) );
     82
     83        viewNode.appendChild( clipboard );
     84
     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 );
     88
     89        // select the hidden div
     90        editor.selection.select( clipboard, true );
     91    }
     92
     93    /**
     94     * Deselect a selected view and remove clipboard
     95     */
     96    function deselect() {
     97        var clipboard,
     98            dom = editor.dom;
     99
     100        if ( selected ) {
     101            clipboard = editor.dom.select( '.wpview-clipboard', selected )[0];
     102            dom.unbind( clipboard );
     103            dom.remove( clipboard );
     104
     105            dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop );
     106            dom.removeClass( selected, 'selected' );
     107
     108            editor.selection.select( selected.nextSibling );
     109            editor.selection.collapse();
     110
     111        }
     112
     113        selected = null;
     114    }
     115
     116    // Check if the `wp.mce` API exists.
     117    if ( typeof wp === 'undefined' || ! wp.mce ) {
     118        return;
     119    }
     120
     121    editor.on( 'BeforeAddUndo', function( event ) {
     122        if ( selected && ! toRemove ) {
     123            event.preventDefault();
     124        }
     125    });
     126
     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 ) {
    17202                return;
    18203            }
    19204
    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                 }
    33 
    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                     }
    55 
    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 );
    61 
    62                     // Otherwise, find the next text node.
    63                     } else {
    64                         walker = new TreeWalker( view.nextSibling, view.nextSibling );
    65                         target = walker.next();
    66                     }
    67 
    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                     }
    81 
    82                     var node = selection.getNode();
    83 
    84                     if ( ! node.innerHTML ) {
    85                         return;
    86                     }
    87 
    88                     node.innerHTML = wp.mce.view.toViews( node.innerHTML );
    89                     wp.mce.view.render( node );
    90                 });
    91             });
    92 
    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                 }
    99 
    100                 e.content = wp.mce.view.toText( e.content );
    101             });
    102 
    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 );
    107 
    108                 // Update the selected view.
    109                 if ( view ) {
    110                     wpView.select( view );
    111 
    112                     // Prevent the selection from propagating to other plugins.
    113                     return false;
    114 
    115                 // If we've clicked off of the selected view, deselect it.
    116                 } else {
    117                     wpView.deselect();
    118                 }
    119             });
    120 
    121             editor.on( 'keydown', function( event ) {
    122                 var keyCode = event.keyCode,
    123                     view, instance;
    124 
    125                 // If a view isn't selected, let the event go on its merry way.
    126                 if ( ! selected ) {
    127                     return;
    128                 }
    129 
    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                 }
    137 
    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();
     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 as 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                }
    151 
    152                 event.preventDefault();
    153             });
    154         },
    155 
    156         getParentView : function( node ) {
    157             while ( node ) {
    158                 if ( this.isView( node ) ) {
    159                     return node;
     263            }
     264        });
     265
     266    });
     267
     268    editor.on( 'PreProcess', function( event ) {
     269        var dom = editor.dom;
     270
     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            if ( 'textContent' in node ) {
     284                node.textContent = '';
     285            } else {
     286                node.innerText = '';
     287            }
     288
     289            // TODO: that makes all views into block tags (as we use <div>).
     290            // Can use 'PostProcess' and a regex instead.
     291            dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node );
     292        });
     293    });
     294
     295    editor.on( 'keydown', function( event ) {
     296        var keyCode = event.keyCode,
     297            view;
     298
     299        // If a view isn't selected, let the event go on its merry way.
     300        if ( ! selected ) {
     301            return;
     302        }
     303
     304        // Let keypresses that involve the command or control keys through.
     305        // Also, let any of the F# keys through.
     306        if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
     307            if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) {
     308                toRemove = selected;
     309            }
     310            return;
     311        }
     312
     313        // If the caret is not within the selected view, deselect the
     314        // view and bail.
     315        view = getParentView( editor.selection.getNode() );
     316
     317        if ( view !== selected ) {
     318            deselect();
     319            return;
     320        }
     321
     322        // If delete or backspace is pressed, delete the view.
     323        if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
     324            editor.dom.remove( selected );
     325        }
     326
     327        event.preventDefault();
     328    });
     329
     330    editor.on( 'keyup', function( event ) {
     331        var padNode,
     332            keyCode = event.keyCode,
     333            body = editor.getBody(),
     334            range;
     335
     336        if ( toRemove ) {
     337            editor.dom.remove( toRemove );
     338            toRemove = false;
     339        }
     340
     341        if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
     342            // Make sure there is padding if the last element is a view
     343            if ( isView( body.lastChild ) ) {
     344                padNode = createPadNode();
     345                body.appendChild( padNode );
     346
     347                if ( body.childNodes.length === 2 ) {
     348                    editor.selection.setCursorLocation( padNode, 0 );
    160349                }
    161 
    162                 node = node.parentNode;
    163             }
    164         },
    165 
    166         isView : function( node ) {
    167             return (/(?:^|\s)wp-view-wrap(?:\s|$)/).test( node.className );
    168         },
    169 
    170         select : function( view ) {
    171             if ( view === selected ) {
    172                 return;
    173             }
    174 
    175             this.deselect();
    176             selected = view;
    177             wp.mce.view.select( selected );
    178         },
    179 
    180         deselect : function() {
    181             if ( selected ) {
    182                 wp.mce.view.deselect( selected );
    183             }
    184 
    185             selected = null;
    186         }
    187     });
    188 
    189     // Register plugin
    190     tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView );
    191 })();
     350            }
     351
     352            range = editor.selection.getRng();
     353
     354            // Allow an initial element in the document to be removed when it is before a view
     355            if ( body.firstChild === range.startContainer && range.collapsed === true &&
     356                    isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) {
     357
     358                editor.dom.remove( range.startContainer );
     359            }
     360        }
     361    });
     362
     363    return {
     364        getViewText: getViewText,
     365        setViewText: setViewText
     366    };
     367});
  • trunk/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css

    r27387 r27408  
    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;
  • trunk/src/wp-includes/media-template.php

    r27400 r27408  
    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.
  • trunk/src/wp-includes/version.php

    r27387 r27408  
    1919 * @global string $tinymce_version
    2020 */
    21 $tinymce_version = '4018-20140303';
     21$tinymce_version = '4018-20140304';
    2222
    2323/**
Note: See TracChangeset for help on using the changeset viewer.