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

git-svn-id: https://develop.svn.wordpress.org/trunk@27408 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Andrew Ozz 2014-03-05 07:00:18 +00:00
parent bceb10680b
commit 57ac5b6015
7 changed files with 730 additions and 425 deletions

View File

@ -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');
}
}

View File

@ -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('<span data-wp-view-end class="wp-view-end"></span>');
});
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="([^"]+)"[^>]*>.*?<span[^>]+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));
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));

View File

@ -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 );
});

View File

@ -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 ) ? '' : '<br data-mce-bogus="1" />' );
}
// 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 <p> 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 <div>).
// 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
};
});

View File

@ -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;

View File

@ -648,6 +648,36 @@ function wp_print_media_templates() {
</div>
</div>
</script>
<?php
//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
// do we want to deal with the difference between display and edit context at all? (e.g. wptexturize() being applied to the caption.
?>
<script type="text/html" id="tmpl-editor-gallery">
<div class="toolbar">
<div class="dashicons dashicons-format-gallery edit"></div>
<div class="dashicons dashicons-no-alt remove"></div>
</div>
<div class="gallery gallery-columns-{{{ data.columns }}}">
<# _.each( data.attachments, function( attachment, index ) { #>
<dl class="gallery-item">
<dt class="gallery-icon">
<?php // TODO: need to figure out the best way to make sure that we have thumbnails ?>
<img src="{{{ attachment.sizes.thumbnail.url }}}" />
</dt>
<dd class="wp-caption-text gallery-caption">
{{ attachment.caption }}
</dd>
</dl>
<?php // this is kind silly, but copied from the gallery shortcode. Maybe it should be removed ?>
<# if ( index % data.columns === data.columns - 1 ) { #>
<br style="clear: both;">
<# } #>
<# } ); #>
</div>
</script>
<?php
/**

View File

@ -18,7 +18,7 @@ $wp_db_version = 26691;
*
* @global string $tinymce_version
*/
$tinymce_version = '4018-20140303';
$tinymce_version = '4018-20140304';
/**
* Holds the required PHP version