1 /* global _wpMediaViewsL10n, _wpmejsSettings, MediaElementPlayer */
3 (function($, _, Backbone) {
6 l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
8 if ( ! _.isUndefined( window._wpmejsSettings ) ) {
9 baseSettings.pluginPath = _wpmejsSettings.pluginPath;
16 mejsSettings: baseSettings,
18 * Pauses every instance of MediaElementPlayer
20 pauseAllPlayers: function() {
22 if ( window.mejs && window.mejs.players ) {
23 for ( p in window.mejs.players ) {
24 window.mejs.players[p].pause();
30 * Utility to identify the user's browser
33 is : function( browser ) {
34 var passes = false, ua = window.navigator.userAgent;
38 passes = ua.match(/MSIE [6-8]/gi) !== null;
41 passes = ua.match(/MSIE/gi) !== null;
44 passes = ua.match(/firefox/gi) !== null;
47 passes = ua.match(/OPR/) !== null;
50 passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) === null;
53 passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) !== null;
62 * Specify compatibility for native playback by browser
66 audio: ['ogg', 'wav'],
67 video: ['ogg', 'webm']
70 audio: ['ogg', 'mpeg'],
71 video: ['ogg', 'webm', 'mp4', 'm4v', 'mpeg']
74 audio: ['ogg', 'mpeg'],
75 video: ['ogg', 'webm']
78 audio: ['mpeg', 'wav'],
79 video: ['mp4', 'm4v', 'mpeg', 'x-ms-wmv', 'quicktime']
83 video: ['mp4', 'm4v', 'mpeg']
88 * Determine if the passed media contains a <source> that provides
89 * native playback in the user's browser
91 * @param {jQuery} media
94 isCompatible: function( media ) {
95 if ( ! media.find( 'source' ).length ) {
99 var ua = this.ua, test = false, found = false, sources;
101 if ( ua.is( 'oldIE' ) ) {
105 sources = media.find( 'source' );
107 _.find( this.compat, function( supports, browser ) {
108 if ( ua.is( browser ) ) {
110 _.each( sources, function( elem ) {
111 var audio = new RegExp( 'audio\/(' + supports.audio.join('|') + ')', 'gi' ),
112 video = new RegExp( 'video\/(' + supports.video.join('|') + ')', 'gi' );
114 if ( elem.type.match( video ) !== null || elem.type.match( audio ) !== null ) {
120 return test || found;
127 * Override the MediaElement method for removing a player.
128 * MediaElement tries to pull the audio/video tag out of
129 * its container and re-add it to the DOM.
131 removePlayer: function(t) {
132 var featureIndex, feature;
134 // invoke features cleanup
135 for ( featureIndex in t.options.features ) {
136 feature = t.options.features[featureIndex];
137 if ( t['clean' + feature] ) {
139 t['clean' + feature](t);
144 if ( ! t.isDynamic ) {
148 if ( 'native' !== t.media.pluginType ) {
152 delete window.mejs.players[t.id];
154 t.container.remove();
156 delete t.node.player;
160 * Allows any class that has set 'player' to a MediaElementPlayer
161 * instance to remove the player when listening to events.
163 * Examples: modal closes, shortcode properties are removed, etc.
165 unsetPlayer : function() {
167 wp.media.mixin.pauseAllPlayers();
168 wp.media.mixin.removePlayer( this.player );
175 * Autowire "collection"-type shortcodes
177 wp.media.playlist = new wp.media.collection({
179 editTitle : l10n.editPlaylistTitle,
181 id: wp.media.view.settings.post.id,
192 * Shortcode modeling for audio
193 * `edit()` prepares the shortcode for the media modal
194 * `shortcode()` builds the new shortcode after update
199 coerce : wp.media.coerce,
202 id : wp.media.view.settings.post.id,
210 edit : function( data ) {
211 var frame, shortcode = wp.shortcode.next( 'audio', data ).shortcode;
214 state: 'audio-details',
215 metadata: _.defaults( shortcode.attrs.named, this.defaults )
221 shortcode : function( model ) {
222 var self = this, content;
224 _.each( this.defaults, function( value, key ) {
225 model[ key ] = self.coerce( model, key );
227 if ( value === model[ key ] ) {
232 content = model.content;
233 delete model.content;
235 return new wp.shortcode({
244 * Shortcode modeling for video
245 * `edit()` prepares the shortcode for the media modal
246 * `shortcode()` builds the new shortcode after update
251 coerce : wp.media.coerce,
254 id : wp.media.view.settings.post.id,
259 preload : 'metadata',
265 edit : function( data ) {
267 shortcode = wp.shortcode.next( 'video', data ).shortcode,
270 attrs = shortcode.attrs.named;
271 attrs.content = shortcode.content;
275 state: 'video-details',
276 metadata: _.defaults( attrs, this.defaults )
282 shortcode : function( model ) {
283 var self = this, content;
285 _.each( this.defaults, function( value, key ) {
286 model[ key ] = self.coerce( model, key );
288 if ( value === model[ key ] ) {
293 content = model.content;
294 delete model.content;
296 return new wp.shortcode({
305 * Shared model class for audio and video. Updates the model after
306 * "Add Audio|Video Source" and "Replace Audio|Video" states return
309 * @augments Backbone.Model
311 media.model.PostMedia = Backbone.Model.extend({
312 initialize: function() {
313 this.attachment = false;
316 setSource: function( attachment ) {
317 this.attachment = attachment;
318 this.extension = attachment.get( 'filename' ).split('.').pop();
320 if ( this.get( 'src' ) && this.extension === this.get( 'src' ).split('.').pop() ) {
324 if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) {
325 this.set( this.extension, this.attachment.get( 'url' ) );
327 this.unset( this.extension );
331 changeAttachment: function( attachment ) {
334 this.setSource( attachment );
337 _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function( ext ) {
344 * The controller for the Audio Details state
347 * @augments wp.media.controller.State
348 * @augments Backbone.Model
350 media.controller.AudioDetails = media.controller.State.extend({
353 toolbar: 'audio-details',
354 title: l10n.audioDetailsTitle,
355 content: 'audio-details',
356 menu: 'audio-details',
361 initialize: function( options ) {
362 this.media = options.media;
363 media.controller.State.prototype.initialize.apply( this, arguments );
368 * The controller for the Video Details state
371 * @augments wp.media.controller.State
372 * @augments Backbone.Model
374 media.controller.VideoDetails = media.controller.State.extend({
377 toolbar: 'video-details',
378 title: l10n.videoDetailsTitle,
379 content: 'video-details',
380 menu: 'video-details',
385 initialize: function( options ) {
386 this.media = options.media;
387 media.controller.State.prototype.initialize.apply( this, arguments );
392 * wp.media.view.MediaFrame.MediaDetails
395 * @augments wp.media.view.MediaFrame.Select
396 * @augments wp.media.view.MediaFrame
397 * @augments wp.media.view.Frame
398 * @augments wp.media.View
399 * @augments wp.Backbone.View
400 * @augments Backbone.View
401 * @mixes wp.media.controller.StateMachine
403 media.view.MediaFrame.MediaDetails = media.view.MediaFrame.Select.extend({
407 menu: 'media-details',
408 content: 'media-details',
409 toolbar: 'media-details',
414 initialize: function( options ) {
415 this.DetailsView = options.DetailsView;
416 this.cancelText = options.cancelText;
417 this.addText = options.addText;
419 this.media = new media.model.PostMedia( options.metadata );
420 this.options.selection = new media.model.Selection( this.media.attachment, { multiple: false } );
421 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
424 bindHandlers: function() {
425 var menu = this.defaults.menu;
427 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
429 this.on( 'menu:create:' + menu, this.createMenu, this );
430 this.on( 'content:render:' + menu, this.renderDetailsContent, this );
431 this.on( 'menu:render:' + menu, this.renderMenu, this );
432 this.on( 'toolbar:render:' + menu, this.renderDetailsToolbar, this );
435 renderDetailsContent: function() {
436 var view = new this.DetailsView({
438 model: this.state().media,
439 attachment: this.state().media.attachment
442 this.content.set( view );
445 renderMenu: function( view ) {
446 var lastState = this.lastState(),
447 previous = lastState && lastState.id,
452 text: this.cancelText,
456 frame.setState( previous );
462 separateCancel: new media.View({
463 className: 'separator',
470 setPrimaryButton: function(text, handler) {
471 this.toolbar.set( new media.view.Toolbar({
479 var controller = this.controller;
480 handler.call( this, controller, controller.state() );
481 // Restore and reset the default state.
482 controller.setState( controller.options.state );
490 renderDetailsToolbar: function() {
491 this.setPrimaryButton( l10n.update, function( controller, state ) {
493 state.trigger( 'update', controller.media.toJSON() );
497 renderReplaceToolbar: function() {
498 this.setPrimaryButton( l10n.replace, function( controller, state ) {
499 var attachment = state.get( 'selection' ).single();
500 controller.media.changeAttachment( attachment );
501 state.trigger( 'replace', controller.media.toJSON() );
505 renderAddSourceToolbar: function() {
506 this.setPrimaryButton( this.addText, function( controller, state ) {
507 var attachment = state.get( 'selection' ).single();
508 controller.media.setSource( attachment );
509 state.trigger( 'add-source', controller.media.toJSON() );
515 * wp.media.view.MediaFrame.AudioDetails
518 * @augments wp.media.view.MediaFrame.MediaDetails
519 * @augments wp.media.view.MediaFrame.Select
520 * @augments wp.media.view.MediaFrame
521 * @augments wp.media.view.Frame
522 * @augments wp.media.View
523 * @augments wp.Backbone.View
524 * @augments Backbone.View
525 * @mixes wp.media.controller.StateMachine
527 media.view.MediaFrame.AudioDetails = media.view.MediaFrame.MediaDetails.extend({
531 menu: 'audio-details',
532 content: 'audio-details',
533 toolbar: 'audio-details',
535 title: l10n.audioDetailsTitle,
539 initialize: function( options ) {
540 options.DetailsView = media.view.AudioDetails;
541 options.cancelText = l10n.audioDetailsCancel;
542 options.addText = l10n.audioAddSourceTitle;
544 media.view.MediaFrame.MediaDetails.prototype.initialize.call( this, options );
547 bindHandlers: function() {
548 media.view.MediaFrame.MediaDetails.prototype.bindHandlers.apply( this, arguments );
550 this.on( 'toolbar:render:replace-audio', this.renderReplaceToolbar, this );
551 this.on( 'toolbar:render:add-audio-source', this.renderAddSourceToolbar, this );
554 createStates: function() {
556 new media.controller.AudioDetails( {
560 new media.controller.MediaLibrary( {
563 title: l10n.audioReplaceTitle,
564 toolbar: 'replace-audio',
566 menu: 'audio-details'
569 new media.controller.MediaLibrary( {
571 id: 'add-audio-source',
572 title: l10n.audioAddSourceTitle,
573 toolbar: 'add-audio-source',
582 * wp.media.view.MediaFrame.VideoDetails
585 * @augments wp.media.view.MediaFrame.MediaDetails
586 * @augments wp.media.view.MediaFrame.Select
587 * @augments wp.media.view.MediaFrame
588 * @augments wp.media.view.Frame
589 * @augments wp.media.View
590 * @augments wp.Backbone.View
591 * @augments Backbone.View
592 * @mixes wp.media.controller.StateMachine
594 media.view.MediaFrame.VideoDetails = media.view.MediaFrame.MediaDetails.extend({
598 menu: 'video-details',
599 content: 'video-details',
600 toolbar: 'video-details',
602 title: l10n.videoDetailsTitle,
606 initialize: function( options ) {
607 options.DetailsView = media.view.VideoDetails;
608 options.cancelText = l10n.videoDetailsCancel;
609 options.addText = l10n.videoAddSourceTitle;
611 media.view.MediaFrame.MediaDetails.prototype.initialize.call( this, options );
614 bindHandlers: function() {
615 media.view.MediaFrame.MediaDetails.prototype.bindHandlers.apply( this, arguments );
617 this.on( 'toolbar:render:replace-video', this.renderReplaceToolbar, this );
618 this.on( 'toolbar:render:add-video-source', this.renderAddSourceToolbar, this );
619 this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this );
620 this.on( 'toolbar:render:add-track', this.renderAddTrackToolbar, this );
623 createStates: function() {
625 new media.controller.VideoDetails({
629 new media.controller.MediaLibrary( {
632 title: l10n.videoReplaceTitle,
633 toolbar: 'replace-video',
635 menu: 'video-details'
638 new media.controller.MediaLibrary( {
640 id: 'add-video-source',
641 title: l10n.videoAddSourceTitle,
642 toolbar: 'add-video-source',
647 new media.controller.MediaLibrary( {
649 id: 'select-poster-image',
650 title: l10n.videoSelectPosterImageTitle,
651 toolbar: 'select-poster-image',
653 menu: 'video-details'
656 new media.controller.MediaLibrary( {
659 title: l10n.videoAddTrackTitle,
660 toolbar: 'add-track',
662 menu: 'video-details'
667 renderSelectPosterImageToolbar: function() {
668 this.setPrimaryButton( l10n.videoSelectPosterImageTitle, function( controller, state ) {
669 var attachment = state.get( 'selection' ).single();
671 controller.media.set( 'poster', attachment.get( 'url' ) );
672 state.trigger( 'set-poster-image', controller.media.toJSON() );
676 renderAddTrackToolbar: function() {
677 this.setPrimaryButton( l10n.videoAddTrackTitle, function( controller, state ) {
678 var attachment = state.get( 'selection' ).single(),
679 content = controller.media.get( 'content' );
681 if ( -1 === content.indexOf( attachment.get( 'url' ) ) ) {
683 '<track srclang="en" label="English"kind="subtitles" src="',
684 attachment.get( 'url' ),
688 controller.media.set( 'content', content );
690 state.trigger( 'add-track', controller.media.toJSON() );
696 * wp.media.view.MediaDetails
699 * @augments wp.media.view.Settings.AttachmentDisplay
700 * @augments wp.media.view.Settings
701 * @augments wp.media.View
702 * @augments wp.Backbone.View
703 * @augments Backbone.View
705 media.view.MediaDetails = media.view.Settings.AttachmentDisplay.extend({
706 initialize: function() {
707 _.bindAll(this, 'success');
709 this.listenTo( this.controller, 'close', media.mixin.unsetPlayer );
710 this.on( 'ready', this.setPlayer );
711 this.on( 'media:setting:remove', media.mixin.unsetPlayer, this );
712 this.on( 'media:setting:remove', this.render );
713 this.on( 'media:setting:remove', this.setPlayer );
714 this.events = _.extend( this.events, {
715 'click .remove-setting' : 'removeSetting',
716 'change .content-track' : 'setTracks',
717 'click .remove-track' : 'setTracks'
720 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
723 prepare: function() {
725 model: this.model.toJSON()
730 * Remove a setting's UI when the model unsets it
732 * @fires wp.media.view.MediaDetails#media:setting:remove
736 removeSetting : function(e) {
737 var wrap = $( e.currentTarget ).parent(), setting;
738 setting = wrap.find( 'input' ).data( 'setting' );
741 this.model.unset( setting );
742 this.trigger( 'media:setting:remove', this );
750 * @fires wp.media.view.MediaDetails#media:setting:remove
752 setTracks : function() {
755 _.each( this.$('.content-track'), function(track) {
756 tracks += $( track ).val();
759 this.model.set( 'content', tracks );
760 this.trigger( 'media:setting:remove', this );
764 * @global MediaElementPlayer
766 setPlayer : function() {
767 if ( ! this.player && this.media ) {
768 this.player = new MediaElementPlayer( this.media, this.settings );
775 setMedia : function() {
779 success : function(mejs) {
780 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay;
782 if ( 'flash' === mejs.pluginType && autoplay ) {
783 mejs.addEventListener( 'canplay', function() {
792 * @returns {media.view.MediaDetails} Returns itself to allow chaining
797 media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
798 setTimeout( function() { self.resetFocus(); }, 10 );
800 this.settings = _.defaults( {
801 success : this.success
804 return this.setMedia();
807 resetFocus: function() {
808 this.$( '.embed-media-settings' ).scrollTop( 0 );
814 * When multiple players in the DOM contain the same src, things get weird.
816 * @param {HTMLElement} elem
817 * @returns {HTMLElement}
819 prepareSrc : function( elem ) {
820 var i = media.view.MediaDetails.instances++;
821 _.each( $( elem ).find( 'source' ), function( source ) {
824 source.src.indexOf('?') > -1 ? '&' : '?',
835 * wp.media.view.AudioDetails
838 * @augments wp.media.view.MediaDetails
839 * @augments wp.media.view.Settings.AttachmentDisplay
840 * @augments wp.media.view.Settings
841 * @augments wp.media.View
842 * @augments wp.Backbone.View
843 * @augments Backbone.View
845 media.view.AudioDetails = media.view.MediaDetails.extend({
846 className: 'audio-details',
847 template: media.template('audio-details'),
849 setMedia: function() {
850 var audio = this.$('.wp-audio-shortcode');
852 if ( audio.find( 'source' ).length ) {
853 if ( audio.is(':hidden') ) {
856 this.media = media.view.MediaDetails.prepareSrc( audio.get(0) );
867 * wp.media.view.VideoDetails
870 * @augments wp.media.view.MediaDetails
871 * @augments wp.media.view.Settings.AttachmentDisplay
872 * @augments wp.media.view.Settings
873 * @augments wp.media.View
874 * @augments wp.Backbone.View
875 * @augments Backbone.View
877 media.view.VideoDetails = media.view.MediaDetails.extend({
878 className: 'video-details',
879 template: media.template('video-details'),
881 setMedia: function() {
882 var video = this.$('.wp-video-shortcode');
884 if ( video.find( 'source' ).length ) {
885 if ( video.is(':hidden') ) {
889 if ( ! video.hasClass('youtube-video') ) {
890 this.media = media.view.MediaDetails.prepareSrc( video.get(0) );
892 this.media = video.get(0);
908 .on( 'click', '.wp-switch-editor', wp.media.mixin.pauseAllPlayers )
909 .on( 'click', '.add-media-source', function( e ) {
910 media.frame.lastMime = $( e.currentTarget ).data( 'mime' );
911 media.frame.setState( 'add-' + media.frame.defaults.id + '-source' );
917 }(jQuery, _, Backbone));