1 /* global tinymce, MediaElementPlayer, WPPlaylistView */
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 || {};
15 viewOptions = ['encodedText'];
17 // Create the `wp.mce` object if necessary.
18 wp.mce = wp.mce || {};
23 * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
24 * that the TinyMCE View is not tied to a particular DOM node.
26 wp.mce.View = function( options ) {
27 options || (options = {});
28 _.extend(this, _.pick(options, viewOptions));
29 this.initialize.apply(this, arguments);
32 _.extend( wp.mce.View.prototype, {
33 initialize: function() {},
34 getHtml: function() {},
36 var html = this.getHtml();
37 // Search all tinymce editor instances and update the placeholders
38 _.each( tinymce.editors, function( editor ) {
40 if ( editor.plugins.wpview ) {
41 doc = editor.getDoc();
42 $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).each(function (i, elem) {
44 // The <ins> is used to mark the end of the wrapper div. Needed when comparing
45 // the content as string for preventing extra undo levels.
46 node.html( html ).append( '<ins data-wpview-end="1"></ins>' );
47 $( self ).trigger( 'ready', elem );
55 // take advantage of the Backbone extend method
56 wp.mce.View.extend = Backbone.View.extend;
61 * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
62 * At its core, it serves as a series of converters, transforming text to a
63 * custom UI, and back again.
68 * wp.mce.views.register( type, view )
70 * Registers a new TinyMCE view.
76 register: function( type, constructor ) {
77 views[ type ] = constructor;
81 * wp.mce.views.get( id )
83 * Returns a TinyMCE view constructor.
85 get: function( type ) {
90 * wp.mce.views.unregister( type )
92 * Unregisters a TinyMCE view.
94 unregister: function( type ) {
99 * wp.mce.views.unbind( editor )
101 * The editor DOM is being rebuilt, run cleanup.
104 _.each( instances, function( instance ) {
111 * Scans a `content` string for each view's pattern, replacing any
112 * matches with wrapper elements, and creates a new instance for
113 * every match, which triggers the related data to be fetched.
116 toViews: function( content ) {
117 var pieces = [ { content: content } ],
120 _.each( views, function( view, viewType ) {
121 current = pieces.slice();
124 _.each( current, function( piece ) {
125 var remaining = piece.content,
128 // Ignore processed pieces, but retain their location.
129 if ( piece.processed ) {
130 pieces.push( piece );
134 // Iterate through the string progressively matching views
135 // and slicing the string as we go.
136 while ( remaining && (result = view.toView( remaining )) ) {
137 // Any text before the match becomes an unprocessed piece.
138 if ( result.index ) {
139 pieces.push({ content: remaining.substring( 0, result.index ) });
142 // Add the processed piece for the match.
144 content: wp.mce.views.toView( viewType, result.content, result.options ),
148 // Update the remaining content.
149 remaining = remaining.slice( result.index + result.content.length );
152 // There are no additional matches. If any content remains,
153 // add it as an unprocessed piece.
155 pieces.push({ content: remaining });
160 return _.pluck( pieces, 'content' ).join('');
164 * Create a placeholder for a particular view type
171 toView: function( viewType, text, options ) {
172 var view = wp.mce.views.get( viewType ),
173 encodedText = window.encodeURIComponent( text ),
174 instance, viewOptions;
181 if ( ! wp.mce.views.getInstance( encodedText ) ) {
182 viewOptions = options;
183 viewOptions.encodedText = encodedText;
184 instance = new view.View( viewOptions );
185 instances[ encodedText ] = instance;
188 return wp.html.string({
192 'class': 'wpview-wrap wpview-type-' + viewType,
193 'data-wpview-text': encodedText,
194 'data-wpview-type': viewType,
195 'contenteditable': 'false'
203 * Refresh views after an update is made
205 * @param view {object} being refreshed
206 * @param text {string} textual representation of the view
208 refreshView: function( view, text ) {
209 var encodedText = window.encodeURIComponent( text ),
213 instance = wp.mce.views.getInstance( encodedText );
216 result = view.toView( text );
217 viewOptions = result.options;
218 viewOptions.encodedText = encodedText;
219 instance = new view.View( viewOptions );
220 instances[ encodedText ] = instance;
223 wp.mce.views.render();
226 getInstance: function( encodedText ) {
227 return instances[ encodedText ];
233 * Renders any view instances inside a DOM node `scope`.
235 * View instances are detected by the presence of wrapper elements.
236 * To generate wrapper elements, pass your content through
237 * `wp.mce.view.toViews( content )`.
240 _.each( instances, function( instance ) {
245 edit: function( node ) {
246 var viewType = $( node ).data('wpview-type'),
247 view = wp.mce.views.get( viewType );
256 shortcode: 'gallery',
257 toView: function( content ) {
258 var match = wp.shortcode.next( this.shortcode, content );
266 content: match.content,
268 shortcode: match.shortcode
272 View: wp.mce.View.extend({
273 className: 'editor-gallery',
274 template: media.template('editor-gallery'),
276 // The fallback post ID to use as a parent for galleries that don't
277 // specify the `ids` or `include` parameters.
279 // Uses the hidden input on the edit posts page by default.
280 postID: $('#post_ID').val(),
282 initialize: function( options ) {
283 this.shortcode = options.shortcode;
288 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
289 this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
292 getHtml: function() {
293 var attrs = this.shortcode.attrs.named,
297 // Don't render errors while still fetching attachments
298 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
302 if ( this.attachments.length ) {
303 attachments = this.attachments.toJSON();
305 _.each( attachments, function( attachment ) {
306 if ( attachment.sizes ) {
307 if ( attachment.sizes.thumbnail ) {
308 attachment.thumbnail = attachment.sizes.thumbnail;
309 } else if ( attachment.sizes.full ) {
310 attachment.thumbnail = attachment.sizes.full;
317 attachments: attachments,
318 columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3
321 return this.template( options );
326 edit: function( node ) {
327 var gallery = wp.media.gallery,
331 data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
332 frame = gallery.edit( data );
334 frame.state('gallery-edit').on( 'update', function( selection ) {
335 var shortcode = gallery.shortcode( selection ).string();
336 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
337 wp.mce.views.refreshView( self, shortcode );
343 wp.mce.views.register( 'gallery', wp.mce.gallery );
346 * Tiny MCE Views for Audio / Video
351 * These are base methods that are shared by each shortcode's MCE controller
358 * @global wp.shortcode
360 * @param {string} content
363 toView: function( content ) {
364 var match = wp.shortcode.next( this.shortcode, content );
372 content: match.content,
374 shortcode: match.shortcode
380 * Called when a TinyMCE view is clicked for editing.
381 * - Parses the shortcode out of the element's data attribute
382 * - Calls the `edit` method on the shortcode model
383 * - Launches the model window
384 * - Bind's an `update` callback which updates the element's data attribute
385 * re-renders the view
387 * @param {HTMLElement} node
389 edit: function( node ) {
390 var media = wp.media[ this.shortcode ],
392 frame, data, callback;
394 wp.media.mixin.pauseAllPlayers();
396 data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
397 frame = media.edit( data );
398 frame.on( 'close', function() {
402 callback = function( selection ) {
403 var shortcode = wp.media[ self.shortcode ].shortcode( selection ).string();
404 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
405 wp.mce.views.refreshView( self, shortcode );
408 if ( _.isArray( self.state ) ) {
409 _.each( self.state, function (state) {
410 frame.state( state ).on( 'update', callback );
413 frame.state( self.state ).on( 'update', callback );
420 * Base View class for audio and video shortcodes
423 * @augments wp.mce.View
424 * @mixes wp.media.mixin
426 wp.mce.media.View = wp.mce.View.extend({
427 initialize: function( options ) {
429 this.shortcode = options.shortcode;
430 _.bindAll( this, 'setPlayer' );
431 $(this).on( 'ready', this.setPlayer );
435 * Creates the player instance for the current node
437 * @global MediaElementPlayer
438 * @global _wpmejsSettings
441 * @param {HTMLElement} node
443 setPlayer: function(e, node) {
444 // if the ready event fires on an empty node
451 firefox = this.ua.is( 'ff' ),
452 className = '.wp-' + this.shortcode.tag + '-shortcode';
454 media = $( node ).find( className );
456 if ( ! this.isCompatible( media ) ) {
457 media.closest( '.wpview-wrap' ).addClass( 'wont-play' );
458 if ( ! media.parent().hasClass( 'wpview-wrap' ) ) {
459 media.parent().replaceWith( media );
461 media.replaceWith( '<p>' + media.find( 'source' ).eq(0).prop( 'src' ) + '</p>' );
464 media.closest( '.wpview-wrap' ).removeClass( 'wont-play' );
466 media.prop( 'preload', 'metadata' );
468 media.prop( 'preload', 'none' );
472 media = wp.media.view.MediaDetails.prepareSrc( media.get(0) );
474 setTimeout( function() {
475 wp.mce.media.loaded = true;
476 self.players.push( new MediaElementPlayer( media, self.mejsSettings ) );
477 }, wp.mce.media.loaded ? 10 : 500 );
481 * Pass data to the View's Underscore template and return the compiled output
485 getHtml: function() {
486 var attrs = this.shortcode.attrs.named;
487 attrs.content = this.shortcode.content;
489 return this.template({ model: _.defaults(
491 wp.media[ this.shortcode.tag ].defaults )
499 _.extend( wp.mce.media.View.prototype, wp.media.mixin );
502 * TinyMCE handler for the video shortcode
504 * @mixes wp.mce.media
506 wp.mce.video = _.extend( {}, wp.mce.media, {
508 state: 'video-details',
509 View: wp.mce.media.View.extend({
510 className: 'editor-video',
511 template: media.template('editor-video')
514 wp.mce.views.register( 'video', wp.mce.video );
517 * TinyMCE handler for the audio shortcode
519 * @mixes wp.mce.media
521 wp.mce.audio = _.extend( {}, wp.mce.media, {
523 state: 'audio-details',
524 View: wp.mce.media.View.extend({
525 className: 'editor-audio',
526 template: media.template('editor-audio')
529 wp.mce.views.register( 'audio', wp.mce.audio );
532 * Base View class for playlist shortcodes
535 * @augments wp.mce.View
536 * @mixes wp.media.mixin
538 wp.mce.media.PlaylistView = wp.mce.View.extend({
539 className: 'editor-playlist',
540 template: media.template('editor-playlist'),
542 initialize: function( options ) {
545 this.attachments = [];
546 this.shortcode = options.shortcode;
551 * Asynchronously fetch the shortcode's attachments
554 this.attachments = wp.media.playlist.attachments( this.shortcode );
555 this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
559 * Get the HTML for the view (which also set's the data), replace the
560 * current HTML, and then invoke the WPPlaylistView instance to render
561 * the playlist in the editor
563 * @global WPPlaylistView
564 * @global tinymce.editors
567 var html = this.getHtml(), self = this;
569 _.each( tinymce.editors, function( editor ) {
571 if ( editor.plugins.wpview ) {
572 doc = editor.getDoc();
573 $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).each(function (i, elem) {
574 var node = $( elem );
576 // The <ins> is used to mark the end of the wrapper div. Needed when comparing
577 // the content as string for preventing extra undo levels.
578 node.html( html ).append( '<ins data-wpview-end="1"></ins>' );
580 if ( ! self.data.tracks ) {
584 self.players.push( new WPPlaylistView({
585 el: $( elem ).find( '.wp-playlist' ).get(0),
594 * Set the data that will be used to compile the Underscore template,
595 * compile the template, and then return it.
599 getHtml: function() {
600 var data = this.shortcode.attrs.named,
601 model = wp.media.playlist,
606 // Don't render errors while still fetching attachments
607 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
611 _.each( model.defaults, function( value, key ) {
612 data[ key ] = model.coerce( data, key );
618 tracklist: data.tracklist,
619 tracknumbers: data.tracknumbers,
621 artists: data.artists
624 if ( ! this.attachments.length ) {
625 return this.template( options );
628 attachments = this.attachments.toJSON();
630 _.each( attachments, function( attachment ) {
631 var size = {}, resize = {}, track = {
632 src : attachment.url,
633 type : attachment.mime,
634 title : attachment.title,
635 caption : attachment.caption,
636 description : attachment.description,
637 meta : attachment.meta
640 if ( 'video' === data.type ) {
641 size.width = attachment.width;
642 size.height = attachment.height;
643 if ( media.view.settings.contentWidth ) {
644 resize.width = media.view.settings.contentWidth - 22;
645 resize.height = Math.ceil( ( size.height * resize.width ) / size.width );
646 if ( ! options.width ) {
647 options.width = resize.width;
648 options.height = resize.height;
651 if ( ! options.width ) {
652 options.width = attachment.width;
653 options.height = attachment.height;
658 resized : _.isEmpty( resize ) ? size : resize
664 track.image = attachment.image;
665 track.thumb = attachment.thumb;
667 tracks.push( track );
670 options.tracks = tracks;
673 return this.template( options );
680 _.extend( wp.mce.media.PlaylistView.prototype, wp.media.mixin );
683 * TinyMCE handler for the playlist shortcode
685 * @mixes wp.mce.media
687 wp.mce.playlist = _.extend( {}, wp.mce.media, {
688 shortcode: 'playlist',
689 state: ['playlist-edit', 'video-playlist-edit'],
690 View: wp.mce.media.PlaylistView
692 wp.mce.views.register( 'playlist', wp.mce.playlist );