wp/wp-includes/js/mce-view.js
changeset 0 d970ebf37754
child 5 5e2f62d02dcd
--- /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('<span data-wp-view-end class="wp-view-end"></span>');
+			});
+		},
+
+		// ### 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 ) : '';
+			});
+		},
+
+		// ### 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