3
* Note: this API is "experimental" meaning that it will probably change
4
* in the next few releases based on feedback from 3.9.0.
5
* If you decide to use it, please follow the development closely.
8
// Ensure the global `wp` object exists.
9
window.wp = window.wp || {};
17
viewOptions = ['encodedText'];
19
// Create the `wp.mce` object if necessary.
20
wp.mce = wp.mce || {};
25
* A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
26
* that the TinyMCE View is not tied to a particular DOM node.
28
* @param {Object} [options={}]
30
wp.mce.View = function( options ) {
31
options = options || {};
32
this.type = options.type;
33
_.extend( this, _.pick( options, viewOptions ) );
34
this.initialize.apply( this, arguments );
37
_.extend( wp.mce.View.prototype, {
38
initialize: function() {},
42
loadingPlaceholder: function() {
44
'<div class="loading-placeholder">' +
45
'<div class="dashicons dashicons-admin-media"></div>' +
46
'<div class="wpview-loading"><ins></ins></div>' +
49
render: function( force ) {
50
if ( force || ! this.rendered() ) {
54
'<p class="wpview-selection-before">\u00a0</p>' +
55
'<div class="wpview-body" contenteditable="false">' +
56
'<div class="toolbar">' +
57
( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +
58
'<div class="dashicons dashicons-no-alt remove"></div>' +
60
'<div class="wpview-content wpview-type-' + this.type + '">' +
61
( this.getHtml() || this.loadingPlaceholder() ) +
63
( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) +
65
'<p class="wpview-selection-after">\u00a0</p>',
69
$( this ).trigger( 'ready' );
71
this.rendered( true );
74
unbind: function() {},
75
getEditors: function( callback ) {
78
_.each( tinymce.editors, function( editor ) {
79
if ( editor.plugins.wpview ) {
84
editors.push( editor );
90
getNodes: function( callback ) {
94
this.getEditors( function( editor ) {
96
.find( '[data-wpview-text="' + self.encodedText + '"]' )
97
.each( function ( i, node ) {
99
callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
108
setContent: function( html, option ) {
109
this.getNodes( function ( editor, node, content ) {
110
var el = ( option === 'wrap' || option === 'replace' ) ? node : content,
113
if ( _.isString( insert ) ) {
114
insert = editor.dom.createFragment( insert );
117
if ( option === 'replace' ) {
118
editor.dom.replace( insert, el );
121
el.appendChild( insert );
125
/* jshint scripturl: true */
126
setIframes: function ( head, body ) {
127
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
128
importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';
130
if ( head || body.indexOf( '<script' ) !== -1 ) {
131
this.getNodes( function ( editor, node, content ) {
132
var dom = editor.dom,
134
bodyClasses = editor.getBody().className || '',
135
iframe, iframeDoc, i, resize;
137
content.innerHTML = '';
140
if ( importStyles ) {
141
if ( ! wp.mce.views.sandboxStyles ) {
142
tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {
143
if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
144
link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
146
styles += dom.getOuterHTML( link ) + '\n';
150
wp.mce.views.sandboxStyles = styles;
152
styles = wp.mce.views.sandboxStyles;
156
// Seems Firefox needs a bit of time to insert/set the view nodes, or the iframe will fail
157
// especially when switching Text => Visual.
158
setTimeout( function() {
159
iframe = dom.add( content, 'iframe', {
160
src: tinymce.Env.ie ? 'javascript:""' : '',
162
allowTransparency: 'true',
164
'class': 'wpview-sandbox',
171
iframeDoc = iframe.contentWindow.document;
178
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
183
'background: transparent;' +
187
'body#wpview-iframe-sandbox {' +
188
'background: transparent;' +
189
'padding: 1px 0 !important;' +
190
'margin: -1px 0 0 !important;' +
192
'body#wpview-iframe-sandbox:before,' +
193
'body#wpview-iframe-sandbox:after {' +
199
'<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
206
resize = function() {
207
// Make sure the iframe still exists.
208
iframe.contentWindow && $( iframe ).height( $( iframeDoc.body ).height() );
211
if ( MutationObserver ) {
212
new MutationObserver( _.debounce( function() {
215
.observe( iframeDoc.body, {
221
for ( i = 1; i < 6; i++ ) {
222
setTimeout( resize, i * 700 );
226
if ( importStyles ) {
227
editor.on( 'wp-body-class-change', function() {
228
iframeDoc.body.className = editor.getBody().className;
234
this.setContent( body );
237
setError: function( message, dashicon ) {
239
'<div class="wpview-error">' +
240
'<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +
241
'<p>' + message + '</p>' +
245
rendered: function( value ) {
248
this.getNodes( function( editor, node ) {
249
if ( value != null ) {
250
$( node ).data( 'rendered', value === true );
252
notRendered = notRendered || ! $( node ).data( 'rendered' );
256
return ! notRendered;
260
// take advantage of the Backbone extend method
261
wp.mce.View.extend = Backbone.View.extend;
266
* A set of utilities that simplifies adding custom UI within a TinyMCE editor.
267
* At its core, it serves as a series of converters, transforming text to a
268
* custom UI, and back again.
273
* wp.mce.views.register( type, view )
275
* Registers a new TinyMCE view.
281
register: function( type, constructor ) {
282
var defaultConstructor = {
285
toView: function( content ) {
286
var match = wp.shortcode.next( this.type, content );
294
content: match.content,
296
shortcode: match.shortcode
302
constructor = _.defaults( constructor, defaultConstructor );
303
constructor.View = wp.mce.View.extend( constructor.View );
305
views[ type ] = constructor;
309
* wp.mce.views.get( id )
311
* Returns a TinyMCE view constructor.
315
get: function( type ) {
316
return views[ type ];
320
* wp.mce.views.unregister( type )
322
* Unregisters a TinyMCE view.
326
unregister: function( type ) {
327
delete views[ type ];
331
* wp.mce.views.unbind( editor )
333
* The editor DOM is being rebuilt, run cleanup.
336
_.each( instances, function( instance ) {
343
* Scans a `content` string for each view's pattern, replacing any
344
* matches with wrapper elements, and creates a new instance for
345
* every match, which triggers the related data to be fetched.
349
toViews: function( content ) {
350
var pieces = [ { content: content } ],
353
_.each( views, function( view, viewType ) {
354
current = pieces.slice();
357
_.each( current, function( piece ) {
358
var remaining = piece.content,
361
// Ignore processed pieces, but retain their location.
362
if ( piece.processed ) {
363
pieces.push( piece );
367
// Iterate through the string progressively matching views
368
// and slicing the string as we go.
369
while ( remaining && (result = view.toView( remaining )) ) {
370
// Any text before the match becomes an unprocessed piece.
371
if ( result.index ) {
372
pieces.push({ content: remaining.substring( 0, result.index ) });
375
// Add the processed piece for the match.
377
content: wp.mce.views.toView( viewType, result.content, result.options ),
381
// Update the remaining content.
382
remaining = remaining.slice( result.index + result.content.length );
385
// There are no additional matches. If any content remains,
386
// add it as an unprocessed piece.
388
pieces.push({ content: remaining });
393
return _.pluck( pieces, 'content' ).join('');
397
* Create a placeholder for a particular view type
404
toView: function( viewType, text, options ) {
405
var view = wp.mce.views.get( viewType ),
406
encodedText = window.encodeURIComponent( text ),
407
instance, viewOptions;
414
if ( ! wp.mce.views.getInstance( encodedText ) ) {
415
viewOptions = options;
416
viewOptions.type = viewType;
417
viewOptions.encodedText = encodedText;
418
instance = new view.View( viewOptions );
419
instances[ encodedText ] = instance;
422
return wp.html.string({
426
'class': 'wpview-wrap',
427
'data-wpview-text': encodedText,
428
'data-wpview-type': viewType
436
* Refresh views after an update is made
438
* @param view {object} being refreshed
439
* @param text {string} textual representation of the view
441
refreshView: function( view, text ) {
442
var encodedText = window.encodeURIComponent( text ),
446
instance = wp.mce.views.getInstance( encodedText );
449
result = view.toView( text );
450
viewOptions = result.options;
451
viewOptions.type = view.type;
452
viewOptions.encodedText = encodedText;
453
instance = new view.View( viewOptions );
454
instances[ encodedText ] = instance;
460
getInstance: function( encodedText ) {
461
return instances[ encodedText ];
467
* Renders any view instances inside a DOM node `scope`.
469
* View instances are detected by the presence of wrapper elements.
470
* To generate wrapper elements, pass your content through
471
* `wp.mce.view.toViews( content )`.
473
render: function( force ) {
474
_.each( instances, function( instance ) {
475
instance.render( force );
479
edit: function( node ) {
480
var viewType = $( node ).data('wpview-type'),
481
view = wp.mce.views.get( viewType );
489
wp.mce.views.register( 'gallery', {
491
template: media.template( 'editor-gallery' ),
493
// The fallback post ID to use as a parent for galleries that don't
494
// specify the `ids` or `include` parameters.
496
// Uses the hidden input on the edit posts page by default.
497
postID: $('#post_ID').val(),
499
initialize: function( options ) {
500
this.shortcode = options.shortcode;
507
this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
508
this.dfd = this.attachments.more().done( function() {
513
getHtml: function() {
514
var attrs = this.shortcode.attrs.named,
518
// Don't render errors while still fetching attachments
519
if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
523
if ( this.attachments.length ) {
524
attachments = this.attachments.toJSON();
526
_.each( attachments, function( attachment ) {
527
if ( attachment.sizes ) {
528
if ( attachment.sizes.thumbnail ) {
529
attachment.thumbnail = attachment.sizes.thumbnail;
530
} else if ( attachment.sizes.full ) {
531
attachment.thumbnail = attachment.sizes.full;
538
attachments: attachments,
539
columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
542
return this.template( options );
546
edit: function( node ) {
547
var gallery = wp.media.gallery,
551
data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
552
frame = gallery.edit( data );
554
frame.state('gallery-edit').on( 'update', function( selection ) {
555
var shortcode = gallery.shortcode( selection ).string();
556
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
557
wp.mce.views.refreshView( self, shortcode );
560
frame.on( 'close', function() {
567
* These are base methods that are shared by the audio and video shortcode's MCE controller.
575
action: 'parse-media-shortcode',
577
initialize: function( options ) {
580
this.shortcode = options.shortcode;
582
_.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
583
$( this ).on( 'ready', this.setNodes );
585
$( document ).on( 'media:edit', this.stopPlayers );
589
this.getEditors( function( editor ) {
590
editor.on( 'hide', self.stopPlayers );
594
setNodes: function () {
596
this.setIframes( this.parsed.head, this.parsed.body );
605
wp.ajax.send( this.action, {
607
post_ID: $( '#post_ID' ).val() || 0,
608
type: this.shortcode.tag,
609
shortcode: this.shortcode.string()
612
.done( function( response ) {
614
self.parsed = response;
615
self.setIframes( response.head, response.body );
620
.fail( function( response ) {
621
self.fail( response || true );
625
fail: function( error ) {
626
if ( ! this.error ) {
634
if ( this.error.message ) {
635
if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' ||
636
this.error.type === 'no-items' ) {
638
this.setError( this.error.message, 'admin-media' );
640
this.setContent( '<p>' + this.original + '</p>', 'replace' );
642
} else if ( this.error.statusText ) {
643
this.setError( this.error.statusText, 'admin-media' );
644
} else if ( this.original ) {
645
this.setContent( '<p>' + this.original + '</p>', 'replace' );
649
stopPlayers: function( remove ) {
650
var rem = remove === 'remove';
652
this.getNodes( function( editor, node, content ) {
654
iframe = $( 'iframe.wpview-sandbox', content ).get(0);
656
if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) {
657
// Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there.
659
for ( p in win.mejs.players ) {
660
win.mejs.players[p].pause();
663
win.mejs.players[p].remove();
672
this.stopPlayers( 'remove' );
677
* Called when a TinyMCE view is clicked for editing.
678
* - Parses the shortcode out of the element's data attribute
679
* - Calls the `edit` method on the shortcode model
680
* - Launches the model window
681
* - Bind's an `update` callback which updates the element's data attribute
682
* re-renders the view
684
* @param {HTMLElement} node
686
edit: function( node ) {
687
var media = wp.media[ this.type ],
689
frame, data, callback;
691
$( document ).trigger( 'media:edit' );
693
data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
694
frame = media.edit( data );
695
frame.on( 'close', function() {
699
callback = function( selection ) {
700
var shortcode = wp.media[ self.type ].shortcode( selection ).string();
701
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
702
wp.mce.views.refreshView( self, shortcode );
705
if ( _.isArray( self.state ) ) {
706
_.each( self.state, function (state) {
707
frame.state( state ).on( 'update', callback );
710
frame.state( self.state ).on( 'update', callback );
717
* TinyMCE handler for the video shortcode
721
wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
722
state: 'video-details'
726
* TinyMCE handler for the audio shortcode
730
wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
731
state: 'audio-details'
735
* TinyMCE handler for the playlist shortcode
739
wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
740
state: [ 'playlist-edit', 'video-playlist-edit' ]
744
* TinyMCE handler for the embed shortcode
746
wp.mce.embedMixin = {
747
View: _.extend( {}, wp.mce.av.View, {
749
action: 'parse-embed',
750
initialize: function( options ) {
751
this.content = options.content;
752
this.original = options.url || options.shortcode.string();
755
this.shortcode = media.embed.shortcode( {
759
this.shortcode = options.shortcode;
762
_.bindAll( this, 'setIframes', 'setNodes', 'fetch' );
763
$( this ).on( 'ready', this.setNodes );
768
edit: function( node ) {
769
var embed = media.embed,
773
isURL = 'embedURL' === this.type;
775
$( document ).trigger( 'media:edit' );
777
data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
778
frame = embed.edit( data, isURL );
779
frame.on( 'close', function() {
782
frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
786
frame.state( 'embed' ).metadata = model.toJSON();
788
frame.state( 'embed' ).on( 'select', function() {
792
shortcode = frame.state( 'embed' ).metadata.url;
794
shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
796
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
797
wp.mce.views.refreshView( self, shortcode );
804
wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
806
wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
807
toView: function( content ) {
808
var re = /(?:^|<p>)(https?:\/\/[^\s"]+?)(?:<\/p>\s*|$)/gi,
809
match = re.exec( tinymce.trim( content ) );