diff --git a/wp-admin/edit-form-advanced.php b/wp-admin/edit-form-advanced.php
index 9e3fa9492d..7ebc320cc7 100644
--- a/wp-admin/edit-form-advanced.php
+++ b/wp-admin/edit-form-advanced.php
@@ -22,6 +22,8 @@ if ( post_type_supports($post_type, 'editor') || post_type_supports($post_type,
wp_enqueue_style( 'media-views' );
wp_plupload_default_settings();
add_action( 'admin_footer', 'wp_print_media_templates' );
+
+ wp_enqueue_script( 'mce-view' );
}
/**
diff --git a/wp-includes/class-wp-editor.php b/wp-includes/class-wp-editor.php
index 3a61f80694..9830eaae9e 100644
--- a/wp-includes/class-wp-editor.php
+++ b/wp-includes/class-wp-editor.php
@@ -167,12 +167,12 @@ final class _WP_Editors {
self::$baseurl = includes_url('js/tinymce');
self::$mce_locale = $mce_locale = ( '' == get_locale() ) ? 'en' : strtolower( substr(get_locale(), 0, 2) ); // only ISO 639-1
$no_captions = (bool) apply_filters( 'disable_captions', '' );
- $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wpeditimage', 'wpgallery', 'wplink', 'wpdialogs' );
+ $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wpeditimage', 'wpgallery', 'wplink', 'wpdialogs', 'wpview' );
$first_run = true;
$ext_plugins = '';
if ( $set['teeny'] ) {
- self::$plugins = $plugins = apply_filters( 'teeny_mce_plugins', array('inlinepopups', 'fullscreen', 'wordpress', 'wplink', 'wpdialogs'), $editor_id );
+ self::$plugins = $plugins = apply_filters( 'teeny_mce_plugins', array('inlinepopups', 'fullscreen', 'wordpress', 'wplink', 'wpdialogs', 'wpview'), $editor_id );
} else {
/*
The following filter takes an associative array of external plugins for TinyMCE in the form 'plugin_name' => 'url'.
diff --git a/wp-includes/js/mce-view.js b/wp-includes/js/mce-view.js
new file mode 100644
index 0000000000..f46181f193
--- /dev/null
+++ b/wp-includes/js/mce-view.js
@@ -0,0 +1,134 @@
+if ( typeof wp === 'undefined' )
+ var wp = {};
+
+(function($){
+ var views = {},
+ instances = {};
+
+ wp.mce = {};
+
+ wp.mce.view = {
+ // The default properties used for the objects in `wp.mce.view.add()`.
+ defaults: {
+ view: Backbone.View,
+ text: function( instance ) {
+ return instance.options.original;
+ }
+ },
+
+ // Registers a new TinyMCE view.
+ //
+ // Accepts a unique `id` and an `options` object.
+ //
+ // `options`
+ // If `view` is an object, it will automatically be passed to
+ // `wp.mce.View.extend( view, properties )` to create a new view class.
+ //
+ // If the `view` provided is already a constructor, the `properties`
+ // variable is ignored.
+ add: function( id, options ) {
+ var parent;
+
+ // Fetch the parent view or the default options.
+ parent = options.extend ? wp.mce.view.get( options.extend ) : wp.mce.view.defaults;
+
+ // Extend the `options` object with the parent's properties.
+ _.defaults( options, parent );
+
+ // If the `view` provided was an object, automatically create
+ // a new `Backbone.View` constructor, using the parent's `view`
+ // constructor as a base.
+ if ( ! _.isFunction( options.view ) )
+ options.view = parent.view.extend( options.view );
+
+ views[ id ] = options;
+ },
+
+ // Returns a TinyMCE view options object.
+ get: function( id ) {
+ return views[ id ];
+ },
+
+ // Unregisters a TinyMCE view.
+ remove: function( id ) {
+ delete views[ id ];
+ },
+
+ // 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: function( content ) {
+ _.each( views, function( view, viewType ) {
+ if ( ! view.pattern )
+ return;
+
+ // Scan for matches.
+ content = content.replace( view.pattern, function( match ) {
+ var instance, id, tag;
+
+ // Create a new view instance.
+ instance = new view.view({
+ original: match,
+ results: _.toArray( arguments ),
+ viewType: viewType
+ });
+
+ // 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;
+
+ // If the view is a span, wrap it in a span.
+ tag = 'span' === instance.tagName ? 'span' : 'div';
+
+ return '<' + tag + ' class="wp-view-wrap" data-wp-view="' + id + '" contenteditable="false">' + tag + '>';
+ });
+ });
+
+ return content;
+ },
+
+ // 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),
+ id = wrapper.data('wp-view'),
+ view = instances[ id ];
+
+ if ( ! view )
+ return;
+
+ // Render the view.
+ view.render();
+ // Detach the view element to ensure events are not unbound.
+ view.$el.detach();
+
+ // 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('');
+ });
+ },
+
+ // 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><\/(?: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 ) : '';
+ });
+ }
+ };
+
+}(jQuery));
\ No newline at end of file
diff --git a/wp-includes/js/mce-view.min.js b/wp-includes/js/mce-view.min.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/wp-includes/js/tinymce/plugins/wpview/editor_plugin.js b/wp-includes/js/tinymce/plugins/wpview/editor_plugin.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js b/wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js
new file mode 100644
index 0000000000..09d58f1e4f
--- /dev/null
+++ b/wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js
@@ -0,0 +1,79 @@
+/**
+ * WordPress View plugin.
+ */
+
+(function() {
+ tinymce.create('tinymce.plugins.wpView', {
+ init : function( editor, url ) {
+ var wpView = this;
+
+ // Check if the `wp.mce` API exists.
+ if ( typeof wp === 'undefined' || ! wp.mce )
+ return;
+
+ editor.onPreInit.add( function( editor ) {
+ // Add elements so we can set `contenteditable` to false.
+ editor.schema.addValidElements('div[*],span[*]');
+ });
+
+ // 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.onBeforeSetContent.add( function( editor, o ) {
+ if ( ! o.content )
+ return;
+
+ o.content = wp.mce.view.toViews( o.content );
+ });
+
+ // When the editor's content has been updated and the DOM has been
+ // processed, render the views in the document.
+ editor.onSetContent.add( function( editor, o ) {
+ wp.mce.view.render( editor.getDoc() );
+ });
+
+ editor.onInit.add( function( editor ) {
+
+ // 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.selection.onSetContent.add( function( selection, o ) {
+ if ( ! o.context )
+ return;
+
+ var node = selection.getNode();
+
+ if ( ! node.innerHTML )
+ return;
+
+ node.innerHTML = wp.mce.view.toViews( node.innerHTML );
+ wp.mce.view.render( node );
+ });
+ });
+
+ // When the editor's contents are being accessed as a string,
+ // transform any views back to their text representations.
+ editor.onPostProcess.add( function( editor, o ) {
+ if ( ( ! o.get && ! o.save ) || ! o.content )
+ return;
+
+ o.content = wp.mce.view.toText( o.content );
+ });
+ },
+
+ getInfo : function() {
+ return {
+ longname : 'WordPress Views',
+ author : 'WordPress',
+ authorurl : 'http://wordpress.org',
+ infourl : 'http://wordpress.org',
+ version : '1.0'
+ };
+ }
+ });
+
+ // Register plugin
+ tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView );
+})();
\ No newline at end of file
diff --git a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css
index 3c66ae14b8..246cdf929d 100644
--- a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css
+++ b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css
@@ -141,3 +141,7 @@ img.wp-oembed {
height: 250px;
}
+/* WordPress TinyMCE Previews */
+div.wp-preview-wrap {
+ display: inline;
+}
diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php
index 6a03efeb29..01a3dec4ea 100644
--- a/wp-includes/script-loader.php
+++ b/wp-includes/script-loader.php
@@ -322,6 +322,8 @@ function wp_default_scripts( &$scripts ) {
'selectMediaMultiple' => __( 'Select one or more media files:' ),
) );
+ $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'backbone', 'jquery' ), false, 1 );
+
if ( is_admin() ) {
$scripts->add( 'ajaxcat', "/wp-admin/js/cat$suffix.js", array( 'wp-lists' ) );
$scripts->add_data( 'ajaxcat', 'group', 1 );