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