web/wp-includes/js/mce-view.js
changeset 204 09a1c134465b
equal deleted inserted replaced
203:f507feede89a 204:09a1c134465b
       
     1 // Ensure the global `wp` object exists.
       
     2 window.wp = window.wp || {};
       
     3 
       
     4 (function($){
       
     5 	var views = {},
       
     6 		instances = {};
       
     7 
       
     8 	// Create the `wp.mce` object if necessary.
       
     9 	wp.mce = wp.mce || {};
       
    10 
       
    11 	// wp.mce.view
       
    12 	// -----------
       
    13 	// A set of utilities that simplifies adding custom UI within a TinyMCE editor.
       
    14 	// At its core, it serves as a series of converters, transforming text to a
       
    15 	// custom UI, and back again.
       
    16 	wp.mce.view = {
       
    17 		// ### defaults
       
    18 		defaults: {
       
    19 			// The default properties used for objects with the `pattern` key in
       
    20 			// `wp.mce.view.add()`.
       
    21 			pattern: {
       
    22 				view: Backbone.View,
       
    23 				text: function( instance ) {
       
    24 					return instance.options.original;
       
    25 				},
       
    26 
       
    27 				toView: function( content ) {
       
    28 					if ( ! this.pattern )
       
    29 						return;
       
    30 
       
    31 					this.pattern.lastIndex = 0;
       
    32 					var match = this.pattern.exec( content );
       
    33 
       
    34 					if ( ! match )
       
    35 						return;
       
    36 
       
    37 					return {
       
    38 						index:   match.index,
       
    39 						content: match[0],
       
    40 						options: {
       
    41 							original: match[0],
       
    42 							results:  match
       
    43 						}
       
    44 					};
       
    45 				}
       
    46 			},
       
    47 
       
    48 			// The default properties used for objects with the `shortcode` key in
       
    49 			// `wp.mce.view.add()`.
       
    50 			shortcode: {
       
    51 				view: Backbone.View,
       
    52 				text: function( instance ) {
       
    53 					return instance.options.shortcode.string();
       
    54 				},
       
    55 
       
    56 				toView: function( content ) {
       
    57 					var match = wp.shortcode.next( this.shortcode, content );
       
    58 
       
    59 					if ( ! match )
       
    60 						return;
       
    61 
       
    62 					return {
       
    63 						index:   match.index,
       
    64 						content: match.content,
       
    65 						options: {
       
    66 							shortcode: match.shortcode
       
    67 						}
       
    68 					};
       
    69 				}
       
    70 			}
       
    71 		},
       
    72 
       
    73 		// ### add( id, options )
       
    74 		// Registers a new TinyMCE view.
       
    75 		//
       
    76 		// Accepts a unique `id` and an `options` object.
       
    77 		//
       
    78 		// `options` accepts the following properties:
       
    79 		//
       
    80 		// * `pattern` is the regular expression used to scan the content and
       
    81 		// detect matching views.
       
    82 		//
       
    83 		// * `view` is a `Backbone.View` constructor. If a plain object is
       
    84 		// provided, it will automatically extend the parent constructor
       
    85 		// (usually `Backbone.View`). Views are instantiated when the `pattern`
       
    86 		// is successfully matched. The instance's `options` object is provided
       
    87 		// with the `original` matched value, the match `results` including
       
    88 		// capture groups, and the `viewType`, which is the constructor's `id`.
       
    89 		//
       
    90 		// * `extend` an existing view by passing in its `id`. The current
       
    91 		// view will inherit all properties from the parent view, and if
       
    92 		// `view` is set to a plain object, it will extend the parent `view`
       
    93 		// constructor.
       
    94 		//
       
    95 		// * `text` is a method that accepts an instance of the `view`
       
    96 		// constructor and transforms it into a text representation.
       
    97 		add: function( id, options ) {
       
    98 			var parent, remove, base, properties;
       
    99 
       
   100 			// Fetch the parent view or the default options.
       
   101 			if ( options.extend )
       
   102 				parent = wp.mce.view.get( options.extend );
       
   103 			else if ( options.shortcode )
       
   104 				parent = wp.mce.view.defaults.shortcode;
       
   105 			else
       
   106 				parent = wp.mce.view.defaults.pattern;
       
   107 
       
   108 			// Extend the `options` object with the parent's properties.
       
   109 			_.defaults( options, parent );
       
   110 			options.id = id;
       
   111 
       
   112 			// Create properties used to enhance the view for use in TinyMCE.
       
   113 			properties = {
       
   114 				// Ensure the wrapper element and references to the view are
       
   115 				// removed. Otherwise, removed views could randomly restore.
       
   116 				remove: function() {
       
   117 					delete instances[ this.el.id ];
       
   118 					this.$el.parent().remove();
       
   119 
       
   120 					// Trigger the inherited `remove` method.
       
   121 					if ( remove )
       
   122 						remove.apply( this, arguments );
       
   123 
       
   124 					return this;
       
   125 				}
       
   126 			};
       
   127 
       
   128 			// If the `view` provided was an object, use the parent's
       
   129 			// `view` constructor as a base. If a `view` constructor
       
   130 			// was provided, treat that as the base.
       
   131 			if ( _.isFunction( options.view ) ) {
       
   132 				base = options.view;
       
   133 			} else {
       
   134 				base   = parent.view;
       
   135 				remove = options.view.remove;
       
   136 				_.defaults( properties, options.view );
       
   137 			}
       
   138 
       
   139 			// If there's a `remove` method on the `base` view that wasn't
       
   140 			// created by this method, inherit it.
       
   141 			if ( ! remove && ! base._mceview )
       
   142 				remove = base.prototype.remove;
       
   143 
       
   144 			// Automatically create the new `Backbone.View` constructor.
       
   145 			options.view = base.extend( properties, {
       
   146 				// Flag that the new view has been created by `wp.mce.view`.
       
   147 				_mceview: true
       
   148 			});
       
   149 
       
   150 			views[ id ] = options;
       
   151 		},
       
   152 
       
   153 		// ### get( id )
       
   154 		// Returns a TinyMCE view options object.
       
   155 		get: function( id ) {
       
   156 			return views[ id ];
       
   157 		},
       
   158 
       
   159 		// ### remove( id )
       
   160 		// Unregisters a TinyMCE view.
       
   161 		remove: function( id ) {
       
   162 			delete views[ id ];
       
   163 		},
       
   164 
       
   165 		// ### toViews( content )
       
   166 		// Scans a `content` string for each view's pattern, replacing any
       
   167 		// matches with wrapper elements, and creates a new view instance for
       
   168 		// every match.
       
   169 		//
       
   170 		// To render the views, call `wp.mce.view.render( scope )`.
       
   171 		toViews: function( content ) {
       
   172 			var pieces = [ { content: content } ],
       
   173 				current;
       
   174 
       
   175 			_.each( views, function( view, viewType ) {
       
   176 				current = pieces.slice();
       
   177 				pieces  = [];
       
   178 
       
   179 				_.each( current, function( piece ) {
       
   180 					var remaining = piece.content,
       
   181 						result;
       
   182 
       
   183 					// Ignore processed pieces, but retain their location.
       
   184 					if ( piece.processed ) {
       
   185 						pieces.push( piece );
       
   186 						return;
       
   187 					}
       
   188 
       
   189 					// Iterate through the string progressively matching views
       
   190 					// and slicing the string as we go.
       
   191 					while ( remaining && (result = view.toView( remaining )) ) {
       
   192 						// Any text before the match becomes an unprocessed piece.
       
   193 						if ( result.index )
       
   194 							pieces.push({ content: remaining.substring( 0, result.index ) });
       
   195 
       
   196 						// Add the processed piece for the match.
       
   197 						pieces.push({
       
   198 							content:   wp.mce.view.toView( viewType, result.options ),
       
   199 							processed: true
       
   200 						});
       
   201 
       
   202 						// Update the remaining content.
       
   203 						remaining = remaining.slice( result.index + result.content.length );
       
   204 					}
       
   205 
       
   206 					// There are no additional matches. If any content remains,
       
   207 					// add it as an unprocessed piece.
       
   208 					if ( remaining )
       
   209 						pieces.push({ content: remaining });
       
   210 				});
       
   211 			});
       
   212 
       
   213 			return _.pluck( pieces, 'content' ).join('');
       
   214 		},
       
   215 
       
   216 		toView: function( viewType, options ) {
       
   217 			var view = wp.mce.view.get( viewType ),
       
   218 				instance, id;
       
   219 
       
   220 			if ( ! view )
       
   221 				return '';
       
   222 
       
   223 			// Create a new view instance.
       
   224 			instance = new view.view( _.extend( options || {}, {
       
   225 				viewType: viewType
       
   226 			}) );
       
   227 
       
   228 			// Use the view's `id` if it already exists. Otherwise,
       
   229 			// create a new `id`.
       
   230 			id = instance.el.id = instance.el.id || _.uniqueId('__wpmce-');
       
   231 			instances[ id ] = instance;
       
   232 
       
   233 			// Create a dummy `$wrapper` property to allow `$wrapper` to be
       
   234 			// called in the view's `render` method without a conditional.
       
   235 			instance.$wrapper = $();
       
   236 
       
   237 			return wp.html.string({
       
   238 				// If the view is a span, wrap it in a span.
       
   239 				tag: 'span' === instance.tagName ? 'span' : 'div',
       
   240 
       
   241 				attrs: {
       
   242 					'class':           'wp-view-wrap wp-view-type-' + viewType,
       
   243 					'data-wp-view':    id,
       
   244 					'contenteditable': false
       
   245 				}
       
   246 			});
       
   247 		},
       
   248 
       
   249 		// ### render( scope )
       
   250 		// Renders any view instances inside a DOM node `scope`.
       
   251 		//
       
   252 		// View instances are detected by the presence of wrapper elements.
       
   253 		// To generate wrapper elements, pass your content through
       
   254 		// `wp.mce.view.toViews( content )`.
       
   255 		render: function( scope ) {
       
   256 			$( '.wp-view-wrap', scope ).each( function() {
       
   257 				var wrapper = $(this),
       
   258 					view = wp.mce.view.instance( this );
       
   259 
       
   260 				if ( ! view )
       
   261 					return;
       
   262 
       
   263 				// Link the real wrapper to the view.
       
   264 				view.$wrapper = wrapper;
       
   265 				// Render the view.
       
   266 				view.render();
       
   267 				// Detach the view element to ensure events are not unbound.
       
   268 				view.$el.detach();
       
   269 
       
   270 				// Empty the wrapper, attach the view element to the wrapper,
       
   271 				// and add an ending marker to the wrapper to help regexes
       
   272 				// scan the HTML string.
       
   273 				wrapper.empty().append( view.el ).append('<span data-wp-view-end class="wp-view-end"></span>');
       
   274 			});
       
   275 		},
       
   276 
       
   277 		// ### toText( content )
       
   278 		// Scans an HTML `content` string and replaces any view instances with
       
   279 		// their respective text representations.
       
   280 		toText: function( content ) {
       
   281 			return content.replace( /<(?:div|span)[^>]+data-wp-view="([^"]+)"[^>]*>.*?<span[^>]+data-wp-view-end[^>]*><\/span><\/(?:div|span)>/g, function( match, id ) {
       
   282 				var instance = instances[ id ],
       
   283 					view;
       
   284 
       
   285 				if ( instance )
       
   286 					view = wp.mce.view.get( instance.options.viewType );
       
   287 
       
   288 				return instance && view ? view.text( instance ) : '';
       
   289 			});
       
   290 		},
       
   291 
       
   292 		// ### Remove internal TinyMCE attributes.
       
   293 		removeInternalAttrs: function( attrs ) {
       
   294 			var result = {};
       
   295 			_.each( attrs, function( value, attr ) {
       
   296 				if ( -1 === attr.indexOf('data-mce') )
       
   297 					result[ attr ] = value;
       
   298 			});
       
   299 			return result;
       
   300 		},
       
   301 
       
   302 		// ### Parse an attribute string and removes internal TinyMCE attributes.
       
   303 		attrs: function( content ) {
       
   304 			return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
       
   305 		},
       
   306 
       
   307 		// ### instance( scope )
       
   308 		//
       
   309 		// Accepts a MCE view wrapper `node` (i.e. a node with the
       
   310 		// `wp-view-wrap` class).
       
   311 		instance: function( node ) {
       
   312 			var id = $( node ).data('wp-view');
       
   313 
       
   314 			if ( id )
       
   315 				return instances[ id ];
       
   316 		},
       
   317 
       
   318 		// ### Select a view.
       
   319 		//
       
   320 		// Accepts a MCE view wrapper `node` (i.e. a node with the
       
   321 		// `wp-view-wrap` class).
       
   322 		select: function( node ) {
       
   323 			var $node = $(node);
       
   324 
       
   325 			// Bail if node is already selected.
       
   326 			if ( $node.hasClass('selected') )
       
   327 				return;
       
   328 
       
   329 			$node.addClass('selected');
       
   330 			$( node.firstChild ).trigger('select');
       
   331 		},
       
   332 
       
   333 		// ### Deselect a view.
       
   334 		//
       
   335 		// Accepts a MCE view wrapper `node` (i.e. a node with the
       
   336 		// `wp-view-wrap` class).
       
   337 		deselect: function( node ) {
       
   338 			var $node = $(node);
       
   339 
       
   340 			// Bail if node is already selected.
       
   341 			if ( ! $node.hasClass('selected') )
       
   342 				return;
       
   343 
       
   344 			$node.removeClass('selected');
       
   345 			$( node.firstChild ).trigger('deselect');
       
   346 		}
       
   347 	};
       
   348 
       
   349 }(jQuery));