5
|
1 |
/* global tinymce */ |
|
2 |
|
0
|
3 |
window.wp = window.wp || {}; |
|
4 |
|
5
|
5 |
/* |
|
6 |
* The TinyMCE view API. |
|
7 |
* |
|
8 |
* Note: this API is "experimental" meaning that it will probably change |
|
9 |
* in the next few releases based on feedback from 3.9.0. |
|
10 |
* If you decide to use it, please follow the development closely. |
|
11 |
* |
|
12 |
* Diagram |
|
13 |
* |
|
14 |
* |- registered view constructor (type) |
|
15 |
* | |- view instance (unique text) |
|
16 |
* | | |- editor 1 |
|
17 |
* | | | |- view node |
|
18 |
* | | | |- view node |
|
19 |
* | | | |- ... |
|
20 |
* | | |- editor 2 |
|
21 |
* | | | |- ... |
|
22 |
* | |- view instance |
|
23 |
* | | |- ... |
|
24 |
* |- registered view |
|
25 |
* | |- ... |
|
26 |
*/ |
|
27 |
( function( window, wp, $ ) { |
|
28 |
'use strict'; |
|
29 |
|
0
|
30 |
var views = {}, |
|
31 |
instances = {}; |
|
32 |
|
|
33 |
wp.mce = wp.mce || {}; |
|
34 |
|
5
|
35 |
/** |
|
36 |
* wp.mce.views |
|
37 |
* |
|
38 |
* A set of utilities that simplifies adding custom UI within a TinyMCE editor. |
|
39 |
* At its core, it serves as a series of converters, transforming text to a |
|
40 |
* custom UI, and back again. |
|
41 |
*/ |
|
42 |
wp.mce.views = { |
0
|
43 |
|
5
|
44 |
/** |
|
45 |
* Registers a new view type. |
|
46 |
* |
|
47 |
* @param {String} type The view type. |
|
48 |
* @param {Object} extend An object to extend wp.mce.View.prototype with. |
|
49 |
*/ |
|
50 |
register: function( type, extend ) { |
|
51 |
views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) ); |
|
52 |
}, |
0
|
53 |
|
5
|
54 |
/** |
|
55 |
* Unregisters a view type. |
|
56 |
* |
|
57 |
* @param {String} type The view type. |
|
58 |
*/ |
|
59 |
unregister: function( type ) { |
|
60 |
delete views[ type ]; |
0
|
61 |
}, |
|
62 |
|
5
|
63 |
/** |
|
64 |
* Returns the settings of a view type. |
|
65 |
* |
|
66 |
* @param {String} type The view type. |
|
67 |
* |
|
68 |
* @return {Function} The view constructor. |
|
69 |
*/ |
|
70 |
get: function( type ) { |
|
71 |
return views[ type ]; |
0
|
72 |
}, |
|
73 |
|
5
|
74 |
/** |
|
75 |
* Unbinds all view nodes. |
|
76 |
* Runs before removing all view nodes from the DOM. |
|
77 |
*/ |
|
78 |
unbind: function() { |
|
79 |
_.each( instances, function( instance ) { |
|
80 |
instance.unbind(); |
|
81 |
} ); |
0
|
82 |
}, |
|
83 |
|
5
|
84 |
/** |
|
85 |
* Scans a given string for each view's pattern, |
|
86 |
* replacing any matches with markers, |
|
87 |
* and creates a new instance for every match. |
|
88 |
* |
|
89 |
* @param {String} content The string to scan. |
|
90 |
* |
|
91 |
* @return {String} The string with markers. |
|
92 |
*/ |
|
93 |
setMarkers: function( content ) { |
0
|
94 |
var pieces = [ { content: content } ], |
5
|
95 |
self = this, |
|
96 |
instance, |
0
|
97 |
current; |
|
98 |
|
5
|
99 |
_.each( views, function( view, type ) { |
0
|
100 |
current = pieces.slice(); |
|
101 |
pieces = []; |
|
102 |
|
|
103 |
_.each( current, function( piece ) { |
|
104 |
var remaining = piece.content, |
|
105 |
result; |
|
106 |
|
|
107 |
// Ignore processed pieces, but retain their location. |
|
108 |
if ( piece.processed ) { |
|
109 |
pieces.push( piece ); |
|
110 |
return; |
|
111 |
} |
|
112 |
|
|
113 |
// Iterate through the string progressively matching views |
|
114 |
// and slicing the string as we go. |
5
|
115 |
while ( remaining && ( result = view.prototype.match( remaining ) ) ) { |
0
|
116 |
// Any text before the match becomes an unprocessed piece. |
5
|
117 |
if ( result.index ) { |
|
118 |
pieces.push( { content: remaining.substring( 0, result.index ) } ); |
|
119 |
} |
|
120 |
|
|
121 |
instance = self.createInstance( type, result.content, result.options ); |
0
|
122 |
|
|
123 |
// Add the processed piece for the match. |
5
|
124 |
pieces.push( { |
|
125 |
content: '<p data-wpview-marker="' + instance.encodedText + '">' + instance.text + '</p>', |
0
|
126 |
processed: true |
5
|
127 |
} ); |
0
|
128 |
|
|
129 |
// Update the remaining content. |
|
130 |
remaining = remaining.slice( result.index + result.content.length ); |
|
131 |
} |
|
132 |
|
5
|
133 |
// There are no additional matches. |
|
134 |
// If any content remains, add it as an unprocessed piece. |
|
135 |
if ( remaining ) { |
|
136 |
pieces.push( { content: remaining } ); |
|
137 |
} |
|
138 |
} ); |
|
139 |
} ); |
0
|
140 |
|
5
|
141 |
return _.pluck( pieces, 'content' ).join( '' ); |
0
|
142 |
}, |
|
143 |
|
5
|
144 |
/** |
|
145 |
* Create a view instance. |
|
146 |
* |
|
147 |
* @param {String} type The view type. |
|
148 |
* @param {String} text The textual representation of the view. |
|
149 |
* @param {Object} options Options. |
|
150 |
* |
|
151 |
* @return {wp.mce.View} The view instance. |
|
152 |
*/ |
|
153 |
createInstance: function( type, text, options ) { |
|
154 |
var View = this.get( type ), |
|
155 |
encodedText, |
|
156 |
instance; |
0
|
157 |
|
5
|
158 |
text = tinymce.DOM.decode( text ), |
|
159 |
encodedText = encodeURIComponent( text ), |
|
160 |
instance = this.getInstance( encodedText ); |
0
|
161 |
|
5
|
162 |
if ( instance ) { |
|
163 |
return instance; |
|
164 |
} |
0
|
165 |
|
5
|
166 |
options = _.extend( options || {}, { |
|
167 |
text: text, |
|
168 |
encodedText: encodedText |
|
169 |
} ); |
0
|
170 |
|
5
|
171 |
return instances[ encodedText ] = new View( options ); |
|
172 |
}, |
0
|
173 |
|
5
|
174 |
/** |
|
175 |
* Get a view instance. |
|
176 |
* |
|
177 |
* @param {(String|HTMLElement)} object The textual representation of the view or the view node. |
|
178 |
* |
|
179 |
* @return {wp.mce.View} The view instance or undefined. |
|
180 |
*/ |
|
181 |
getInstance: function( object ) { |
|
182 |
if ( typeof object === 'string' ) { |
|
183 |
return instances[ encodeURIComponent( object ) ]; |
|
184 |
} |
0
|
185 |
|
5
|
186 |
return instances[ $( object ).attr( 'data-wpview-text' ) ]; |
0
|
187 |
}, |
|
188 |
|
5
|
189 |
/** |
|
190 |
* Given a view node, get the view's text. |
|
191 |
* |
|
192 |
* @param {HTMLElement} node The view node. |
|
193 |
* |
|
194 |
* @return {String} The textual representation of the view. |
|
195 |
*/ |
|
196 |
getText: function( node ) { |
|
197 |
return decodeURIComponent( $( node ).attr( 'data-wpview-text' ) || '' ); |
0
|
198 |
}, |
|
199 |
|
5
|
200 |
/** |
|
201 |
* Renders all view nodes that are not yet rendered. |
|
202 |
* |
|
203 |
* @param {Boolean} force Rerender all view nodes. |
|
204 |
*/ |
|
205 |
render: function( force ) { |
|
206 |
_.each( instances, function( instance ) { |
|
207 |
instance.render( force ); |
|
208 |
} ); |
0
|
209 |
}, |
|
210 |
|
5
|
211 |
/** |
|
212 |
* Update the text of a given view node. |
|
213 |
* |
|
214 |
* @param {String} text The new text. |
|
215 |
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. |
|
216 |
* @param {HTMLElement} node The view node to update. |
|
217 |
*/ |
|
218 |
update: function( text, editor, node ) { |
|
219 |
var instance = this.getInstance( node ); |
0
|
220 |
|
5
|
221 |
if ( instance ) { |
|
222 |
instance.update( text, editor, node ); |
|
223 |
} |
0
|
224 |
}, |
|
225 |
|
5
|
226 |
/** |
|
227 |
* Renders any editing interface based on the view type. |
|
228 |
* |
|
229 |
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. |
|
230 |
* @param {HTMLElement} node The view node to edit. |
|
231 |
*/ |
|
232 |
edit: function( editor, node ) { |
|
233 |
var instance = this.getInstance( node ); |
0
|
234 |
|
5
|
235 |
if ( instance && instance.edit ) { |
|
236 |
instance.edit( instance.text, function( text ) { |
|
237 |
instance.update( text, editor, node ); |
|
238 |
} ); |
|
239 |
} |
0
|
240 |
}, |
|
241 |
|
5
|
242 |
/** |
|
243 |
* Remove a given view node from the DOM. |
|
244 |
* |
|
245 |
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. |
|
246 |
* @param {HTMLElement} node The view node to remove. |
|
247 |
*/ |
|
248 |
remove: function( editor, node ) { |
|
249 |
var instance = this.getInstance( node ); |
0
|
250 |
|
5
|
251 |
if ( instance ) { |
|
252 |
instance.remove( editor, node ); |
|
253 |
} |
0
|
254 |
} |
|
255 |
}; |
|
256 |
|
5
|
257 |
/** |
|
258 |
* A Backbone-like View constructor intended for use when rendering a TinyMCE View. |
|
259 |
* The main difference is that the TinyMCE View is not tied to a particular DOM node. |
|
260 |
* |
|
261 |
* @param {Object} options Options. |
|
262 |
*/ |
|
263 |
wp.mce.View = function( options ) { |
|
264 |
_.extend( this, options ); |
|
265 |
this.initialize(); |
|
266 |
}; |
|
267 |
|
|
268 |
wp.mce.View.extend = Backbone.View.extend; |
|
269 |
|
|
270 |
_.extend( wp.mce.View.prototype, { |
|
271 |
|
|
272 |
/** |
|
273 |
* The content. |
|
274 |
* |
|
275 |
* @type {*} |
|
276 |
*/ |
|
277 |
content: null, |
|
278 |
|
|
279 |
/** |
|
280 |
* Whether or not to display a loader. |
|
281 |
* |
|
282 |
* @type {Boolean} |
|
283 |
*/ |
|
284 |
loader: true, |
|
285 |
|
|
286 |
/** |
|
287 |
* Runs after the view instance is created. |
|
288 |
*/ |
|
289 |
initialize: function() {}, |
|
290 |
|
|
291 |
/** |
|
292 |
* Retuns the content to render in the view node. |
|
293 |
* |
|
294 |
* @return {*} |
|
295 |
*/ |
|
296 |
getContent: function() { |
|
297 |
return this.content; |
|
298 |
}, |
|
299 |
|
|
300 |
/** |
|
301 |
* Renders all view nodes tied to this view instance that are not yet rendered. |
|
302 |
* |
|
303 |
* @param {String} content The content to render. Optional. |
|
304 |
* @param {Boolean} force Rerender all view nodes tied to this view instance. |
|
305 |
*/ |
|
306 |
render: function( content, force ) { |
|
307 |
if ( content != null ) { |
|
308 |
this.content = content; |
|
309 |
} |
|
310 |
|
|
311 |
content = this.getContent(); |
|
312 |
|
|
313 |
// If there's nothing to render an no loader needs to be shown, stop. |
|
314 |
if ( ! this.loader && ! content ) { |
|
315 |
return; |
|
316 |
} |
|
317 |
|
|
318 |
// We're about to rerender all views of this instance, so unbind rendered views. |
|
319 |
force && this.unbind(); |
|
320 |
|
|
321 |
// Replace any left over markers. |
|
322 |
this.replaceMarkers(); |
|
323 |
|
|
324 |
if ( content ) { |
|
325 |
this.setContent( content, function( editor, node, contentNode ) { |
|
326 |
$( node ).data( 'rendered', true ); |
|
327 |
this.bindNode.call( this, editor, node, contentNode ); |
|
328 |
}, force ? null : false ); |
|
329 |
} else { |
|
330 |
this.setLoader(); |
|
331 |
} |
|
332 |
}, |
|
333 |
|
|
334 |
/** |
|
335 |
* Binds a given node after its content is added to the DOM. |
|
336 |
*/ |
|
337 |
bindNode: function() {}, |
|
338 |
|
|
339 |
/** |
|
340 |
* Unbinds a given node before its content is removed from the DOM. |
|
341 |
*/ |
|
342 |
unbindNode: function() {}, |
|
343 |
|
|
344 |
/** |
|
345 |
* Unbinds all view nodes tied to this view instance. |
|
346 |
* Runs before their content is removed from the DOM. |
|
347 |
*/ |
|
348 |
unbind: function() { |
|
349 |
this.getNodes( function( editor, node, contentNode ) { |
|
350 |
this.unbindNode.call( this, editor, node, contentNode ); |
|
351 |
$( node ).trigger( 'wp-mce-view-unbind' ); |
|
352 |
}, true ); |
|
353 |
}, |
|
354 |
|
|
355 |
/** |
|
356 |
* Gets all the TinyMCE editor instances that support views. |
|
357 |
* |
|
358 |
* @param {Function} callback A callback. |
|
359 |
*/ |
|
360 |
getEditors: function( callback ) { |
|
361 |
_.each( tinymce.editors, function( editor ) { |
|
362 |
if ( editor.plugins.wpview ) { |
|
363 |
callback.call( this, editor ); |
|
364 |
} |
|
365 |
}, this ); |
|
366 |
}, |
|
367 |
|
|
368 |
/** |
|
369 |
* Gets all view nodes tied to this view instance. |
|
370 |
* |
|
371 |
* @param {Function} callback A callback. |
|
372 |
* @param {Boolean} rendered Get (un)rendered view nodes. Optional. |
|
373 |
*/ |
|
374 |
getNodes: function( callback, rendered ) { |
|
375 |
this.getEditors( function( editor ) { |
|
376 |
var self = this; |
|
377 |
|
|
378 |
$( editor.getBody() ) |
|
379 |
.find( '[data-wpview-text="' + self.encodedText + '"]' ) |
|
380 |
.filter( function() { |
|
381 |
var data; |
|
382 |
|
|
383 |
if ( rendered == null ) { |
|
384 |
return true; |
|
385 |
} |
|
386 |
|
|
387 |
data = $( this ).data( 'rendered' ) === true; |
|
388 |
|
|
389 |
return rendered ? data : ! data; |
|
390 |
} ) |
|
391 |
.each( function() { |
|
392 |
callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) ); |
|
393 |
} ); |
|
394 |
} ); |
|
395 |
}, |
|
396 |
|
|
397 |
/** |
|
398 |
* Gets all marker nodes tied to this view instance. |
|
399 |
* |
|
400 |
* @param {Function} callback A callback. |
|
401 |
*/ |
|
402 |
getMarkers: function( callback ) { |
|
403 |
this.getEditors( function( editor ) { |
|
404 |
var self = this; |
|
405 |
|
|
406 |
$( editor.getBody() ) |
|
407 |
.find( '[data-wpview-marker="' + this.encodedText + '"]' ) |
|
408 |
.each( function() { |
|
409 |
callback.call( self, editor, this ); |
|
410 |
} ); |
|
411 |
} ); |
|
412 |
}, |
|
413 |
|
|
414 |
/** |
|
415 |
* Replaces all marker nodes tied to this view instance. |
|
416 |
*/ |
|
417 |
replaceMarkers: function() { |
|
418 |
this.getMarkers( function( editor, node ) { |
|
419 |
if ( $( node ).text() !== this.text ) { |
|
420 |
editor.dom.setAttrib( node, 'data-wpview-marker', null ); |
|
421 |
return; |
|
422 |
} |
|
423 |
|
|
424 |
editor.dom.replace( |
|
425 |
editor.dom.createFragment( |
|
426 |
'<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' + |
|
427 |
'<p class="wpview-selection-before">\u00a0</p>' + |
|
428 |
'<div class="wpview-body" contenteditable="false">' + |
|
429 |
'<div class="wpview-content wpview-type-' + this.type + '"></div>' + |
|
430 |
'</div>' + |
|
431 |
'<p class="wpview-selection-after">\u00a0</p>' + |
|
432 |
'</div>' |
|
433 |
), |
|
434 |
node |
|
435 |
); |
|
436 |
} ); |
|
437 |
}, |
|
438 |
|
|
439 |
/** |
|
440 |
* Removes all marker nodes tied to this view instance. |
|
441 |
*/ |
|
442 |
removeMarkers: function() { |
|
443 |
this.getMarkers( function( editor, node ) { |
|
444 |
editor.dom.setAttrib( node, 'data-wpview-marker', null ); |
|
445 |
} ); |
|
446 |
}, |
|
447 |
|
|
448 |
/** |
|
449 |
* Sets the content for all view nodes tied to this view instance. |
|
450 |
* |
|
451 |
* @param {*} content The content to set. |
|
452 |
* @param {Function} callback A callback. Optional. |
|
453 |
* @param {Boolean} rendered Only set for (un)rendered nodes. Optional. |
|
454 |
*/ |
|
455 |
setContent: function( content, callback, rendered ) { |
|
456 |
if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) { |
|
457 |
this.setIframes( content.head || '', content.body, callback, rendered ); |
|
458 |
} else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) { |
|
459 |
this.setIframes( '', content, callback, rendered ); |
|
460 |
} else { |
|
461 |
this.getNodes( function( editor, node, contentNode ) { |
|
462 |
content = content.body || content; |
|
463 |
|
|
464 |
if ( content.indexOf( '<iframe' ) !== -1 ) { |
|
465 |
content += '<div class="wpview-overlay"></div>'; |
|
466 |
} |
|
467 |
|
|
468 |
contentNode.innerHTML = ''; |
|
469 |
contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content ); |
|
470 |
|
|
471 |
callback && callback.call( this, editor, node, contentNode ); |
|
472 |
}, rendered ); |
|
473 |
} |
|
474 |
}, |
|
475 |
|
|
476 |
/** |
|
477 |
* Sets the content in an iframe for all view nodes tied to this view instance. |
|
478 |
* |
|
479 |
* @param {String} head HTML string to be added to the head of the document. |
|
480 |
* @param {String} body HTML string to be added to the body of the document. |
|
481 |
* @param {Function} callback A callback. Optional. |
|
482 |
* @param {Boolean} rendered Only set for (un)rendered nodes. Optional. |
|
483 |
*/ |
|
484 |
setIframes: function( head, body, callback, rendered ) { |
|
485 |
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, |
|
486 |
self = this; |
|
487 |
|
|
488 |
this.getNodes( function( editor, node, contentNode ) { |
|
489 |
var dom = editor.dom, |
|
490 |
styles = '', |
|
491 |
bodyClasses = editor.getBody().className || '', |
|
492 |
editorHead = editor.getDoc().getElementsByTagName( 'head' )[0]; |
|
493 |
|
|
494 |
tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( link ) { |
|
495 |
if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 && |
|
496 |
link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) { |
|
497 |
|
|
498 |
styles += dom.getOuterHTML( link ); |
|
499 |
} |
|
500 |
} ); |
|
501 |
|
|
502 |
// Seems the browsers need a bit of time to insert/set the view nodes, |
|
503 |
// or the iframe will fail especially when switching Text => Visual. |
|
504 |
setTimeout( function() { |
|
505 |
var iframe, iframeDoc, observer, i; |
|
506 |
|
|
507 |
contentNode.innerHTML = ''; |
|
508 |
|
|
509 |
iframe = dom.add( contentNode, 'iframe', { |
|
510 |
/* jshint scripturl: true */ |
|
511 |
src: tinymce.Env.ie ? 'javascript:""' : '', |
|
512 |
frameBorder: '0', |
|
513 |
allowTransparency: 'true', |
|
514 |
scrolling: 'no', |
|
515 |
'class': 'wpview-sandbox', |
|
516 |
style: { |
|
517 |
width: '100%', |
|
518 |
display: 'block' |
|
519 |
} |
|
520 |
} ); |
|
521 |
|
|
522 |
dom.add( contentNode, 'div', { 'class': 'wpview-overlay' } ); |
|
523 |
|
|
524 |
iframeDoc = iframe.contentWindow.document; |
|
525 |
|
|
526 |
iframeDoc.open(); |
|
527 |
|
|
528 |
iframeDoc.write( |
|
529 |
'<!DOCTYPE html>' + |
|
530 |
'<html>' + |
|
531 |
'<head>' + |
|
532 |
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' + |
|
533 |
head + |
|
534 |
styles + |
|
535 |
'<style>' + |
|
536 |
'html {' + |
|
537 |
'background: transparent;' + |
|
538 |
'padding: 0;' + |
|
539 |
'margin: 0;' + |
|
540 |
'}' + |
|
541 |
'body#wpview-iframe-sandbox {' + |
|
542 |
'background: transparent;' + |
|
543 |
'padding: 1px 0 !important;' + |
|
544 |
'margin: -1px 0 0 !important;' + |
|
545 |
'}' + |
|
546 |
'body#wpview-iframe-sandbox:before,' + |
|
547 |
'body#wpview-iframe-sandbox:after {' + |
|
548 |
'display: none;' + |
|
549 |
'content: "";' + |
|
550 |
'}' + |
|
551 |
'</style>' + |
|
552 |
'</head>' + |
|
553 |
'<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' + |
|
554 |
body + |
|
555 |
'</body>' + |
|
556 |
'</html>' |
|
557 |
); |
|
558 |
|
|
559 |
iframeDoc.close(); |
|
560 |
|
|
561 |
function resize() { |
|
562 |
var $iframe, iframeDocHeight; |
|
563 |
|
|
564 |
// Make sure the iframe still exists. |
|
565 |
if ( iframe.contentWindow ) { |
|
566 |
$iframe = $( iframe ); |
|
567 |
iframeDocHeight = $( iframeDoc.body ).height(); |
|
568 |
|
|
569 |
if ( $iframe.height() !== iframeDocHeight ) { |
|
570 |
$iframe.height( iframeDocHeight ); |
|
571 |
editor.nodeChanged(); |
|
572 |
} |
|
573 |
} |
|
574 |
} |
|
575 |
|
|
576 |
$( iframe.contentWindow ).on( 'load', resize ); |
|
577 |
|
|
578 |
if ( MutationObserver ) { |
|
579 |
observer = new MutationObserver( _.debounce( resize, 100 ) ); |
|
580 |
|
|
581 |
observer.observe( iframeDoc.body, { |
|
582 |
attributes: true, |
|
583 |
childList: true, |
|
584 |
subtree: true |
|
585 |
} ); |
|
586 |
|
|
587 |
$( node ).one( 'wp-mce-view-unbind', function() { |
|
588 |
observer.disconnect(); |
|
589 |
} ); |
|
590 |
} else { |
|
591 |
for ( i = 1; i < 6; i++ ) { |
|
592 |
setTimeout( resize, i * 700 ); |
|
593 |
} |
|
594 |
} |
|
595 |
|
|
596 |
function classChange() { |
|
597 |
iframeDoc.body.className = editor.getBody().className; |
|
598 |
} |
|
599 |
|
|
600 |
editor.on( 'wp-body-class-change', classChange ); |
|
601 |
|
|
602 |
$( node ).one( 'wp-mce-view-unbind', function() { |
|
603 |
editor.off( 'wp-body-class-change', classChange ); |
|
604 |
} ); |
|
605 |
|
|
606 |
callback && callback.call( self, editor, node, contentNode ); |
|
607 |
}, 50 ); |
|
608 |
}, rendered ); |
|
609 |
}, |
|
610 |
|
|
611 |
/** |
|
612 |
* Sets a loader for all view nodes tied to this view instance. |
|
613 |
*/ |
|
614 |
setLoader: function() { |
|
615 |
this.setContent( |
|
616 |
'<div class="loading-placeholder">' + |
|
617 |
'<div class="dashicons dashicons-admin-media"></div>' + |
|
618 |
'<div class="wpview-loading"><ins></ins></div>' + |
|
619 |
'</div>' |
|
620 |
); |
|
621 |
}, |
|
622 |
|
|
623 |
/** |
|
624 |
* Sets an error for all view nodes tied to this view instance. |
|
625 |
* |
|
626 |
* @param {String} message The error message to set. |
|
627 |
* @param {String} dashicon A dashicon ID (optional). {@link https://developer.wordpress.org/resource/dashicons/} |
|
628 |
*/ |
|
629 |
setError: function( message, dashicon ) { |
|
630 |
this.setContent( |
|
631 |
'<div class="wpview-error">' + |
|
632 |
'<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' + |
|
633 |
'<p>' + message + '</p>' + |
|
634 |
'</div>' |
|
635 |
); |
|
636 |
}, |
|
637 |
|
|
638 |
/** |
|
639 |
* Tries to find a text match in a given string. |
|
640 |
* |
|
641 |
* @param {String} content The string to scan. |
|
642 |
* |
|
643 |
* @return {Object} |
|
644 |
*/ |
|
645 |
match: function( content ) { |
|
646 |
var match = wp.shortcode.next( this.type, content ); |
|
647 |
|
|
648 |
if ( match ) { |
|
649 |
return { |
|
650 |
index: match.index, |
|
651 |
content: match.content, |
|
652 |
options: { |
|
653 |
shortcode: match.shortcode |
|
654 |
} |
|
655 |
}; |
|
656 |
} |
|
657 |
}, |
|
658 |
|
|
659 |
/** |
|
660 |
* Update the text of a given view node. |
|
661 |
* |
|
662 |
* @param {String} text The new text. |
|
663 |
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. |
|
664 |
* @param {HTMLElement} node The view node to update. |
|
665 |
*/ |
|
666 |
update: function( text, editor, node ) { |
|
667 |
_.find( views, function( view, type ) { |
|
668 |
var match = view.prototype.match( text ); |
|
669 |
|
|
670 |
if ( match ) { |
|
671 |
$( node ).data( 'rendered', false ); |
|
672 |
editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) ); |
|
673 |
wp.mce.views.createInstance( type, text, match.options ).render(); |
|
674 |
editor.focus(); |
|
675 |
|
|
676 |
return true; |
|
677 |
} |
|
678 |
} ); |
|
679 |
}, |
|
680 |
|
|
681 |
/** |
|
682 |
* Remove a given view node from the DOM. |
|
683 |
* |
|
684 |
* @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. |
|
685 |
* @param {HTMLElement} node The view node to remove. |
|
686 |
*/ |
|
687 |
remove: function( editor, node ) { |
|
688 |
this.unbindNode.call( this, editor, node, $( node ).find( '.wpview-content' ).get( 0 ) ); |
|
689 |
$( node ).trigger( 'wp-mce-view-unbind' ); |
|
690 |
editor.dom.remove( node ); |
|
691 |
editor.focus(); |
|
692 |
} |
|
693 |
} ); |
|
694 |
} )( window, window.wp, window.jQuery ); |
|
695 |
|
|
696 |
/* |
|
697 |
* The WordPress core TinyMCE views. |
|
698 |
* Views for the gallery, audio, video, playlist and embed shortcodes, |
|
699 |
* and a view for embeddable URLs. |
|
700 |
*/ |
|
701 |
( function( window, views, $ ) { |
|
702 |
var postID = $( '#post_ID' ).val() || 0, |
|
703 |
media, gallery, av, embed; |
|
704 |
|
|
705 |
media = { |
|
706 |
state: [], |
|
707 |
|
|
708 |
edit: function( text, update ) { |
|
709 |
var media = wp.media[ this.type ], |
|
710 |
frame = media.edit( text ); |
|
711 |
|
|
712 |
this.pausePlayers && this.pausePlayers(); |
|
713 |
|
|
714 |
_.each( this.state, function( state ) { |
|
715 |
frame.state( state ).on( 'update', function( selection ) { |
|
716 |
update( media.shortcode( selection ).string() ); |
|
717 |
} ); |
|
718 |
} ); |
|
719 |
|
|
720 |
frame.on( 'close', function() { |
|
721 |
frame.detach(); |
|
722 |
} ); |
|
723 |
|
|
724 |
frame.open(); |
|
725 |
} |
|
726 |
}; |
|
727 |
|
|
728 |
gallery = _.extend( {}, media, { |
|
729 |
state: [ 'gallery-edit' ], |
|
730 |
template: wp.media.template( 'editor-gallery' ), |
|
731 |
|
|
732 |
initialize: function() { |
|
733 |
var attachments = wp.media.gallery.attachments( this.shortcode, postID ), |
|
734 |
attrs = this.shortcode.attrs.named, |
|
735 |
self = this; |
|
736 |
|
|
737 |
attachments.more() |
|
738 |
.done( function() { |
|
739 |
attachments = attachments.toJSON(); |
|
740 |
|
|
741 |
_.each( attachments, function( attachment ) { |
|
742 |
if ( attachment.sizes ) { |
|
743 |
if ( attrs.size && attachment.sizes[ attrs.size ] ) { |
|
744 |
attachment.thumbnail = attachment.sizes[ attrs.size ]; |
|
745 |
} else if ( attachment.sizes.thumbnail ) { |
|
746 |
attachment.thumbnail = attachment.sizes.thumbnail; |
|
747 |
} else if ( attachment.sizes.full ) { |
|
748 |
attachment.thumbnail = attachment.sizes.full; |
|
749 |
} |
|
750 |
} |
|
751 |
} ); |
|
752 |
|
|
753 |
self.render( self.template( { |
|
754 |
attachments: attachments, |
|
755 |
columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns |
|
756 |
} ) ); |
|
757 |
} ) |
|
758 |
.fail( function( jqXHR, textStatus ) { |
|
759 |
self.setError( textStatus ); |
|
760 |
} ); |
|
761 |
} |
|
762 |
} ); |
|
763 |
|
|
764 |
av = _.extend( {}, media, { |
|
765 |
action: 'parse-media-shortcode', |
|
766 |
|
|
767 |
initialize: function() { |
|
768 |
var self = this; |
|
769 |
|
|
770 |
if ( this.url ) { |
|
771 |
this.loader = false; |
|
772 |
this.shortcode = wp.media.embed.shortcode( { |
|
773 |
url: this.text |
|
774 |
} ); |
|
775 |
} |
|
776 |
|
|
777 |
wp.ajax.post( this.action, { |
|
778 |
post_ID: postID, |
|
779 |
type: this.shortcode.tag, |
|
780 |
shortcode: this.shortcode.string() |
|
781 |
} ) |
|
782 |
.done( function( response ) { |
|
783 |
self.render( response ); |
|
784 |
} ) |
|
785 |
.fail( function( response ) { |
|
786 |
if ( self.url ) { |
|
787 |
self.removeMarkers(); |
|
788 |
} else { |
|
789 |
self.setError( response.message || response.statusText, 'admin-media' ); |
|
790 |
} |
|
791 |
} ); |
|
792 |
|
|
793 |
this.getEditors( function( editor ) { |
|
794 |
editor.on( 'wpview-selected', function() { |
|
795 |
self.pausePlayers(); |
|
796 |
} ); |
|
797 |
} ); |
|
798 |
}, |
|
799 |
|
|
800 |
pausePlayers: function() { |
|
801 |
this.getNodes( function( editor, node, content ) { |
|
802 |
var win = $( 'iframe.wpview-sandbox', content ).get( 0 ); |
|
803 |
|
|
804 |
if ( win && ( win = win.contentWindow ) && win.mejs ) { |
|
805 |
_.each( win.mejs.players, function( player ) { |
|
806 |
try { |
|
807 |
player.pause(); |
|
808 |
} catch ( e ) {} |
|
809 |
} ); |
|
810 |
} |
|
811 |
} ); |
|
812 |
} |
|
813 |
} ); |
|
814 |
|
|
815 |
embed = _.extend( {}, av, { |
|
816 |
action: 'parse-embed', |
|
817 |
|
|
818 |
edit: function( text, update ) { |
|
819 |
var media = wp.media.embed, |
|
820 |
frame = media.edit( text, this.url ), |
|
821 |
self = this; |
|
822 |
|
|
823 |
this.pausePlayers(); |
|
824 |
|
|
825 |
frame.state( 'embed' ).props.on( 'change:url', function( model, url ) { |
|
826 |
if ( url && model.get( 'url' ) ) { |
|
827 |
frame.state( 'embed' ).metadata = model.toJSON(); |
|
828 |
} |
|
829 |
} ); |
|
830 |
|
|
831 |
frame.state( 'embed' ).on( 'select', function() { |
|
832 |
var data = frame.state( 'embed' ).metadata; |
|
833 |
|
|
834 |
if ( self.url ) { |
|
835 |
update( data.url ); |
|
836 |
} else { |
|
837 |
update( media.shortcode( data ).string() ); |
|
838 |
} |
|
839 |
} ); |
|
840 |
|
|
841 |
frame.on( 'close', function() { |
|
842 |
frame.detach(); |
|
843 |
} ); |
|
844 |
|
|
845 |
frame.open(); |
|
846 |
} |
|
847 |
} ); |
|
848 |
|
|
849 |
views.register( 'gallery', _.extend( {}, gallery ) ); |
|
850 |
|
|
851 |
views.register( 'audio', _.extend( {}, av, { |
|
852 |
state: [ 'audio-details' ] |
|
853 |
} ) ); |
|
854 |
|
|
855 |
views.register( 'video', _.extend( {}, av, { |
|
856 |
state: [ 'video-details' ] |
|
857 |
} ) ); |
|
858 |
|
|
859 |
views.register( 'playlist', _.extend( {}, av, { |
|
860 |
state: [ 'playlist-edit', 'video-playlist-edit' ] |
|
861 |
} ) ); |
|
862 |
|
|
863 |
views.register( 'embed', _.extend( {}, embed ) ); |
|
864 |
|
|
865 |
views.register( 'embedURL', _.extend( {}, embed, { |
|
866 |
match: function( content ) { |
|
867 |
var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi, |
|
868 |
match = re.exec( content ); |
|
869 |
|
|
870 |
if ( match ) { |
|
871 |
return { |
|
872 |
index: match.index + match[1].length, |
|
873 |
content: match[2], |
|
874 |
options: { |
|
875 |
url: true |
|
876 |
} |
|
877 |
}; |
|
878 |
} |
|
879 |
} |
|
880 |
} ) ); |
|
881 |
} )( window, window.wp.mce.views, window.jQuery ); |