diff -r 000000000000 -r d970ebf37754 wp/wp-includes/js/mce-view.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wp/wp-includes/js/mce-view.js Wed Nov 06 03:21:17 2013 +0000 @@ -0,0 +1,349 @@ +// Ensure the global `wp` object exists. +window.wp = window.wp || {}; + +(function($){ + var views = {}, + instances = {}; + + // 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; + }, + + 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 + } + }; + } + }, + + // 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(); + }, + + 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 + } + }; + } + } + }, + + // ### 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; + }, + + // ### get( id ) + // Returns a TinyMCE view options object. + get: function( id ) { + return views[ id ]; + }, + + // ### 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: function( content ) { + var pieces = [ { content: content } ], + current; + + _.each( views, function( view, viewType ) { + current = pieces.slice(); + pieces = []; + + _.each( current, function( piece ) { + var remaining = piece.content, + result; + + // Ignore processed pieces, but retain their location. + if ( piece.processed ) { + pieces.push( piece ); + return; + } + + // Iterate through the string progressively matching views + // 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 ) + 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 ), + processed: true + }); + + // Update the remaining content. + remaining = remaining.slice( result.index + result.content.length ); + } + + // There are no additional matches. If any content remains, + // add it as an unprocessed piece. + if ( remaining ) + pieces.push({ content: remaining }); + }); + }); + + return _.pluck( pieces, 'content' ).join(''); + }, + + toView: function( viewType, options ) { + var view = wp.mce.view.get( viewType ), + instance, id; + + if ( ! view ) + return ''; + + // Create a new view instance. + instance = new view.view( _.extend( options || {}, { + 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; + + // Create a dummy `$wrapper` property to allow `$wrapper` to be + // called in the view's `render` method without a conditional. + instance.$wrapper = $(); + + return wp.html.string({ + // If the view is a span, wrap it in a span. + tag: 'span' === instance.tagName ? 'span' : 'div', + + attrs: { + 'class': 'wp-view-wrap wp-view-type-' + viewType, + 'data-wp-view': id, + 'contenteditable': false + } + }); + }, + + // ### 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 ); + + if ( ! view ) + return; + + // 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(); + + // 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(''); + }); + }, + + // ### 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 ) : ''; + }); + }, + + // ### 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; + }, + + // ### Parse an attribute string and removes internal TinyMCE attributes. + attrs: function( content ) { + return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) ); + }, + + // ### 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'); + } + }; + +}(jQuery)); \ No newline at end of file