1 // Ensure the global `wp` object exists.
2 window.wp = window.wp || {};
8 // Create the `wp.mce` object if necessary.
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.
19 // The default properties used for objects with the `pattern` key in
20 // `wp.mce.view.add()`.
23 text: function( instance ) {
24 return instance.options.original;
27 toView: function( content ) {
31 this.pattern.lastIndex = 0;
32 var match = this.pattern.exec( content );
48 // The default properties used for objects with the `shortcode` key in
49 // `wp.mce.view.add()`.
52 text: function( instance ) {
53 return instance.options.shortcode.string();
56 toView: function( content ) {
57 var match = wp.shortcode.next( this.shortcode, content );
64 content: match.content,
66 shortcode: match.shortcode
73 // ### add( id, options )
74 // Registers a new TinyMCE view.
76 // Accepts a unique `id` and an `options` object.
78 // `options` accepts the following properties:
80 // * `pattern` is the regular expression used to scan the content and
81 // detect matching views.
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`.
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`
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;
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;
106 parent = wp.mce.view.defaults.pattern;
108 // Extend the `options` object with the parent's properties.
109 _.defaults( options, parent );
112 // Create properties used to enhance the view for use in TinyMCE.
114 // Ensure the wrapper element and references to the view are
115 // removed. Otherwise, removed views could randomly restore.
117 delete instances[ this.el.id ];
118 this.$el.parent().remove();
120 // Trigger the inherited `remove` method.
122 remove.apply( this, arguments );
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 ) ) {
135 remove = options.view.remove;
136 _.defaults( properties, options.view );
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;
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`.
150 views[ id ] = options;
154 // Returns a TinyMCE view options object.
155 get: function( id ) {
160 // Unregisters a TinyMCE view.
161 remove: function( id ) {
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
170 // To render the views, call `wp.mce.view.render( scope )`.
171 toViews: function( content ) {
172 var pieces = [ { content: content } ],
175 _.each( views, function( view, viewType ) {
176 current = pieces.slice();
179 _.each( current, function( piece ) {
180 var remaining = piece.content,
183 // Ignore processed pieces, but retain their location.
184 if ( piece.processed ) {
185 pieces.push( piece );
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.
194 pieces.push({ content: remaining.substring( 0, result.index ) });
196 // Add the processed piece for the match.
198 content: wp.mce.view.toView( viewType, result.options ),
202 // Update the remaining content.
203 remaining = remaining.slice( result.index + result.content.length );
206 // There are no additional matches. If any content remains,
207 // add it as an unprocessed piece.
209 pieces.push({ content: remaining });
213 return _.pluck( pieces, 'content' ).join('');
216 toView: function( viewType, options ) {
217 var view = wp.mce.view.get( viewType ),
223 // Create a new view instance.
224 instance = new view.view( _.extend( options || {}, {
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;
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 = $();
237 return wp.html.string({
238 // If the view is a span, wrap it in a span.
239 tag: 'span' === instance.tagName ? 'span' : 'div',
242 'class': 'wp-view-wrap wp-view-type-' + viewType,
244 'contenteditable': false
249 // ### render( scope )
250 // Renders any view instances inside a DOM node `scope`.
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 );
263 // Link the real wrapper to the view.
264 view.$wrapper = wrapper;
267 // Detach the view element to ensure events are not unbound.
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>');
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 ],
286 view = wp.mce.view.get( instance.options.viewType );
288 return instance && view ? view.text( instance ) : '';
292 // ### Remove internal TinyMCE attributes.
293 removeInternalAttrs: function( attrs ) {
295 _.each( attrs, function( value, attr ) {
296 if ( -1 === attr.indexOf('data-mce') )
297 result[ attr ] = value;
302 // ### Parse an attribute string and removes internal TinyMCE attributes.
303 attrs: function( content ) {
304 return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
307 // ### instance( scope )
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');
315 return instances[ id ];
318 // ### Select a view.
320 // Accepts a MCE view wrapper `node` (i.e. a node with the
321 // `wp-view-wrap` class).
322 select: function( node ) {
325 // Bail if node is already selected.
326 if ( $node.hasClass('selected') )
329 $node.addClass('selected');
330 $( node.firstChild ).trigger('select');
333 // ### Deselect a view.
335 // Accepts a MCE view wrapper `node` (i.e. a node with the
336 // `wp-view-wrap` class).
337 deselect: function( node ) {
340 // Bail if node is already selected.
341 if ( ! $node.hasClass('selected') )
344 $node.removeClass('selected');
345 $( node.firstChild ).trigger('deselect');