]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/mce-view.js
WordPress 3.9.1
[autoinstalls/wordpress.git] / wp-includes / js / mce-view.js
1 /* global tinymce, MediaElementPlayer, WPPlaylistView */
2 /**
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.
6  */
7
8 // Ensure the global `wp` object exists.
9 window.wp = window.wp || {};
10
11 (function($){
12         var views = {},
13                 instances = {},
14                 media = wp.media,
15                 viewOptions = ['encodedText'];
16
17         // Create the `wp.mce` object if necessary.
18         wp.mce = wp.mce || {};
19
20         /**
21          * wp.mce.View
22          *
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.
25          */
26         wp.mce.View = function( options ) {
27                 options || (options = {});
28                 _.extend(this, _.pick(options, viewOptions));
29                 this.initialize.apply(this, arguments);
30         };
31
32         _.extend( wp.mce.View.prototype, {
33                 initialize: function() {},
34                 getHtml: function() {},
35                 render: function() {
36                         var html = this.getHtml();
37                         // Search all tinymce editor instances and update the placeholders
38                         _.each( tinymce.editors, function( editor ) {
39                                 var doc, self = this;
40                                 if ( editor.plugins.wpview ) {
41                                         doc = editor.getDoc();
42                                         $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).each(function (i, elem) {
43                                                 var node = $( 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 );
48                                         });
49                                 }
50                         }, this );
51                 },
52                 unbind: function() {}
53         } );
54
55         // take advantage of the Backbone extend method
56         wp.mce.View.extend = Backbone.View.extend;
57
58         /**
59          * wp.mce.views
60          *
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.
64          */
65         wp.mce.views = {
66
67                 /**
68                  * wp.mce.views.register( type, view )
69                  *
70                  * Registers a new TinyMCE view.
71                  *
72                  * @param type
73                  * @param constructor
74                  *
75                  */
76                 register: function( type, constructor ) {
77                         views[ type ] = constructor;
78                 },
79
80                 /**
81                  * wp.mce.views.get( id )
82                  *
83                  * Returns a TinyMCE view constructor.
84                  */
85                 get: function( type ) {
86                         return views[ type ];
87                 },
88
89                 /**
90                  * wp.mce.views.unregister( type )
91                  *
92                  * Unregisters a TinyMCE view.
93                  */
94                 unregister: function( type ) {
95                         delete views[ type ];
96                 },
97
98                 /**
99                  * wp.mce.views.unbind( editor )
100                  *
101                  * The editor DOM is being rebuilt, run cleanup.
102                  */
103                 unbind: function() {
104                         _.each( instances, function( instance ) {
105                                 instance.unbind();
106                         } );
107                 },
108
109                 /**
110                  * toViews( content )
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.
114                  *
115                  */
116                 toViews: function( content ) {
117                         var pieces = [ { content: content } ],
118                                 current;
119
120                         _.each( views, function( view, viewType ) {
121                                 current = pieces.slice();
122                                 pieces  = [];
123
124                                 _.each( current, function( piece ) {
125                                         var remaining = piece.content,
126                                                 result;
127
128                                         // Ignore processed pieces, but retain their location.
129                                         if ( piece.processed ) {
130                                                 pieces.push( piece );
131                                                 return;
132                                         }
133
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 ) });
140                                                 }
141
142                                                 // Add the processed piece for the match.
143                                                 pieces.push({
144                                                         content: wp.mce.views.toView( viewType, result.content, result.options ),
145                                                         processed: true
146                                                 });
147
148                                                 // Update the remaining content.
149                                                 remaining = remaining.slice( result.index + result.content.length );
150                                         }
151
152                                         // There are no additional matches. If any content remains,
153                                         // add it as an unprocessed piece.
154                                         if ( remaining ) {
155                                                 pieces.push({ content: remaining });
156                                         }
157                                 });
158                         });
159
160                         return _.pluck( pieces, 'content' ).join('');
161                 },
162
163                 /**
164                  * Create a placeholder for a particular view type
165                  *
166                  * @param viewType
167                  * @param text
168                  * @param options
169                  *
170                  */
171                 toView: function( viewType, text, options ) {
172                         var view = wp.mce.views.get( viewType ),
173                                 encodedText = window.encodeURIComponent( text ),
174                                 instance, viewOptions;
175
176
177                         if ( ! view ) {
178                                 return text;
179                         }
180
181                         if ( ! wp.mce.views.getInstance( encodedText ) ) {
182                                 viewOptions = options;
183                                 viewOptions.encodedText = encodedText;
184                                 instance = new view.View( viewOptions );
185                                 instances[ encodedText ] = instance;
186                         }
187
188                         return wp.html.string({
189                                 tag: 'div',
190
191                                 attrs: {
192                                         'class': 'wpview-wrap wpview-type-' + viewType,
193                                         'data-wpview-text': encodedText,
194                                         'data-wpview-type': viewType,
195                                         'contenteditable': 'false'
196                                 },
197
198                                 content: '\u00a0'
199                         });
200                 },
201
202                 /**
203                  * Refresh views after an update is made
204                  *
205                  * @param view {object} being refreshed
206                  * @param text {string} textual representation of the view
207                  */
208                 refreshView: function( view, text ) {
209                         var encodedText = window.encodeURIComponent( text ),
210                                 viewOptions,
211                                 result, instance;
212
213                         instance = wp.mce.views.getInstance( encodedText );
214
215                         if ( ! instance ) {
216                                 result = view.toView( text );
217                                 viewOptions = result.options;
218                                 viewOptions.encodedText = encodedText;
219                                 instance = new view.View( viewOptions );
220                                 instances[ encodedText ] = instance;
221                         }
222
223                         wp.mce.views.render();
224                 },
225
226                 getInstance: function( encodedText ) {
227                         return instances[ encodedText ];
228                 },
229
230                 /**
231                  * render( scope )
232                  *
233                  * Renders any view instances inside a DOM node `scope`.
234                  *
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 )`.
238                  */
239                 render: function() {
240                         _.each( instances, function( instance ) {
241                                 instance.render();
242                         } );
243                 },
244
245                 edit: function( node ) {
246                         var viewType = $( node ).data('wpview-type'),
247                                 view = wp.mce.views.get( viewType );
248
249                         if ( view ) {
250                                 view.edit( node );
251                         }
252                 }
253         };
254
255         wp.mce.gallery = {
256                 shortcode: 'gallery',
257                 toView:  function( content ) {
258                         var match = wp.shortcode.next( this.shortcode, content );
259
260                         if ( ! match ) {
261                                 return;
262                         }
263
264                         return {
265                                 index:   match.index,
266                                 content: match.content,
267                                 options: {
268                                         shortcode: match.shortcode
269                                 }
270                         };
271                 },
272                 View: wp.mce.View.extend({
273                         className: 'editor-gallery',
274                         template:  media.template('editor-gallery'),
275
276                         // The fallback post ID to use as a parent for galleries that don't
277                         // specify the `ids` or `include` parameters.
278                         //
279                         // Uses the hidden input on the edit posts page by default.
280                         postID: $('#post_ID').val(),
281
282                         initialize: function( options ) {
283                                 this.shortcode = options.shortcode;
284                                 this.fetch();
285                         },
286
287                         fetch: function() {
288                                 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
289                                 this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
290                         },
291
292                         getHtml: function() {
293                                 var attrs = this.shortcode.attrs.named,
294                                         attachments = false,
295                                         options;
296
297                                 // Don't render errors while still fetching attachments
298                                 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
299                                         return;
300                                 }
301
302                                 if ( this.attachments.length ) {
303                                         attachments = this.attachments.toJSON();
304
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;
311                                                         }
312                                                 }
313                                         } );
314                                 }
315
316                                 options = {
317                                         attachments: attachments,
318                                         columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3
319                                 };
320
321                                 return this.template( options );
322
323                         }
324                 }),
325
326                 edit: function( node ) {
327                         var gallery = wp.media.gallery,
328                                 self = this,
329                                 frame, data;
330
331                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
332                         frame = gallery.edit( data );
333
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 );
338                                 frame.detach();
339                         });
340                 }
341
342         };
343         wp.mce.views.register( 'gallery', wp.mce.gallery );
344
345         /**
346          * Tiny MCE Views for Audio / Video
347          *
348          */
349
350         /**
351          * These are base methods that are shared by each shortcode's MCE controller
352          *
353          * @mixin
354          */
355         wp.mce.media = {
356                 loaded: false,
357                 /**
358                  * @global wp.shortcode
359                  *
360                  * @param {string} content
361                  * @returns {Object}
362                  */
363                 toView:  function( content ) {
364                         var match = wp.shortcode.next( this.shortcode, content );
365
366                         if ( ! match ) {
367                                 return;
368                         }
369
370                         return {
371                                 index:   match.index,
372                                 content: match.content,
373                                 options: {
374                                         shortcode: match.shortcode
375                                 }
376                         };
377                 },
378
379                 /**
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
386                  *
387                  * @param {HTMLElement} node
388                  */
389                 edit: function( node ) {
390                         var media = wp.media[ this.shortcode ],
391                                 self = this,
392                                 frame, data, callback;
393
394                         wp.media.mixin.pauseAllPlayers();
395
396                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
397                         frame = media.edit( data );
398                         frame.on( 'close', function() {
399                                 frame.detach();
400                         } );
401
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 );
406                                 frame.detach();
407                         };
408                         if ( _.isArray( self.state ) ) {
409                                 _.each( self.state, function (state) {
410                                         frame.state( state ).on( 'update', callback );
411                                 } );
412                         } else {
413                                 frame.state( self.state ).on( 'update', callback );
414                         }
415                         frame.open();
416                 }
417         };
418
419         /**
420          * Base View class for audio and video shortcodes
421          *
422          * @constructor
423          * @augments wp.mce.View
424          * @mixes wp.media.mixin
425          */
426         wp.mce.media.View = wp.mce.View.extend({
427                 initialize: function( options ) {
428                         this.players = [];
429                         this.shortcode = options.shortcode;
430                         _.bindAll( this, 'setPlayer' );
431                         $(this).on( 'ready', this.setPlayer );
432                 },
433
434                 /**
435                  * Creates the player instance for the current node
436                  *
437                  * @global MediaElementPlayer
438                  * @global _wpmejsSettings
439                  *
440                  * @param {Event} e
441                  * @param {HTMLElement} node
442                  */
443                 setPlayer: function(e, node) {
444                         // if the ready event fires on an empty node
445                         if ( ! node ) {
446                                 return;
447                         }
448
449                         var self = this,
450                                 media,
451                                 firefox = this.ua.is( 'ff' ),
452                                 className = '.wp-' +  this.shortcode.tag + '-shortcode';
453
454                         media = $( node ).find( className );
455
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 );
460                                 }
461                                 media.replaceWith( '<p>' + media.find( 'source' ).eq(0).prop( 'src' ) + '</p>' );
462                                 return;
463                         } else {
464                                 media.closest( '.wpview-wrap' ).removeClass( 'wont-play' );
465                                 if ( firefox ) {
466                                         media.prop( 'preload', 'metadata' );
467                                 } else {
468                                         media.prop( 'preload', 'none' );
469                                 }
470                         }
471
472                         media = wp.media.view.MediaDetails.prepareSrc( media.get(0) );
473
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 );
478                 },
479
480                 /**
481                  * Pass data to the View's Underscore template and return the compiled output
482                  *
483                  * @returns {string}
484                  */
485                 getHtml: function() {
486                         var attrs = this.shortcode.attrs.named;
487                         attrs.content = this.shortcode.content;
488
489                         return this.template({ model: _.defaults(
490                                 attrs,
491                                 wp.media[ this.shortcode.tag ].defaults )
492                         });
493                 },
494
495                 unbind: function() {
496                         this.unsetPlayers();
497                 }
498         });
499         _.extend( wp.mce.media.View.prototype, wp.media.mixin );
500
501         /**
502          * TinyMCE handler for the video shortcode
503          *
504          * @mixes wp.mce.media
505          */
506         wp.mce.video = _.extend( {}, wp.mce.media, {
507                 shortcode: 'video',
508                 state: 'video-details',
509                 View: wp.mce.media.View.extend({
510                         className: 'editor-video',
511                         template:  media.template('editor-video')
512                 })
513         } );
514         wp.mce.views.register( 'video', wp.mce.video );
515
516         /**
517          * TinyMCE handler for the audio shortcode
518          *
519          * @mixes wp.mce.media
520          */
521         wp.mce.audio = _.extend( {}, wp.mce.media, {
522                 shortcode: 'audio',
523                 state: 'audio-details',
524                 View: wp.mce.media.View.extend({
525                         className: 'editor-audio',
526                         template:  media.template('editor-audio')
527                 })
528         } );
529         wp.mce.views.register( 'audio', wp.mce.audio );
530
531         /**
532          * Base View class for playlist shortcodes
533          *
534          * @constructor
535          * @augments wp.mce.View
536          * @mixes wp.media.mixin
537          */
538         wp.mce.media.PlaylistView = wp.mce.View.extend({
539                 className: 'editor-playlist',
540                 template:  media.template('editor-playlist'),
541
542                 initialize: function( options ) {
543                         this.players = [];
544                         this.data = {};
545                         this.attachments = [];
546                         this.shortcode = options.shortcode;
547                         this.fetch();
548                 },
549
550                 /**
551                  * Asynchronously fetch the shortcode's attachments
552                  */
553                 fetch: function() {
554                         this.attachments = wp.media.playlist.attachments( this.shortcode );
555                         this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
556                 },
557
558                 /**
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
562                  *
563                  * @global WPPlaylistView
564                  * @global tinymce.editors
565                  */
566                 render: function() {
567                         var html = this.getHtml(), self = this;
568
569                         _.each( tinymce.editors, function( editor ) {
570                                 var doc;
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 );
575
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>' );
579
580                                                 if ( ! self.data.tracks ) {
581                                                         return;
582                                                 }
583
584                                                 self.players.push( new WPPlaylistView({
585                                                         el: $( elem ).find( '.wp-playlist' ).get(0),
586                                                         metadata: self.data
587                                                 }).player );
588                                         });
589                                 }
590                         }, this );
591                 },
592
593                 /**
594                  * Set the data that will be used to compile the Underscore template,
595                  *  compile the template, and then return it.
596                  *
597                  * @returns {string}
598                  */
599                 getHtml: function() {
600                         var data = this.shortcode.attrs.named,
601                                 model = wp.media.playlist,
602                                 options,
603                                 attachments,
604                                 tracks = [];
605
606                         // Don't render errors while still fetching attachments
607                         if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
608                                 return;
609                         }
610
611                         _.each( model.defaults, function( value, key ) {
612                                 data[ key ] = model.coerce( data, key );
613                         });
614
615                         options = {
616                                 type: data.type,
617                                 style: data.style,
618                                 tracklist: data.tracklist,
619                                 tracknumbers: data.tracknumbers,
620                                 images: data.images,
621                                 artists: data.artists
622                         };
623
624                         if ( ! this.attachments.length ) {
625                                 return this.template( options );
626                         }
627
628                         attachments = this.attachments.toJSON();
629
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
638                                 };
639
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;
649                                                 }
650                                         } else {
651                                                 if ( ! options.width ) {
652                                                         options.width = attachment.width;
653                                                         options.height = attachment.height;
654                                                 }
655                                         }
656                                         track.dimensions = {
657                                                 original : size,
658                                                 resized : _.isEmpty( resize ) ? size : resize
659                                         };
660                                 } else {
661                                         options.width = 400;
662                                 }
663
664                                 track.image = attachment.image;
665                                 track.thumb = attachment.thumb;
666
667                                 tracks.push( track );
668                         } );
669
670                         options.tracks = tracks;
671                         this.data = options;
672
673                         return this.template( options );
674                 },
675
676                 unbind: function() {
677                         this.unsetPlayers();
678                 }
679         });
680         _.extend( wp.mce.media.PlaylistView.prototype, wp.media.mixin );
681
682         /**
683          * TinyMCE handler for the playlist shortcode
684          *
685          * @mixes wp.mce.media
686          */
687         wp.mce.playlist = _.extend( {}, wp.mce.media, {
688                 shortcode: 'playlist',
689                 state: ['playlist-edit', 'video-playlist-edit'],
690                 View: wp.mce.media.PlaylistView
691         } );
692         wp.mce.views.register( 'playlist', wp.mce.playlist );
693 }(jQuery));