|
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)); |