From 3c8d2a99f21878ee20600dde31aa7e08585ff0fb Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sun, 9 Mar 2014 05:24:10 +0000 Subject: [PATCH] Audio/Video shortcodes in the media modal: * Make a generic model, `wp.media.model.PostMedia`, which replaces `wp.media.model.PostAudio` and `wp.media.model.PostVideo` * Make a generic library, `wp.media.controller.MediaLibrary`, which replaces `wp.media.controller.ReplaceVideo` and `wp.media.controller.ReplaceAudio` * `wp.media.controller.MediaLibrary` is used to create new states that want to load a library filtered by type, making it incredibly simple to add states to frames. See `wp.media.view.MediaFrame.VideoDetails` and `wp.media.view.MediaFrame.AudioDetails`. UX changes: * Add the ability to manage HTML5 fallbacks - add additional ``s or remove specific ``s * In the Video Details state, add the ability to select a poster image See #27016. git-svn-id: https://develop.svn.wordpress.org/trunk@27478 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/js/media-models.js | 53 +-- src/wp-includes/js/media-views.js | 345 +++++++++++------- .../js/mediaelement/wp-mediaelement.css | 27 +- .../js/tinymce/plugins/wpgallery/plugin.js | 4 +- src/wp-includes/media-template.php | 23 +- src/wp-includes/media.php | 14 +- 6 files changed, 272 insertions(+), 194 deletions(-) diff --git a/src/wp-includes/js/media-models.js b/src/wp-includes/js/media-models.js index 1d22ac55a8..92517e23c3 100644 --- a/src/wp-includes/js/media-models.js +++ b/src/wp-includes/js/media-models.js @@ -453,56 +453,31 @@ window.wp = window.wp || {}; }); /** - * wp.media.model.PostAudio + * wp.media.model.PostMedia * * @constructor * @augments Backbone.Model **/ - media.model.PostAudio = Backbone.Model.extend({ + media.model.PostMedia = Backbone.Model.extend({ initialize: function() { this.attachment = false; }, + setSource: function ( attachment ) { + this.attachment = attachment; + this.extension = attachment.get('filename' ).split('.').pop(); + + if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { + this.set( this.extension, this.attachment.get( 'url' ) ); + } else { + this.unset( this.extension ); + } + }, + changeAttachment: function( attachment ) { var self = this; - this.attachment = attachment; - this.extension = attachment.get('filename' ).split('.').pop(); - - if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { - this.set( this.extension, attachment.get( 'url' ) ); - } else { - this.unset( this.extension ); - } - - _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function (ext) { - self.unset( ext ); - } ); - } - }); - - /** - * wp.media.model.PostVideo - * - * @constructor - * @augments Backbone.Model - **/ - media.model.PostVideo = Backbone.Model.extend({ - initialize: function() { - this.attachment = false; - }, - - changeAttachment: function( attachment ) { - var self = this; - - this.attachment = attachment; - this.extension = attachment.get('filename' ).split('.').pop(); - - if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { - this.set( this.extension, attachment.get( 'url' ) ); - } else { - this.unset( this.extension ); - } + this.setSource( attachment ); _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function (ext) { self.unset( ext ); diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js index e3ca06650d..2f499f6495 100644 --- a/src/wp-includes/js/media-views.js +++ b/src/wp-includes/js/media-views.js @@ -786,7 +786,7 @@ }, media.controller.Library.prototype.defaults ), initialize: function( options ) { - this.audio = options.audio; + this.media = options.media; media.controller.State.prototype.initialize.apply( this, arguments ); } }); @@ -812,7 +812,7 @@ }, media.controller.Library.prototype.defaults ), initialize: function( options ) { - this.video = options.video; + this.media = options.media; media.controller.State.prototype.initialize.apply( this, arguments ); } }); @@ -1183,34 +1183,27 @@ _.extend( media.controller.EditImage.prototype, media.selectionSync ); /** - * wp.media.controller.ReplaceVideo - * - * Replace a selected single video + * wp.media.controller.MediaLibrary * * @constructor * @augments wp.media.controller.Library * @augments wp.media.controller.State * @augments Backbone.Model */ - media.controller.ReplaceVideo = media.controller.Library.extend({ + media.controller.MediaLibrary = media.controller.Library.extend({ defaults: _.defaults({ - id: 'replace-video', filterable: 'uploaded', multiple: false, - toolbar: 'replace', - title: l10n.replaceVideoTitle, - priority: 60, - syncSelection: false + priority: 80, + syncSelection: false, + displaySettings: true }, media.controller.Library.prototype.defaults ), initialize: function( options ) { var library, comparator; - this.video = options.video; - // If we haven't been provided a `library`, create a `Selection`. - if ( ! this.get('library') ) { - this.set( 'library', media.query({ type: 'video' }) ); - } + this.media = options.media; + this.set( 'library', media.query({ type: options.type }) ); media.controller.Library.prototype.initialize.apply( this, arguments ); @@ -1244,75 +1237,9 @@ updateSelection: function() { var selection = this.get('selection'), - attachment = this.video.attachment; + attachment; - selection.reset( attachment ? [ attachment ] : [] ); - } - }); - - /** - * wp.media.controller.ReplaceAudio - * - * Replace a selected single audio file - * - * @constructor - * @augments wp.media.controller.Library - * @augments wp.media.controller.State - * @augments Backbone.Model - */ - media.controller.ReplaceAudio = media.controller.Library.extend({ - defaults: _.defaults({ - id: 'replace-audio', - filterable: 'uploaded', - multiple: false, - toolbar: 'replace', - title: l10n.replaceAudioTitle, - priority: 60, - syncSelection: false - }, media.controller.Library.prototype.defaults ), - - initialize: function( options ) { - var library, comparator; - - this.audio = options.audio; - // If we haven't been provided a `library`, create a `Selection`. - if ( ! this.get('library') ) { - this.set( 'library', media.query({ type: 'audio' }) ); - } - - media.controller.Library.prototype.initialize.apply( this, arguments ); - - library = this.get('library'); - comparator = library.comparator; - - // Overload the library's comparator to push items that are not in - // the mirrored query to the front of the aggregate collection. - library.comparator = function( a, b ) { - var aInQuery = !! this.mirroring.get( a.cid ), - bInQuery = !! this.mirroring.get( b.cid ); - - if ( ! aInQuery && bInQuery ) { - return -1; - } else if ( aInQuery && ! bInQuery ) { - return 1; - } else { - return comparator.apply( this, arguments ); - } - }; - - // Add all items in the selection to the library, so any featured - // images that are not initially loaded still appear. - library.observe( this.get('selection') ); - }, - - activate: function() { - this.updateSelection(); - media.controller.Library.prototype.activate.apply( this, arguments ); - }, - - updateSelection: function() { - var selection = this.get('selection'), - attachment = this.audio.attachment; + attachment = this.media.attachment; selection.reset( attachment ? [ attachment ] : [] ); } @@ -2794,8 +2721,8 @@ }, initialize: function( options ) { - this.audio = new media.model.PostAudio( options.metadata ); - this.options.selection = new media.model.Selection( this.audio.attachment, { multiple: false } ); + this.media = new media.model.PostMedia( options.metadata ); + this.options.selection = new media.model.Selection( this.media.attachment, { multiple: false } ); media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); }, @@ -2806,35 +2733,44 @@ this.on( 'menu:render:audio-details', this.renderMenu, this ); this.on( 'toolbar:render:audio-details', this.renderAudioDetailsToolbar, this ); // override the select toolbar - this.on( 'toolbar:render:replace', this.renderReplaceAudioToolbar, this ); + this.on( 'toolbar:render:replace-audio', this.renderReplaceAudioToolbar, this ); + this.on( 'toolbar:render:add-audio-source', this.renderAddAudioSourceToolbar, this ); }, createStates: function() { this.states.add([ - new media.controller.AudioDetails({ - audio: this.audio, + new media.controller.AudioDetails( { + media: this.media, editable: false, menu: 'audio-details' - }), - new media.controller.ReplaceAudio({ + } ), + + new media.controller.MediaLibrary( { + type: 'audio', id: 'replace-audio', - library: media.query( { type: 'audio' } ), - audio: this.audio, - multiple: false, - title: l10n.audioReplaceTitle, - menu: 'audio-details', - toolbar: 'replace', - priority: 80, - displaySettings: true - }) + title: l10n.audioReplaceTitle, + toolbar: 'replace-audio', + media: this.media, + menu: 'audio-details' + } ), + + + new media.controller.MediaLibrary( { + type: 'audio', + id: 'add-audio-source', + title: l10n.audioAddSourceTitle, + toolbar: 'add-audio-source', + media: this.media, + menu: 'audio-details' + } ) ]); }, renderAudioDetailsContent: function() { var view = new media.view.AudioDetails({ controller: this, - model: this.state().audio, - attachment: this.state().audio.attachment + model: this.state().media, + attachment: this.state().media.attachment }).render(); this.content.set( view ); @@ -2880,9 +2816,7 @@ controller.close(); - // not sure if we want to use wp.media.string.image which will create a shortcode or - // perhaps wp.html.string to at least to build the - state.trigger( 'update', controller.audio.toJSON() ); + state.trigger( 'update', controller.media.toJSON() ); // Restore and reset the default state. controller.setState( controller.options.state ); @@ -2908,11 +2842,39 @@ selection = state.get( 'selection' ), attachment = selection.single(); - controller.audio.changeAttachment( attachment, state.display( attachment ) ); + controller.media.changeAttachment( attachment, state.display( attachment ) ); // not sure if we want to use wp.media.string.image which will create a shortcode or // perhaps wp.html.string to at least to build the - state.trigger( 'replace', controller.audio.toJSON() ); + state.trigger( 'replace', controller.media.toJSON() ); + + // Restore and reset the default state. + controller.setState( controller.options.state ); + controller.reset(); + } + } + } + }) ); + }, + + renderAddAudioSourceToolbar: function() { + this.toolbar.set( new media.view.Toolbar({ + controller: this, + items: { + replace: { + style: 'primary', + text: l10n.audioAddSourceTitle, + priority: 80, + + click: function() { + var controller = this.controller, + state = controller.state(), + selection = state.get( 'selection' ), + attachment = selection.single(); + + controller.media.setSource( attachment, state.display( attachment ) ); + + state.trigger( 'add-source', controller.media.toJSON() ); // Restore and reset the default state. controller.setState( controller.options.state ); @@ -2949,8 +2911,8 @@ }, initialize: function( options ) { - this.video = new media.model.PostVideo( options.metadata ); - this.options.selection = new media.model.Selection( this.video.attachment, { multiple: false } ); + this.media = new media.model.PostMedia( options.metadata ); + this.options.selection = new media.model.Selection( this.media.attachment, { multiple: false } ); media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); }, @@ -2961,35 +2923,53 @@ this.on( 'menu:render:video-details', this.renderMenu, this ); this.on( 'toolbar:render:video-details', this.renderVideoDetailsToolbar, this ); // override the select toolbar - this.on( 'toolbar:render:replace', this.renderReplaceVideoToolbar, this ); + this.on( 'toolbar:render:replace-video', this.renderReplaceVideoToolbar, this ); + this.on( 'toolbar:render:add-video-source', this.renderAddVideoSourceToolbar, this ); + this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this ); }, createStates: function() { this.states.add([ new media.controller.VideoDetails({ - video: this.video, + media: this.media, editable: false, menu: 'video-details' }), - new media.controller.ReplaceVideo({ + + new media.controller.MediaLibrary( { + type: 'video', id: 'replace-video', - library: media.query( { type: 'video' } ), - video: this.video, - multiple: false, - title: l10n.videoReplaceTitle, - menu: 'video-details', - toolbar: 'replace', - priority: 80, - displaySettings: true - }) + title: l10n.videoReplaceTitle, + toolbar: 'replace-video', + media: this.media, + menu: 'video-details' + } ), + + new media.controller.MediaLibrary( { + type: 'video', + id: 'add-video-source', + title: l10n.videoAddSourceTitle, + toolbar: 'add-video-source', + media: this.media, + menu: 'video-details' + } ), + + new media.controller.MediaLibrary( { + type: 'image', + id: 'select-poster-image', + title: l10n.videoSelectPosterImageTitle, + toolbar: 'select-poster-image', + media: this.media, + menu: 'video-details' + } ) ]); }, renderVideoDetailsContent: function() { var view = new media.view.VideoDetails({ controller: this, - model: this.state().video, - attachment: this.state().video.attachment + model: this.state().media, + attachment: this.state().media.attachment }).render(); this.content.set( view ); @@ -3037,7 +3017,7 @@ // not sure if we want to use wp.media.string.image which will create a shortcode or // perhaps wp.html.string to at least to build the - state.trigger( 'update', controller.video.toJSON() ); + state.trigger( 'update', controller.media.toJSON() ); // Restore and reset the default state. controller.setState( controller.options.state ); @@ -3063,9 +3043,65 @@ selection = state.get( 'selection' ), attachment = selection.single(); - controller.video.changeAttachment( attachment, state.display( attachment ) ); + controller.media.changeAttachment( attachment, state.display( attachment ) ); - state.trigger( 'replace', controller.video.toJSON() ); + state.trigger( 'replace', controller.media.toJSON() ); + + // Restore and reset the default state. + controller.setState( controller.options.state ); + controller.reset(); + } + } + } + }) ); + }, + + renderAddVideoSourceToolbar: function() { + this.toolbar.set( new media.view.Toolbar({ + controller: this, + items: { + replace: { + style: 'primary', + text: l10n.videoAddSourceTitle, + priority: 80, + + click: function() { + var controller = this.controller, + state = controller.state(), + selection = state.get( 'selection' ), + attachment = selection.single(); + + controller.media.setSource( attachment, state.display( attachment ) ); + + state.trigger( 'add-source', controller.media.toJSON() ); + + // Restore and reset the default state. + controller.setState( controller.options.state ); + controller.reset(); + } + } + } + }) ); + }, + + renderSelectPosterImageToolbar: function() { + this.toolbar.set( new media.view.Toolbar({ + controller: this, + items: { + replace: { + style: 'primary', + text: l10n.videoSelectPosterImageTitle, + priority: 80, + + click: function() { + var controller = this.controller, + state = controller.state(), + selection = state.get( 'selection' ), + attachment = selection.single(); + + controller.media.set( 'poster', attachment.get( 'url' ) ); + + state.trigger( 'set-poster-image', controller.media.toJSON() ); // Restore and reset the default state. controller.setState( controller.options.state ); @@ -6415,10 +6451,16 @@ */ media.view.MediaDetails = media.view.Settings.AttachmentDisplay.extend({ initialize: function() { - _.bindAll(this, 'success', 'close'); + _.bindAll(this, 'success', 'unsetPlayer'); - this.listenTo( this.controller, 'close', this.close ); + this.listenTo( this.controller, 'close', this.unsetPlayer ); this.on( 'ready', this.setPlayer ); + this.on( 'media:setting:remove', this.unsetPlayer ); + this.on( 'media:setting:remove', this.render ); + this.on( 'media:setting:remove', this.setPlayer ); + this.events = _.extend( this.events, { + 'click .remove-setting' : 'removeSetting' + } ); media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments ); }, @@ -6454,6 +6496,16 @@ return media; }, + removeSetting : function (e) { + var setting = $( e.currentTarget ).parent(); + + this.model.unset( setting.find( 'input' ).data( 'setting' ) ); + + setting.remove(); + + this.trigger( 'media:setting:remove', this ); + }, + setPlayer : function () { if ( ! this.player && this.media ) { this.player = new MediaElementPlayer( this.media, this.settings ); @@ -6472,7 +6524,7 @@ * MediaElement tries to pull the audio/video tag out of * its container and re-add it to the DOM. */ - remove: function() { + removePlayer: function() { var t = this.player, featureIndex, feature; // invoke features cleanup @@ -6500,12 +6552,12 @@ delete t.node.player; }, - close : function() { + unsetPlayer : function() { if ( this.player ) { if ( _.isUndefined( this.mejs.pluginType ) ) { this.mejs.pause(); } - this.remove(); + this.removePlayer(); this.player = false; } }, @@ -6560,9 +6612,17 @@ template: media.template('audio-details'), setMedia: function() { - var audio = this.$('.wp-audio-shortcode').get(0); + var audio = this.$('.wp-audio-shortcode'); - this.media = this.prepareSrc( audio ); + if ( audio.find( 'source' ).length ) { + if ( audio.is(':hidden') ) { + audio.show(); + } + this.media = this.prepareSrc( audio.get(0) ); + } else { + audio.hide(); + this.media = false; + } return this; } @@ -6586,13 +6646,20 @@ setMedia: function() { var video = this.$('.wp-video-shortcode'); - if ( ! video.hasClass('youtube-video') ) { - video = this.prepareSrc( video.get(0) ); - } else { - video = video.get(0); - } + if ( video.find( 'source' ).length ) { + if ( video.is(':hidden') ) { + video.show(); + } - this.media = video; + if ( ! video.hasClass('youtube-video') ) { + this.media = this.prepareSrc( video.get(0) ); + } else { + this.media = video.get(0); + } + } else { + video.hide(); + this.media = false; + } return this; } diff --git a/src/wp-includes/js/mediaelement/wp-mediaelement.css b/src/wp-includes/js/mediaelement/wp-mediaelement.css index 623ddb01da..34750c3f70 100644 --- a/src/wp-includes/js/mediaelement/wp-mediaelement.css +++ b/src/wp-includes/js/mediaelement/wp-mediaelement.css @@ -14,18 +14,33 @@ width: auto !important; } -.audio-details .wp-audio-shortcode { +.media-embed-details .wp-audio-shortcode { display: inline-block; max-width: 400px; } -.video-details .embed-video-settings, -.audio-details .embed-audio-settings { - top: 10px; +.media-embed-details .embed-media-settings { + padding-top: 0; } -.video-details .embed-video-settings .checkbox-setting, -.audio-details .embed-audio-settings .checkbox-setting { +.media-embed-details .instructions { + padding: 16px; + max-width: 600px; +} + +.media-embed-details .setting a { + color: #a00; +} + +.media-embed-details .setting a:hover { + color: #f00; +} + +.media-embed-details .embed-media-settings { + top: 70px; +} + +.media-embed-details .embed-media-settings .checkbox-setting { width: 100px; clear: none; } diff --git a/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js b/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js index cb73983d89..018d91eb21 100644 --- a/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js +++ b/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js @@ -97,7 +97,7 @@ tinymce.PluginManager.add('wpgallery', function( editor ) { frame.on( 'close', function () { frame.detach(); } ); - frame.state( 'video-details' ).on( 'update replace', function ( selection ) { + frame.state( 'video-details' ).on( 'update replace add-source select-poster-image', function ( selection ) { var shortcode = wp.media.video.shortcode( selection ); editor.dom.setAttrib( node, 'data-wp-media', window.encodeURIComponent( shortcode ) ); frame.detach(); @@ -108,7 +108,7 @@ tinymce.PluginManager.add('wpgallery', function( editor ) { frame.on( 'close', function () { frame.detach(); } ); - frame.state( 'audio-details' ).on( 'update replace', function ( selection ) { + frame.state( 'audio-details' ).on( 'update replace add-source', function ( selection ) { var shortcode = wp.media.audio.shortcode( selection ); editor.dom.setAttrib( node, 'data-wp-media', window.encodeURIComponent( shortcode ) ); frame.detach(); diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index f70db59407..3cd510098b 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -663,7 +663,8 @@ function wp_print_media_templates() {