diff --git a/src/wp-includes/class-wp-editor.php b/src/wp-includes/class-wp-editor.php index af2a071c6c..d209c588b7 100644 --- a/src/wp-includes/class-wp-editor.php +++ b/src/wp-includes/class-wp-editor.php @@ -242,6 +242,7 @@ final class _WP_Editors { 'wpgallery', 'wplink', 'wpdialogs', + 'wpview', ) ) ); if ( ( $key = array_search( 'spellchecker', $plugins ) ) !== false ) { @@ -501,6 +502,9 @@ final class _WP_Editors { if ( self::$has_medialib ) { add_thickbox(); wp_enqueue_script('media-upload'); + + if ( self::$has_tinymce ) + wp_enqueue_script('mce-view'); } } diff --git a/src/wp-includes/js/mce-view.js b/src/wp-includes/js/mce-view.js index 912c4c7ce4..0881680b77 100644 --- a/src/wp-includes/js/mce-view.js +++ b/src/wp-includes/js/mce-view.js @@ -1,173 +1,95 @@ +/* global tinymce */ + // Ensure the global `wp` object exists. window.wp = window.wp || {}; (function($){ var views = {}, - instances = {}; + instances = {}, + media = wp.media, + viewOptions = ['encodedText']; // Create the `wp.mce` object if necessary. wp.mce = wp.mce || {}; - // wp.mce.view - // ----------- - // A set of utilities that simplifies adding custom UI within a TinyMCE editor. - // At its core, it serves as a series of converters, transforming text to a - // custom UI, and back again. - wp.mce.view = { - // ### defaults - defaults: { - // The default properties used for objects with the `pattern` key in - // `wp.mce.view.add()`. - pattern: { - view: Backbone.View, - text: function( instance ) { - return instance.options.original; - }, + /** + * wp.mce.View + * + * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is + * that the TinyMCE View is not tied to a particular DOM node. + */ + wp.mce.View = function( options ) { + options || (options = {}); + _.extend(this, _.pick(options, viewOptions)); + this.initialize.apply(this, arguments); + }; - toView: function( content ) { - if ( ! this.pattern ) - return; - - this.pattern.lastIndex = 0; - var match = this.pattern.exec( content ); - - if ( ! match ) - return; - - return { - index: match.index, - content: match[0], - options: { - original: match[0], - results: match - } - }; + _.extend( wp.mce.View.prototype, { + initialize: function() {}, + html: function() {}, + render: function() { + var html = this.getHtml(); + // Search all tinymce editor instances and update the placeholders + _.each( tinymce.editors, function( editor ) { + var doc; + if ( editor.plugins.wpview ) { + doc = editor.getDoc(); + $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).html( html ); } - }, + }, this ); + } + } ); - // The default properties used for objects with the `shortcode` key in - // `wp.mce.view.add()`. - shortcode: { - view: Backbone.View, - text: function( instance ) { - return instance.options.shortcode.string(); - }, + // take advantage of the Backbone extend method + wp.mce.View.extend = Backbone.View.extend; - toView: function( content ) { - var match = wp.shortcode.next( this.shortcode, content ); + /** + * wp.mce.views + * + * A set of utilities that simplifies adding custom UI within a TinyMCE editor. + * At its core, it serves as a series of converters, transforming text to a + * custom UI, and back again. + */ + wp.mce.views = { - if ( ! match ) - return; - - return { - index: match.index, - content: match.content, - options: { - shortcode: match.shortcode - } - }; - } - } + /** + * wp.mce.views.register( type, view ) + * + * Registers a new TinyMCE view. + * + * @param type + * @param constructor + * + */ + register: function( type, constructor ) { + views[ type ] = constructor; }, - // ### add( id, options ) - // Registers a new TinyMCE view. - // - // Accepts a unique `id` and an `options` object. - // - // `options` accepts the following properties: - // - // * `pattern` is the regular expression used to scan the content and - // detect matching views. - // - // * `view` is a `Backbone.View` constructor. If a plain object is - // provided, it will automatically extend the parent constructor - // (usually `Backbone.View`). Views are instantiated when the `pattern` - // is successfully matched. The instance's `options` object is provided - // with the `original` matched value, the match `results` including - // capture groups, and the `viewType`, which is the constructor's `id`. - // - // * `extend` an existing view by passing in its `id`. The current - // view will inherit all properties from the parent view, and if - // `view` is set to a plain object, it will extend the parent `view` - // constructor. - // - // * `text` is a method that accepts an instance of the `view` - // constructor and transforms it into a text representation. - add: function( id, options ) { - var parent, remove, base, properties; - - // Fetch the parent view or the default options. - if ( options.extend ) - parent = wp.mce.view.get( options.extend ); - else if ( options.shortcode ) - parent = wp.mce.view.defaults.shortcode; - else - parent = wp.mce.view.defaults.pattern; - - // Extend the `options` object with the parent's properties. - _.defaults( options, parent ); - options.id = id; - - // Create properties used to enhance the view for use in TinyMCE. - properties = { - // Ensure the wrapper element and references to the view are - // removed. Otherwise, removed views could randomly restore. - remove: function() { - delete instances[ this.el.id ]; - this.$el.parent().remove(); - - // Trigger the inherited `remove` method. - if ( remove ) - remove.apply( this, arguments ); - - return this; - } - }; - - // If the `view` provided was an object, use the parent's - // `view` constructor as a base. If a `view` constructor - // was provided, treat that as the base. - if ( _.isFunction( options.view ) ) { - base = options.view; - } else { - base = parent.view; - remove = options.view.remove; - _.defaults( properties, options.view ); - } - - // If there's a `remove` method on the `base` view that wasn't - // created by this method, inherit it. - if ( ! remove && ! base._mceview ) - remove = base.prototype.remove; - - // Automatically create the new `Backbone.View` constructor. - options.view = base.extend( properties, { - // Flag that the new view has been created by `wp.mce.view`. - _mceview: true - }); - - views[ id ] = options; + /** + * wp.mce.views.get( id ) + * + * Returns a TinyMCE view constructor. + */ + get: function( type ) { + return views[ type ]; }, - // ### get( id ) - // Returns a TinyMCE view options object. - get: function( id ) { - return views[ id ]; + /** + * wp.mce.views.unregister( type ) + * + * Unregisters a TinyMCE view. + */ + unregister: function( type ) { + delete views[ type ]; }, - // ### remove( id ) - // Unregisters a TinyMCE view. - remove: function( id ) { - delete views[ id ]; - }, - - // ### toViews( content ) - // Scans a `content` string for each view's pattern, replacing any - // matches with wrapper elements, and creates a new view instance for - // every match. - // - // To render the views, call `wp.mce.view.render( scope )`. + /** + * toViews( content ) + * Scans a `content` string for each view's pattern, replacing any + * matches with wrapper elements, and creates a new instance for + * every match, which triggers the related data to be fetched. + * + */ toViews: function( content ) { var pieces = [ { content: content } ], current; @@ -190,12 +112,13 @@ window.wp = window.wp || {}; // and slicing the string as we go. while ( remaining && (result = view.toView( remaining )) ) { // Any text before the match becomes an unprocessed piece. - if ( result.index ) + if ( result.index ) { pieces.push({ content: remaining.substring( 0, result.index ) }); + } // Add the processed piece for the match. pieces.push({ - content: wp.mce.view.toView( viewType, result.options ), + content: wp.mce.views.toView( viewType, result.content, result.options ), processed: true }); @@ -205,145 +128,178 @@ window.wp = window.wp || {}; // There are no additional matches. If any content remains, // add it as an unprocessed piece. - if ( remaining ) + if ( remaining ) { pieces.push({ content: remaining }); + } }); }); return _.pluck( pieces, 'content' ).join(''); }, - toView: function( viewType, options ) { - var view = wp.mce.view.get( viewType ), - instance, id; + /** + * Create a placeholder for a particular view type + * + * @param viewType + * @param text + * @param options + * + */ + toView: function( viewType, text, options ) { + var view = wp.mce.views.get( viewType ), + encodedText = window.encodeURIComponent( text ), + instance, viewOptions; - if ( ! view ) - return ''; - // Create a new view instance. - instance = new view.view( _.extend( options || {}, { - viewType: viewType - }) ); + if ( ! view ) { + return text; + } - // Use the view's `id` if it already exists. Otherwise, - // create a new `id`. - id = instance.el.id = instance.el.id || _.uniqueId('__wpmce-'); - instances[ id ] = instance; - - // Create a dummy `$wrapper` property to allow `$wrapper` to be - // called in the view's `render` method without a conditional. - instance.$wrapper = $(); + if ( ! wp.mce.views.getInstance( encodedText ) ) { + viewOptions = options; + viewOptions.encodedText = encodedText; + instance = new view.View( viewOptions ); + instances[ encodedText ] = instance; + } return wp.html.string({ - // If the view is a span, wrap it in a span. - tag: 'span' === instance.tagName ? 'span' : 'div', + tag: 'div', attrs: { - 'class': 'wp-view-wrap wp-view-type-' + viewType, - 'data-wp-view': id, - 'contenteditable': false - } + 'class': 'wpview-wrap wpview-type-' + viewType, + 'data-wpview-text': encodedText, + 'data-wpview-type': viewType, + 'contenteditable': 'false' + }, + + content: '\u00a0' }); }, - // ### render( scope ) - // Renders any view instances inside a DOM node `scope`. - // - // View instances are detected by the presence of wrapper elements. - // To generate wrapper elements, pass your content through - // `wp.mce.view.toViews( content )`. - render: function( scope ) { - $( '.wp-view-wrap', scope ).each( function() { - var wrapper = $(this), - view = wp.mce.view.instance( this ); + /** + * Refresh views after an update is made + * + * @param view {object} being refreshed + * @param text {string} textual representation of the view + */ + refreshView: function( view, text ) { + var encodedText = window.encodeURIComponent( text ), + viewOptions, + result, instance; - if ( ! view ) - return; + instance = wp.mce.views.getInstance( encodedText ); - // Link the real wrapper to the view. - view.$wrapper = wrapper; - // Render the view. - view.render(); - // Detach the view element to ensure events are not unbound. - view.$el.detach(); + if ( ! instance ) { + result = view.toView( text ); + viewOptions = result.options; + viewOptions.encodedText = encodedText; + instance = new view.View( viewOptions ); + instances[ encodedText ] = instance; + } - // Empty the wrapper, attach the view element to the wrapper, - // and add an ending marker to the wrapper to help regexes - // scan the HTML string. - wrapper.empty().append( view.el ).append(''); - }); + wp.mce.views.render(); }, - // ### toText( content ) - // Scans an HTML `content` string and replaces any view instances with - // their respective text representations. - toText: function( content ) { - return content.replace( /<(?:div|span)[^>]+data-wp-view="([^"]+)"[^>]*>.*?]+data-wp-view-end[^>]*><\/span><\/(?:div|span)>/g, function( match, id ) { - var instance = instances[ id ], - view; - - if ( instance ) - view = wp.mce.view.get( instance.options.viewType ); - - return instance && view ? view.text( instance ) : ''; - }); + getInstance: function( encodedText ) { + return instances[ encodedText ]; }, - // ### Remove internal TinyMCE attributes. - removeInternalAttrs: function( attrs ) { - var result = {}; - _.each( attrs, function( value, attr ) { - if ( -1 === attr.indexOf('data-mce') ) - result[ attr ] = value; - }); - return result; + /** + * render( scope ) + * + * Renders any view instances inside a DOM node `scope`. + * + * View instances are detected by the presence of wrapper elements. + * To generate wrapper elements, pass your content through + * `wp.mce.view.toViews( content )`. + */ + render: function() { + _.each( instances, function( instance ) { + instance.render(); + } ); }, - // ### Parse an attribute string and removes internal TinyMCE attributes. - attrs: function( content ) { - return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) ); - }, + edit: function( node ) { + var viewType = $( node ).data('wpview-type'), + view = wp.mce.views.get( viewType ); - // ### instance( scope ) - // - // Accepts a MCE view wrapper `node` (i.e. a node with the - // `wp-view-wrap` class). - instance: function( node ) { - var id = $( node ).data('wp-view'); - - if ( id ) - return instances[ id ]; - }, - - // ### Select a view. - // - // Accepts a MCE view wrapper `node` (i.e. a node with the - // `wp-view-wrap` class). - select: function( node ) { - var $node = $(node); - - // Bail if node is already selected. - if ( $node.hasClass('selected') ) - return; - - $node.addClass('selected'); - $( node.firstChild ).trigger('select'); - }, - - // ### Deselect a view. - // - // Accepts a MCE view wrapper `node` (i.e. a node with the - // `wp-view-wrap` class). - deselect: function( node ) { - var $node = $(node); - - // Bail if node is already selected. - if ( ! $node.hasClass('selected') ) - return; - - $node.removeClass('selected'); - $( node.firstChild ).trigger('deselect'); + if ( view ) { + view.edit( node ); + } } }; -}(jQuery)); \ No newline at end of file + wp.mce.gallery = { + shortcode: 'gallery', + toView: function( content ) { + var match = wp.shortcode.next( this.shortcode, content ); + + if ( ! match ) { + return; + } + + return { + index: match.index, + content: match.content, + options: { + shortcode: match.shortcode + } + }; + }, + View: wp.mce.View.extend({ + className: 'editor-gallery', + template: media.template('editor-gallery'), + + // The fallback post ID to use as a parent for galleries that don't + // specify the `ids` or `include` parameters. + // + // Uses the hidden input on the edit posts page by default. + postID: $('#post_ID').val(), + + initialize: function( options ) { + this.shortcode = options.shortcode; + this.fetch(); + }, + + fetch: function() { + this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID ); + this.attachments.more().done( _.bind( this.render, this ) ); + }, + + getHtml: function() { + var attrs = this.shortcode.attrs.named, + options; + + if ( ! this.attachments.length ) { + return; + } + + options = { + attachments: this.attachments.toJSON(), + columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3 + }; + + return this.template( options ); + + } + }), + + edit: function( node ) { + var gallery = wp.media.gallery, + self = this, + frame, data; + + data = window.decodeURIComponent( $( node ).data('wpview-text') ); + frame = gallery.edit( data ); + + frame.state('gallery-edit').on( 'update', function( selection ) { + var shortcode = gallery.shortcode( selection ).string(); + $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); + wp.mce.views.refreshView( self, shortcode ); + frame.detach(); + }); + } + + }; + wp.mce.views.register( 'gallery', wp.mce.gallery ); +}(jQuery)); diff --git a/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js b/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js index def97d6faf..6e645709f3 100644 --- a/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js +++ b/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js @@ -59,7 +59,7 @@ tinymce.PluginManager.add('wpgallery', function( editor ) { return; } - // Check if the `wp.media.gallery` API exists. + // Check if the `wp.media` API exists. if ( typeof wp === 'undefined' || ! wp.media ) { return; } @@ -166,7 +166,11 @@ tinymce.PluginManager.add('wpgallery', function( editor ) { }); editor.on( 'BeforeSetContent', function( event ) { - event.content = replaceGalleryShortcodes( event.content ); + // 'wpview' handles the gallery shortcode when present + if ( ! editor.plugins.wpview ) { + event.content = replaceGalleryShortcodes( event.content ); + } + event.content = replaceAVShortcodes( event.content ); }); diff --git a/src/wp-includes/js/tinymce/plugins/wpview/plugin.js b/src/wp-includes/js/tinymce/plugins/wpview/plugin.js index 0c56ecba1f..e56e3da334 100644 --- a/src/wp-includes/js/tinymce/plugins/wpview/plugin.js +++ b/src/wp-includes/js/tinymce/plugins/wpview/plugin.js @@ -2,190 +2,366 @@ /** * WordPress View plugin. */ - -(function() { - var VK = tinymce.VK, +tinymce.PluginManager.add( 'wpview', function( editor ) { + var selected, + VK = tinymce.util.VK, TreeWalker = tinymce.dom.TreeWalker, - selected; + toRemove = false; - tinymce.create('tinymce.plugins.wpView', { - init : function( editor ) { - var wpView = this; - - // Check if the `wp.mce` API exists. - if ( typeof wp === 'undefined' || ! wp.mce ) { - return; + function getParentView( node ) { + while ( node && node.nodeName !== 'BODY' ) { + if ( isView( node ) ) { + return node; } - editor.on( 'PreInit', function() { - // Add elements so we can set `contenteditable` to false. - editor.schema.addValidElements('div[*],span[*]'); - }); + node = node.parentNode; + } + } - // When the editor's content changes, scan the new content for - // matching view patterns, and transform the matches into - // view wrappers. Since the editor's DOM is outdated at this point, - // we'll wait to render the views. - editor.on( 'BeforeSetContent', function( e ) { - if ( ! e.content ) { - return; - } + function isView( node ) { + return node && /\bwpview-wrap\b/.test( node.className ); + } - e.content = wp.mce.view.toViews( e.content ); - }); + function createPadNode() { + return editor.dom.create( 'p', { 'data-wpview-pad': 1 }, + ( tinymce.Env.ie && tinymce.Env.ie < 11 ) ? '' : '
' ); + } - // When the editor's content has been updated and the DOM has been - // processed, render the views in the document. - editor.on( 'SetContent', function() { - wp.mce.view.render( editor.getDoc() ); - }); + /** + * Get the text/shortcode string for a view. + * + * @param view The view wrapper's HTML id or node + * @returns string The text/shoercode string of the view + */ + function getViewText( view ) { + view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view ); - editor.on( 'init', function() { - var selection = editor.selection; - // When a view is selected, ensure content that is being pasted - // or inserted is added to a text node (instead of the view). - editor.on( 'BeforeSetContent', function() { - var walker, target, - view = wpView.getParentView( selection.getNode() ); + if ( view ) { + return window.decodeURIComponent( editor.dom.getAttrib( view, 'data-wpview-text' ) || '' ); + } + return ''; + } - // If the selection is not within a view, bail. - if ( ! view ) { - return; - } + /** + * Set the view's original text/shortcode string + * + * @param view The view wrapper's HTML id or node + * @param text The text string to be set + */ + function setViewText( view, text ) { + view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view ); - // If there are no additional nodes or the next node is a - // view, create a text node after the current view. - if ( ! view.nextSibling || wpView.isView( view.nextSibling ) ) { - target = editor.getDoc().createTextNode(''); - editor.dom.insertAfter( target, view ); + if ( view ) { + editor.dom.setAttrib( view, 'data-wpview-text', window.encodeURIComponent( text || '' ) ); + return true; + } + return false; + } - // Otherwise, find the next text node. - } else { - walker = new TreeWalker( view.nextSibling, view.nextSibling ); - target = walker.next(); - } + function _stop( event ) { + event.stopPropagation(); + } - // Select the `target` text node. - selection.select( target ); - selection.collapse( true ); - }); + function select( viewNode ) { + var clipboard, + dom = editor.dom; - // When the selection's content changes, scan any new content - // for matching views and immediately render them. - // - // Runs on paste and on inserting nodes/html. - editor.on( 'SetContent', function( e ) { - if ( ! e.context ) { - return; - } + // Bail if node is already selected. + if ( viewNode === selected ) { + return; + } - var node = selection.getNode(); + deselect(); + selected = viewNode; + dom.addClass( viewNode, 'selected' ); - if ( ! node.innerHTML ) { - return; - } + clipboard = dom.create( 'div', { + 'class': 'wpview-clipboard', + 'contenteditable': 'true' + }, getViewText( viewNode ) ); - node.innerHTML = wp.mce.view.toViews( node.innerHTML ); - wp.mce.view.render( node ); - }); - }); + viewNode.appendChild( clipboard ); - // When the editor's contents are being accessed as a string, - // transform any views back to their text representations. - editor.on( 'PostProcess', function( e ) { - if ( ( ! e.get && ! e.save ) || ! e.content ) { - return; - } + // Both of the following are necessary to prevent manipulating the selection/focus + editor.dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop ); + editor.dom.bind( selected, 'beforedeactivate focusin focusout', _stop ); - e.content = wp.mce.view.toText( e.content ); - }); + // select the hidden div + editor.selection.select( clipboard, true ); + } - // Triggers when the selection is changed. - // Add the event handler to the top of the stack. - editor.on( 'NodeChange', function( e ) { - var view = wpView.getParentView( e.element ); + /** + * Deselect a selected view and remove clipboard + */ + function deselect() { + var clipboard, + dom = editor.dom; - // Update the selected view. - if ( view ) { - wpView.select( view ); + if ( selected ) { + clipboard = editor.dom.select( '.wpview-clipboard', selected )[0]; + dom.unbind( clipboard ); + dom.remove( clipboard ); - // Prevent the selection from propagating to other plugins. - return false; + dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop ); + dom.removeClass( selected, 'selected' ); - // If we've clicked off of the selected view, deselect it. - } else { - wpView.deselect(); - } - }); + editor.selection.select( selected.nextSibling ); + editor.selection.collapse(); - editor.on( 'keydown', function( event ) { - var keyCode = event.keyCode, - view, instance; + } - // If a view isn't selected, let the event go on its merry way. - if ( ! selected ) { - return; - } + selected = null; + } - // If the caret is not within the selected view, deselect the - // view and bail. - view = wpView.getParentView( editor.selection.getNode() ); - if ( view !== selected ) { - wpView.deselect(); - return; - } + // Check if the `wp.mce` API exists. + if ( typeof wp === 'undefined' || ! wp.mce ) { + return; + } - // If delete or backspace is pressed, delete the view. - if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { - if ( (instance = wp.mce.view.instance( selected )) ) { - instance.remove(); - wpView.deselect(); - } - } - - // Let keypresses that involve the command or control keys through. - // Also, let any of the F# keys through. - if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) { - return; - } - - event.preventDefault(); - }); - }, - - getParentView : function( node ) { - while ( node ) { - if ( this.isView( node ) ) { - return node; - } - - node = node.parentNode; - } - }, - - isView : function( node ) { - return (/(?:^|\s)wp-view-wrap(?:\s|$)/).test( node.className ); - }, - - select : function( view ) { - if ( view === selected ) { - return; - } - - this.deselect(); - selected = view; - wp.mce.view.select( selected ); - }, - - deselect : function() { - if ( selected ) { - wp.mce.view.deselect( selected ); - } - - selected = null; + editor.on( 'BeforeAddUndo', function( event ) { + if ( selected && ! toRemove ) { + event.preventDefault(); } }); - // Register plugin - tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView ); -})(); + // When the editor's content changes, scan the new content for + // matching view patterns, and transform the matches into + // view wrappers. + editor.on( 'BeforeSetContent', function( e ) { + if ( ! e.content ) { + return; + } + + e.content = wp.mce.views.toViews( e.content ); + }); + + // When the editor's content has been updated and the DOM has been + // processed, render the views in the document. + editor.on( 'SetContent', function( event ) { + var body, padNode; + + wp.mce.views.render(); + + // Add padding

if the noneditable node is last + if ( event.load || ! event.set ) { + body = editor.getBody(); + + if ( isView( body.lastChild ) ) { + padNode = createPadNode(); + body.appendChild( padNode ); + editor.selection.setCursorLocation( padNode, 0 ); + } + } + + // refreshEmptyContentNode(); + }); + + // Detect mouse down events that are adjacent to a view when a view is the first view or the last view + editor.on( 'click', function( event ) { + var body = editor.getBody(), + doc = editor.getDoc(), + scrollTop = doc.documentElement.scrollTop || body.scrollTop || 0, + x, y, firstNode, lastNode, padNode; + + if ( event.target.nodeName === 'HTML' && ! event.metaKey && ! event.ctrlKey ) { + firstNode = body.firstChild; + lastNode = body.lastChild; + x = event.clientX; + y = event.clientY; + + if ( isView( firstNode ) && ( ( x < firstNode.offsetLeft && y < ( firstNode.offsetHeight - scrollTop ) ) || + y < firstNode.offsetTop ) ) { + // detect events above or to the left of the first view + + padNode = createPadNode(); + body.insertBefore( padNode, firstNode ); + } else if ( isView( lastNode ) && ( x > ( lastNode.offsetLeft + lastNode.offsetWidth ) || + ( ( scrollTop + y ) - ( lastNode.offsetTop + lastNode.offsetHeight ) ) > 0 ) ) { + // detect events to the right and below the last view + + padNode = createPadNode(); + body.appendChild( padNode ); + } + + if ( padNode ) { + editor.selection.setCursorLocation( padNode, 0 ); + } + } + }); + + editor.on( 'init', function() { + var selection = editor.selection; + // When a view is selected, ensure content that is being pasted + // or inserted is added to a text node (instead of the view). + editor.on( 'BeforeSetContent', function() { + var walker, target, + view = getParentView( selection.getNode() ); + + // If the selection is not within a view, bail. + if ( ! view ) { + return; + } + + if ( ! view.nextSibling || isView( view.nextSibling ) ) { + // If there are no additional nodes or the next node is a + // view, create a text node after the current view. + target = editor.getDoc().createTextNode(''); + editor.dom.insertAfter( target, view ); + } else { + // Otherwise, find the next text node. + walker = new TreeWalker( view.nextSibling, view.nextSibling ); + target = walker.next(); + } + + // Select the `target` text node. + selection.select( target ); + selection.collapse( true ); + }); + + // When the selection's content changes, scan any new content + // for matching views. + // + // Runs on paste and on inserting nodes/html. + editor.on( 'SetContent', function( e ) { + if ( ! e.context ) { + return; + } + + var node = selection.getNode(); + + if ( ! node.innerHTML ) { + return; + } + + node.innerHTML = wp.mce.views.toViews( node.innerHTML ); + }); + + editor.dom.bind( editor.getBody(), 'mousedown mouseup click', function( event ) { + var view = getParentView( event.target ); + + // Contain clicks inside the view wrapper + if ( view ) { + event.stopPropagation(); + + if ( event.type === 'click' ) { + if ( ! event.metaKey && ! event.ctrlKey ) { + if ( editor.dom.hasClass( event.target, 'edit' ) ) { + wp.mce.views.edit( view ); + } else if ( editor.dom.hasClass( event.target, 'remove' ) ) { + editor.dom.remove( view ); + } + } + } + select( view ); + // returning false stops the ugly bars from appearing in IE11 and stops the view being selected as a range in FF + // unfortunately, it also inhibits the dragging fo views to a new location + return false; + } else { + if ( event.type === 'click' ) { + deselect(); + } + } + }); + + }); + + editor.on( 'PreProcess', function( event ) { + var dom = editor.dom; + + // Remove empty padding nodes + tinymce.each( dom.select( 'p[data-wpview-pad]', event.node ), function( node ) { + if ( dom.isEmpty( node ) ) { + dom.remove( node ); + } else { + dom.setAttrib( node, 'data-wpview-pad', null ); + } + }); + + // Replace the wpview node with the wpview string/shortcode? + tinymce.each( dom.select( 'div[data-wpview-text]', event.node ), function( node ) { + // Empty the wrap node + if ( 'textContent' in node ) { + node.textContent = ''; + } else { + node.innerText = ''; + } + + // TODO: that makes all views into block tags (as we use

). + // Can use 'PostProcess' and a regex instead. + dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node ); + }); + }); + + editor.on( 'keydown', function( event ) { + var keyCode = event.keyCode, + view; + + // If a view isn't selected, let the event go on its merry way. + if ( ! selected ) { + return; + } + + // Let keypresses that involve the command or control keys through. + // Also, let any of the F# keys through. + if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) { + if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) { + toRemove = selected; + } + return; + } + + // If the caret is not within the selected view, deselect the + // view and bail. + view = getParentView( editor.selection.getNode() ); + + if ( view !== selected ) { + deselect(); + return; + } + + // If delete or backspace is pressed, delete the view. + if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { + editor.dom.remove( selected ); + } + + event.preventDefault(); + }); + + editor.on( 'keyup', function( event ) { + var padNode, + keyCode = event.keyCode, + body = editor.getBody(), + range; + + if ( toRemove ) { + editor.dom.remove( toRemove ); + toRemove = false; + } + + if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { + // Make sure there is padding if the last element is a view + if ( isView( body.lastChild ) ) { + padNode = createPadNode(); + body.appendChild( padNode ); + + if ( body.childNodes.length === 2 ) { + editor.selection.setCursorLocation( padNode, 0 ); + } + } + + range = editor.selection.getRng(); + + // Allow an initial element in the document to be removed when it is before a view + if ( body.firstChild === range.startContainer && range.collapsed === true && + isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) { + + editor.dom.remove( range.startContainer ); + } + } + }); + + return { + getViewText: getViewText, + setViewText: setViewText + }; +}); diff --git a/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css b/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css index 95f1f6c831..afff0c8599 100644 --- a/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css +++ b/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css @@ -198,6 +198,141 @@ img::selection { outline: 0; } + +/** + * WP Views + */ + +/* IE hasLayout. Needed for all IE incl. 11 (ugh, not again!!) */ +.wpview-wrap { + width: 99.99%; + position: relative; +} + +/* delegate the handling of the selection to the wpview tinymce plugin */ +.wpview-wrap, +.wpview-wrap * { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* hide the shortcode content, but allow the content to still be selected */ +.wpview-wrap .wpview-clipboard { + position: absolute; + top: 0; + left: 0; + z-index: -1; + clip: rect(1px, 1px, 1px, 1px); + overflow: hidden; + outline: 0; +} + +/** + * Gallery preview + */ +.wpview-type-gallery { + position: relative; + padding: 0 0 12px; + margin-bottom: 16px; + cursor: pointer; +} + + .wpview-type-gallery:after { + content: ''; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + + .wpview-type-gallery.selected { + background-color: #efefef; +} + +.wpview-type-gallery .toolbar { + position: absolute; + top: 0; + left: 0; + background-color: #333; + color: white; + padding: 4px; + display: none; +} + +.wpview-type-gallery.selected .toolbar { + display: block; +} + +.wpview-type-gallery .toolbar span { + cursor: pointer; +} + +.gallery img[data-mce-selected]:focus { + outline: none; +} + +.gallery a { + cursor: default; +} + +.gallery { + margin: auto; + line-height: 1; +} + +.gallery .gallery-item { + float: left; + margin: 10px 0 0 0; + text-align: center; +} + +.gallery .gallery-caption, +.gallery .gallery-icon { + margin: 0; +} + +.gallery-columns-1 .gallery-item { + width: 99%; +} + +.gallery-columns-2 .gallery-item { + width: 49.5%; +} + +.gallery-columns-3 .gallery-item { + width: 33%; +} + +.gallery-columns-4 .gallery-item { + width: 24.75%; +} + +.gallery-columns-5 .gallery-item { + width: 19.825%; +} + +.gallery-columns-6 .gallery-item { + width: 16%; +} + +.gallery-columns-7 .gallery-item { + width: 14%; +} + +.gallery-columns-8 .gallery-item { + width: 12%; +} + +.gallery-columns-9 .gallery-item { + width: 11%; +} + +.gallery img { + border: 1px solid #cfcfcf; +} + img.wp-oembed { border: 1px dashed #888; background: #f7f5f2 url(images/embedded.png) no-repeat scroll center center; diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 5304899f3c..97430d0f18 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -648,6 +648,36 @@ function wp_print_media_templates() {
+ + +