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 unsetPlayers : function() {
166 if ( this.players && this.players.length ) {
167 wp.media.mixin.pauseAllPlayers();
168 _.each( this.players, function (player) {
169 wp.media.mixin.removePlayer( player );
177 * Autowire "collection"-type shortcodes
179 wp.media.playlist = new wp.media.collection({
181 editTitle : l10n.editPlaylistTitle,
183 id: wp.media.view.settings.post.id,
194 * Shortcode modeling for audio
195 * `edit()` prepares the shortcode for the media modal
196 * `shortcode()` builds the new shortcode after update
201 coerce : wp.media.coerce,
204 id : wp.media.view.settings.post.id,
212 edit : function( data ) {
213 var frame, shortcode = wp.shortcode.next( 'audio', data ).shortcode;
216 state: 'audio-details',
217 metadata: _.defaults( shortcode.attrs.named, this.defaults )
223 shortcode : function( model ) {
224 var self = this, content;
226 _.each( this.defaults, function( value, key ) {
227 model[ key ] = self.coerce( model, key );
229 if ( value === model[ key ] ) {
234 content = model.content;
235 delete model.content;
237 return new wp.shortcode({
246 * Shortcode modeling for video
247 * `edit()` prepares the shortcode for the media modal
248 * `shortcode()` builds the new shortcode after update
253 coerce : wp.media.coerce,
256 id : wp.media.view.settings.post.id,
261 preload : 'metadata',
267 edit : function( data ) {
269 shortcode = wp.shortcode.next( 'video', data ).shortcode,
272 attrs = shortcode.attrs.named;
273 attrs.content = shortcode.content;
277 state: 'video-details',
278 metadata: _.defaults( attrs, this.defaults )
284 shortcode : function( model ) {
285 var self = this, content;
287 _.each( this.defaults, function( value, key ) {
288 model[ key ] = self.coerce( model, key );
290 if ( value === model[ key ] ) {
295 content = model.content;
296 delete model.content;
298 return new wp.shortcode({
307 * Shared model class for audio and video. Updates the model after
308 * "Add Audio|Video Source" and "Replace Audio|Video" states return
311 * @augments Backbone.Model
313 media.model.PostMedia = Backbone.Model.extend({
314 initialize: function() {
315 this.attachment = false;
318 setSource: function( attachment ) {
319 this.attachment = attachment;
320 this.extension = attachment.get( 'filename' ).split('.').pop();
322 if ( this.get( 'src' ) && this.extension === this.get( 'src' ).split('.').pop() ) {
326 if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) {
327 this.set( this.extension, this.attachment.get( 'url' ) );
329 this.unset( this.extension );
333 changeAttachment: function( attachment ) {
336 this.setSource( attachment );
339 _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function( ext ) {
346 * The controller for the Audio Details state
349 * @augments wp.media.controller.State
350 * @augments Backbone.Model
352 media.controller.AudioDetails = media.controller.State.extend({
355 toolbar: 'audio-details',
356 title: l10n.audioDetailsTitle,
357 content: 'audio-details',
358 menu: 'audio-details',
363 initialize: function( options ) {
364 this.media = options.media;
365 media.controller.State.prototype.initialize.apply( this, arguments );
370 * The controller for the Video Details state
373 * @augments wp.media.controller.State
374 * @augments Backbone.Model
376 media.controller.VideoDetails = media.controller.State.extend({
379 toolbar: 'video-details',
380 title: l10n.videoDetailsTitle,
381 content: 'video-details',
382 menu: 'video-details',
387 initialize: function( options ) {
388 this.media = options.media;
389 media.controller.State.prototype.initialize.apply( this, arguments );
394 * wp.media.view.MediaFrame.MediaDetails
397 * @augments wp.media.view.MediaFrame.Select
398 * @augments wp.media.view.MediaFrame
399 * @augments wp.media.view.Frame
400 * @augments wp.media.View
401 * @augments wp.Backbone.View
402 * @augments Backbone.View
403 * @mixes wp.media.controller.StateMachine
405 media.view.MediaFrame.MediaDetails = media.view.MediaFrame.Select.extend({
409 menu: 'media-details',
410 content: 'media-details',
411 toolbar: 'media-details',
416 initialize: function( options ) {
417 this.DetailsView = options.DetailsView;
418 this.cancelText = options.cancelText;
419 this.addText = options.addText;
421 this.media = new media.model.PostMedia( options.metadata );
422 this.options.selection = new media.model.Selection( this.media.attachment, { multiple: false } );
423 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
426 bindHandlers: function() {
427 var menu = this.defaults.menu;
429 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
431 this.on( 'menu:create:' + menu, this.createMenu, this );
432 this.on( 'content:render:' + menu, this.renderDetailsContent, this );
433 this.on( 'menu:render:' + menu, this.renderMenu, this );
434 this.on( 'toolbar:render:' + menu, this.renderDetailsToolbar, this );
437 renderDetailsContent: function() {
438 var view = new this.DetailsView({
440 model: this.state().media,
441 attachment: this.state().media.attachment
444 this.content.set( view );
447 renderMenu: function( view ) {
448 var lastState = this.lastState(),
449 previous = lastState && lastState.id,
454 text: this.cancelText,
458 frame.setState( previous );
464 separateCancel: new media.View({
465 className: 'separator',
472 setPrimaryButton: function(text, handler) {
473 this.toolbar.set( new media.view.Toolbar({
481 var controller = this.controller;
482 handler.call( this, controller, controller.state() );
483 // Restore and reset the default state.
484 controller.setState( controller.options.state );
492 renderDetailsToolbar: function() {
493 this.setPrimaryButton( l10n.update, function( controller, state ) {
495 state.trigger( 'update', controller.media.toJSON() );
499 renderReplaceToolbar: function() {
500 this.setPrimaryButton( l10n.replace, function( controller, state ) {
501 var attachment = state.get( 'selection' ).single();
502 controller.media.changeAttachment( attachment );
503 state.trigger( 'replace', controller.media.toJSON() );
507 renderAddSourceToolbar: function() {
508 this.setPrimaryButton( this.addText, function( controller, state ) {
509 var attachment = state.get( 'selection' ).single();
510 controller.media.setSource( attachment );
511 state.trigger( 'add-source', controller.media.toJSON() );
517 * wp.media.view.MediaFrame.AudioDetails
520 * @augments wp.media.view.MediaFrame.MediaDetails
521 * @augments wp.media.view.MediaFrame.Select
522 * @augments wp.media.view.MediaFrame
523 * @augments wp.media.view.Frame
524 * @augments wp.media.View
525 * @augments wp.Backbone.View
526 * @augments Backbone.View
527 * @mixes wp.media.controller.StateMachine
529 media.view.MediaFrame.AudioDetails = media.view.MediaFrame.MediaDetails.extend({
533 menu: 'audio-details',
534 content: 'audio-details',
535 toolbar: 'audio-details',
537 title: l10n.audioDetailsTitle,
541 initialize: function( options ) {
542 options.DetailsView = media.view.AudioDetails;
543 options.cancelText = l10n.audioDetailsCancel;
544 options.addText = l10n.audioAddSourceTitle;
546 media.view.MediaFrame.MediaDetails.prototype.initialize.call( this, options );
549 bindHandlers: function() {
550 media.view.MediaFrame.MediaDetails.prototype.bindHandlers.apply( this, arguments );
552 this.on( 'toolbar:render:replace-audio', this.renderReplaceToolbar, this );
553 this.on( 'toolbar:render:add-audio-source', this.renderAddSourceToolbar, this );
556 createStates: function() {
558 new media.controller.AudioDetails( {
562 new media.controller.MediaLibrary( {
565 title: l10n.audioReplaceTitle,
566 toolbar: 'replace-audio',
568 menu: 'audio-details'
571 new media.controller.MediaLibrary( {
573 id: 'add-audio-source',
574 title: l10n.audioAddSourceTitle,
575 toolbar: 'add-audio-source',
584 * wp.media.view.MediaFrame.VideoDetails
587 * @augments wp.media.view.MediaFrame.MediaDetails
588 * @augments wp.media.view.MediaFrame.Select
589 * @augments wp.media.view.MediaFrame
590 * @augments wp.media.view.Frame
591 * @augments wp.media.View
592 * @augments wp.Backbone.View
593 * @augments Backbone.View
594 * @mixes wp.media.controller.StateMachine
596 media.view.MediaFrame.VideoDetails = media.view.MediaFrame.MediaDetails.extend({
600 menu: 'video-details',
601 content: 'video-details',
602 toolbar: 'video-details',
604 title: l10n.videoDetailsTitle,
608 initialize: function( options ) {
609 options.DetailsView = media.view.VideoDetails;
610 options.cancelText = l10n.videoDetailsCancel;
611 options.addText = l10n.videoAddSourceTitle;
613 media.view.MediaFrame.MediaDetails.prototype.initialize.call( this, options );
616 bindHandlers: function() {
617 media.view.MediaFrame.MediaDetails.prototype.bindHandlers.apply( this, arguments );
619 this.on( 'toolbar:render:replace-video', this.renderReplaceToolbar, this );
620 this.on( 'toolbar:render:add-video-source', this.renderAddSourceToolbar, this );
621 this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this );
622 this.on( 'toolbar:render:add-track', this.renderAddTrackToolbar, this );
625 createStates: function() {
627 new media.controller.VideoDetails({
631 new media.controller.MediaLibrary( {
634 title: l10n.videoReplaceTitle,
635 toolbar: 'replace-video',
637 menu: 'video-details'
640 new media.controller.MediaLibrary( {
642 id: 'add-video-source',
643 title: l10n.videoAddSourceTitle,
644 toolbar: 'add-video-source',
649 new media.controller.MediaLibrary( {
651 id: 'select-poster-image',
652 title: l10n.videoSelectPosterImageTitle,
653 toolbar: 'select-poster-image',
655 menu: 'video-details'
658 new media.controller.MediaLibrary( {
661 title: l10n.videoAddTrackTitle,
662 toolbar: 'add-track',
664 menu: 'video-details'
669 renderSelectPosterImageToolbar: function() {
670 this.setPrimaryButton( l10n.videoSelectPosterImageTitle, function( controller, state ) {
671 var attachment = state.get( 'selection' ).single();
673 controller.media.set( 'poster', attachment.get( 'url' ) );
674 state.trigger( 'set-poster-image', controller.media.toJSON() );
678 renderAddTrackToolbar: function() {
679 this.setPrimaryButton( l10n.videoAddTrackTitle, function( controller, state ) {
680 var attachment = state.get( 'selection' ).single(),
681 content = controller.media.get( 'content' );
683 if ( -1 === content.indexOf( attachment.get( 'url' ) ) ) {
685 '<track srclang="en" label="English"kind="subtitles" src="',
686 attachment.get( 'url' ),
690 controller.media.set( 'content', content );
692 state.trigger( 'add-track', controller.media.toJSON() );
698 * wp.media.view.MediaDetails
701 * @augments wp.media.view.Settings.AttachmentDisplay
702 * @augments wp.media.view.Settings
703 * @augments wp.media.View
704 * @augments wp.Backbone.View
705 * @augments Backbone.View
707 media.view.MediaDetails = media.view.Settings.AttachmentDisplay.extend({
708 initialize: function() {
709 _.bindAll(this, 'success');
711 this.listenTo( this.controller, 'close', media.mixin.unsetPlayers );
712 this.on( 'ready', this.setPlayer );
713 this.on( 'media:setting:remove', media.mixin.unsetPlayers, this );
714 this.on( 'media:setting:remove', this.render );
715 this.on( 'media:setting:remove', this.setPlayer );
716 this.events = _.extend( this.events, {
717 'click .remove-setting' : 'removeSetting',
718 'change .content-track' : 'setTracks',
719 'click .remove-track' : 'setTracks'
722 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
725 prepare: function() {
727 model: this.model.toJSON()
732 * Remove a setting's UI when the model unsets it
734 * @fires wp.media.view.MediaDetails#media:setting:remove
738 removeSetting : function(e) {
739 var wrap = $( e.currentTarget ).parent(), setting;
740 setting = wrap.find( 'input' ).data( 'setting' );
743 this.model.unset( setting );
744 this.trigger( 'media:setting:remove', this );
752 * @fires wp.media.view.MediaDetails#media:setting:remove
754 setTracks : function() {
757 _.each( this.$('.content-track'), function(track) {
758 tracks += $( track ).val();
761 this.model.set( 'content', tracks );
762 this.trigger( 'media:setting:remove', this );
766 * @global MediaElementPlayer
768 setPlayer : function() {
769 if ( ! this.players.length && this.media ) {
770 this.players.push( new MediaElementPlayer( this.media, this.settings ) );
777 setMedia : function() {
781 success : function(mejs) {
782 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay;
784 if ( 'flash' === mejs.pluginType && autoplay ) {
785 mejs.addEventListener( 'canplay', function() {
794 * @returns {media.view.MediaDetails} Returns itself to allow chaining
799 media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
800 setTimeout( function() { self.resetFocus(); }, 10 );
802 this.settings = _.defaults( {
803 success : this.success
806 return this.setMedia();
809 resetFocus: function() {
810 this.$( '.embed-media-settings' ).scrollTop( 0 );
816 * When multiple players in the DOM contain the same src, things get weird.
818 * @param {HTMLElement} elem
819 * @returns {HTMLElement}
821 prepareSrc : function( elem ) {
822 var i = media.view.MediaDetails.instances++;
823 _.each( $( elem ).find( 'source' ), function( source ) {
826 source.src.indexOf('?') > -1 ? '&' : '?',
837 * wp.media.view.AudioDetails
840 * @augments wp.media.view.MediaDetails
841 * @augments wp.media.view.Settings.AttachmentDisplay
842 * @augments wp.media.view.Settings
843 * @augments wp.media.View
844 * @augments wp.Backbone.View
845 * @augments Backbone.View
847 media.view.AudioDetails = media.view.MediaDetails.extend({
848 className: 'audio-details',
849 template: media.template('audio-details'),
851 setMedia: function() {
852 var audio = this.$('.wp-audio-shortcode');
854 if ( audio.find( 'source' ).length ) {
855 if ( audio.is(':hidden') ) {
858 this.media = media.view.MediaDetails.prepareSrc( audio.get(0) );
869 * wp.media.view.VideoDetails
872 * @augments wp.media.view.MediaDetails
873 * @augments wp.media.view.Settings.AttachmentDisplay
874 * @augments wp.media.view.Settings
875 * @augments wp.media.View
876 * @augments wp.Backbone.View
877 * @augments Backbone.View
879 media.view.VideoDetails = media.view.MediaDetails.extend({
880 className: 'video-details',
881 template: media.template('video-details'),
883 setMedia: function() {
884 var video = this.$('.wp-video-shortcode');
886 if ( video.find( 'source' ).length ) {
887 if ( video.is(':hidden') ) {
891 if ( ! video.hasClass('youtube-video') ) {
892 this.media = media.view.MediaDetails.prepareSrc( video.get(0) );
894 this.media = video.get(0);
910 .on( 'click', '.wp-switch-editor', wp.media.mixin.pauseAllPlayers )
911 .on( 'click', '.add-media-source', function( e ) {
912 media.frame.lastMime = $( e.currentTarget ).data( 'mime' );
913 media.frame.setState( 'add-' + media.frame.defaults.id + '-source' );
919 }(jQuery, _, Backbone));