Simplify creation of audio, video, and playlist MCE views by placing them in iframe sandboxes.

Wins:
* Eliminates duplication of code between PHP and JS
* Views can load JS without messing with TinyMCE and scope
* MEjs doesn't break when it loads a file plugin-mode. This allows any file type the MEjs supports to play in MCE views.
* YouTube now works as the source for video.
* Users can still style the views, editor stylesheets are included in these sandboxes.
* Audio and Video URLs and `[embed]`s are no longer broken.
* Remove the crazy compat code necessary to determine what file types play in what browser.
* Remove unneeded Underscore templates.
* Remove the compat code for playlists.

See #28905.


git-svn-id: https://develop.svn.wordpress.org/trunk@29179 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Scott Taylor 2014-07-15 22:17:58 +00:00
parent 9e573243cf
commit cba1ae06af
5 changed files with 139 additions and 474 deletions

View File

@ -104,6 +104,65 @@ window.wp = window.wp || {};
}
}, this );
},
/* jshint scripturl: true */
createIframe: function ( content ) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
iframe, iframeDoc, i, resize,
dom = tinymce.DOM;
if ( content.indexOf( '<script' ) !== -1 ) {
iframe = dom.create( 'iframe', {
src: tinymce.Env.ie ? 'javascript:""' : '',
frameBorder: '0',
allowTransparency: 'true',
scrolling: 'no',
style: {
width: '100%',
display: 'block'
}
} );
this.setContent( iframe );
iframeDoc = iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(
'<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
'</head>' +
'<body style="padding: 0; margin: 0;" class="' + dom.doc.body.className + '">' +
content +
'</body>' +
'</html>'
);
iframeDoc.close();
resize = function() {
$( iframe ).height( $( iframeDoc.body ).height() );
};
if ( MutationObserver ) {
new MutationObserver( _.debounce( function() {
resize();
}, 100 ) )
.observe( iframeDoc.body, {
attributes: true,
childList: true,
subtree: true
} );
} else {
for ( i = 1; i < 6; i++ ) {
setTimeout( resize, i * 700 );
}
}
} else {
this.setContent( content );
}
},
setError: function( message, dashicon ) {
this.setContent(
'<div class="wpview-error">' +
@ -421,54 +480,60 @@ window.wp = window.wp || {};
wp.mce.av = {
loaded: false,
View: _.extend( {}, wp.media.mixin, {
View: {
overlay: true,
action: 'parse-media-shortcode',
initialize: function( options ) {
this.players = [];
this.shortcode = options.shortcode;
_.bindAll( this, 'setPlayer', 'pausePlayers' );
$( this ).on( 'ready', this.setPlayer );
$( this ).on( 'ready', function( event, editor ) {
editor.on( 'hide', this.pausePlayers );
} );
$( document ).on( 'media:edit', this.pausePlayers );
this.fetching = false;
_.bindAll( this, 'createIframe', 'setNode', 'fetch' );
$( this ).on( 'ready', this.setNode );
},
/**
* Creates the player instance for the current node
*
* @global MediaElementPlayer
*
* @param {Event} event
* @param {Object} editor
* @param {HTMLElement} node
*/
setPlayer: function( event, editor, node ) {
var self = this,
media;
media = $( node ).find( '.wp-' + this.shortcode.tag + '-shortcode' );
if ( ! this.isCompatible( media ) ) {
media.closest( '.wpview-wrap' ).addClass( 'wont-play' );
media.replaceWith( '<p>' + media.find( 'source' ).eq(0).prop( 'src' ) + '</p>' );
return;
} else {
media.closest( '.wpview-wrap' ).removeClass( 'wont-play' );
if ( this.ua.is( 'ff' ) ) {
media.prop( 'preload', 'metadata' );
} else {
media.prop( 'preload', 'none' );
}
setNode: function () {
if ( this.parsed ) {
this.createIframe( this.parsed );
} else if ( ! this.fetching ) {
this.fetch();
}
},
media = wp.media.view.MediaDetails.prepareSrc( media.get(0) );
fetch: function () {
var self = this;
this.fetching = true;
setTimeout( function() {
wp.mce.av.loaded = true;
self.players.push( new MediaElementPlayer( media, self.mejsSettings ) );
}, wp.mce.av.loaded ? 10 : 500 );
wp.ajax.send( this.action, {
data: {
post_ID: $( '#post_ID' ).val() || 0,
type: this.shortcode.tag,
shortcode: this.shortcode.string()
}
} )
.always( function() {
self.fetching = false;
} )
.done( function( response ) {
if ( response ) {
self.parsed = response;
self.createIframe( response );
}
} )
.fail( function( response ) {
if ( response && response.message ) {
if ( ( response.type === 'not-embeddable' && self.type === 'embed' ) ||
response.type === 'not-ssl' ) {
self.setError( response.message, 'admin-media' );
} else {
self.setContent( '<p>' + self.original + '</p>', null, 'replace' );
}
} else if ( response && response.statusText ) {
self.setError( response.statusText, 'admin-media' );
}
} );
},
/**
@ -477,19 +542,12 @@ window.wp = window.wp || {};
* @returns {string}
*/
getHtml: function() {
var attrs = this.shortcode.attrs.named;
attrs.content = this.shortcode.content;
return this.template({ model: _.defaults(
attrs,
wp.media[ this.shortcode.tag ].defaults )
});
},
unbind: function() {
this.unsetPlayers();
if ( ! this.parsed ) {
return '';
}
return this.parsed;
}
} ),
},
/**
* Called when a TinyMCE view is clicked for editing.
@ -537,10 +595,7 @@ window.wp = window.wp || {};
* @mixes wp.mce.av
*/
wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
state: 'video-details',
View: _.extend( {}, wp.mce.av.View, {
template: media.template( 'editor-video' )
} )
state: 'video-details'
} ) );
/**
@ -549,10 +604,7 @@ window.wp = window.wp || {};
* @mixes wp.mce.av
*/
wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
state: 'audio-details',
View: _.extend( {}, wp.mce.av.View, {
template: media.template( 'editor-audio' )
} )
state: 'audio-details'
} ) );
/**
@ -561,274 +613,34 @@ window.wp = window.wp || {};
* @mixes wp.mce.av
*/
wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
state: ['playlist-edit', 'video-playlist-edit'],
View: _.extend( {}, wp.media.mixin, {
template: media.template( 'editor-playlist' ),
overlay: true,
initialize: function( options ) {
this.players = [];
this.data = {};
this.attachments = [];
this.shortcode = options.shortcode;
$( this ).on( 'ready', function( event, editor ) {
editor.on( 'hide', this.pausePlayers );
} );
$( document ).on( 'media:edit', this.pausePlayers );
this.fetch();
$( this ).on( 'ready', this.setPlaylist );
},
/**
* Asynchronously fetch the shortcode's attachments
*/
fetch: function() {
this.attachments = wp.media.playlist.attachments( this.shortcode );
this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
},
setPlaylist: function( event, editor, element ) {
if ( ! this.data.tracks ) {
return;
}
this.players.push( new WPPlaylistView( {
el: $( element ).find( '.wp-playlist' ).get( 0 ),
metadata: this.data
} ).player );
},
/**
* Set the data that will be used to compile the Underscore template,
* compile the template, and then return it.
*
* @returns {string}
*/
getHtml: function() {
var data = this.shortcode.attrs.named,
model = wp.media.playlist,
options,
attachments,
tracks = [];
// Don't render errors while still fetching attachments
if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
return '';
}
_.each( model.defaults, function( value, key ) {
data[ key ] = model.coerce( data, key );
});
options = {
type: data.type,
style: data.style,
tracklist: data.tracklist,
tracknumbers: data.tracknumbers,
images: data.images,
artists: data.artists
};
if ( ! this.attachments.length ) {
return this.template( options );
}
attachments = this.attachments.toJSON();
_.each( attachments, function( attachment ) {
var size = {}, resize = {}, track = {
src : attachment.url,
type : attachment.mime,
title : attachment.title,
caption : attachment.caption,
description : attachment.description,
meta : attachment.meta
};
if ( 'video' === data.type ) {
size.width = attachment.width;
size.height = attachment.height;
if ( media.view.settings.contentWidth ) {
resize.width = media.view.settings.contentWidth - 22;
resize.height = Math.ceil( ( size.height * resize.width ) / size.width );
if ( ! options.width ) {
options.width = resize.width;
options.height = resize.height;
}
} else {
if ( ! options.width ) {
options.width = attachment.width;
options.height = attachment.height;
}
}
track.dimensions = {
original : size,
resized : _.isEmpty( resize ) ? size : resize
};
} else {
options.width = 400;
}
track.image = attachment.image;
track.thumb = attachment.thumb;
tracks.push( track );
} );
options.tracks = tracks;
this.data = options;
return this.template( options );
},
unbind: function() {
this.unsetPlayers();
}
} )
state: [ 'playlist-edit', 'video-playlist-edit' ]
} ) );
/**
* TinyMCE handler for the embed shortcode
*/
wp.mce.embedView = _.extend( {}, wp.media.mixin, {
overlay: true,
initialize: function( options ) {
this.players = [];
this.content = options.content;
this.fetching = false;
this.parsed = false;
this.original = options.url || options.shortcode.string();
wp.mce.embedMixin = {
View: _.extend( {}, wp.mce.av.View, {
overlay: true,
action: 'parse-embed',
initialize: function( options ) {
this.content = options.content;
this.fetching = false;
this.parsed = false;
this.original = options.url || options.shortcode.string();
if ( options.url ) {
this.shortcode = '[embed]' + options.url + '[/embed]';
} else {
this.shortcode = options.shortcode.string();
}
_.bindAll( this, 'setHtml', 'setNode', 'fetch' );
$( this ).on( 'ready', this.setNode );
},
unbind: function() {
var self = this;
_.each( this.players, function ( player ) {
player.pause();
self.removePlayer( player );
} );
this.players = [];
},
setNode: function () {
if ( this.parsed ) {
this.setHtml( this.parsed );
this.parseMediaShortcodes();
} else if ( ! this.fetching ) {
this.fetch();
}
},
fetch: function () {
var self = this;
this.fetching = true;
wp.ajax.send( 'parse-embed', {
data: {
post_ID: $( '#post_ID' ).val() || 0,
shortcode: this.shortcode
}
} )
.always( function() {
self.fetching = false;
} )
.done( function( response ) {
if ( response ) {
self.parsed = response;
self.setHtml( response );
}
} )
.fail( function( response ) {
if ( response && response.message ) {
if ( ( response.type === 'not-embeddable' && self.type === 'embed' ) ||
response.type === 'not-ssl' ) {
self.setError( response.message, 'admin-media' );
} else {
self.setContent( '<p>' + self.original + '</p>', null, 'replace' );
}
} else if ( response && response.statusText ) {
self.setError( response.statusText, 'admin-media' );
}
} );
},
/* jshint scripturl: true */
setHtml: function ( content ) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
iframe, iframeDoc, i, resize,
dom = tinymce.DOM;
if ( content.indexOf( '<script' ) !== -1 ) {
iframe = dom.create( 'iframe', {
src: tinymce.Env.ie ? 'javascript:""' : '',
frameBorder: '0',
allowTransparency: 'true',
style: {
width: '100%',
display: 'block'
}
} );
this.setContent( iframe );
iframeDoc = iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(
'<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
'</head>' +
'<body>' +
content +
'</body>' +
'</html>'
);
iframeDoc.close();
resize = function() {
$( iframe ).height( $( iframeDoc ).height() );
};
if ( MutationObserver ) {
new MutationObserver( _.debounce( function() {
resize();
}, 100 ) )
.observe( iframeDoc.body, {
attributes: true,
childList: true,
subtree: true
if ( options.url ) {
this.shortcode = media.embed.shortcode( {
url: options.url
} );
} else {
for ( i = 1; i < 6; i++ ) {
setTimeout( resize, i * 700 );
}
this.shortcode = options.shortcode;
}
} else {
this.setContent( content );
_.bindAll( this, 'createIframe', 'setNode', 'fetch' );
$( this ).on( 'ready', this.setNode );
}
this.parseMediaShortcodes();
},
parseMediaShortcodes: function () {
var self = this;
$( '.wp-audio-shortcode, .wp-video-shortcode', this.node ).each( function ( i, element ) {
self.players.push( new MediaElementPlayer( element, self.mejsSettings ) );
} );
}
} );
wp.mce.embedMixin = {
View: wp.mce.embedView,
} ),
edit: function( node ) {
var embed = media.embed,
self = this,

View File

@ -46,103 +46,6 @@
} );
},
/**
* Utility to identify the user's browser
*/
ua: {
is : function( browser ) {
var passes = false, ua = window.navigator.userAgent;
switch ( browser ) {
case 'oldie':
passes = ua.match(/MSIE [6-8]/gi) !== null;
break;
case 'ie':
passes = /MSIE /.test( ua ) || ( /Trident\//.test( ua ) && /rv:\d/.test( ua ) ); // IE11
break;
case 'ff':
passes = ua.match(/firefox/gi) !== null;
break;
case 'opera':
passes = ua.match(/OPR/) !== null;
break;
case 'safari':
passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) === null;
break;
case 'chrome':
passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) !== null;
break;
}
return passes;
}
},
/**
* Specify compatibility for native playback by browser
*/
compat :{
'opera' : {
audio: ['ogg', 'wav'],
video: ['ogg', 'webm']
},
'chrome' : {
audio: ['ogg', 'mpeg'],
video: ['ogg', 'webm', 'mp4', 'm4v', 'mpeg']
},
'ff' : {
audio: ['ogg', 'mpeg'],
video: ['ogg', 'webm']
},
'safari' : {
audio: ['mpeg', 'wav'],
video: ['mp4', 'm4v', 'mpeg', 'x-ms-wmv', 'quicktime']
},
'ie' : {
audio: ['mpeg'],
video: ['mp4', 'm4v', 'mpeg']
}
},
/**
* Determine if the passed media contains a <source> that provides
* native playback in the user's browser
*
* @param {jQuery} media
* @returns {Boolean}
*/
isCompatible: function( media ) {
if ( ! media.find( 'source' ).length ) {
return false;
}
var ua = this.ua, test = false, found = false, sources;
if ( ua.is( 'oldIE' ) ) {
return false;
}
sources = media.find( 'source' );
_.find( this.compat, function( supports, browser ) {
if ( ua.is( browser ) ) {
found = true;
_.each( sources, function( elem ) {
var audio = new RegExp( 'audio\/(' + supports.audio.join('|') + ')', 'gi' ),
video = new RegExp( 'video\/(' + supports.video.join('|') + ')', 'gi' );
if ( elem.type.match( video ) !== null || elem.type.match( audio ) !== null ) {
test = true;
}
} );
}
return test || found;
} );
return test;
},
/**
* Override the MediaElement method for removing a player.
* MediaElement tries to pull the audio/video tag out of

View File

@ -111,6 +111,10 @@ video.wp-video-shortcode,
line-height: 1.5;
}
.wp-admin .wp-playlist {
margin: 0 0 18px;
}
.wp-playlist video {
display: inline-block;
max-width: 100%;

View File

@ -7,7 +7,6 @@
initialize : function (options) {
this.index = 0;
this.settings = {};
this.compatMode = $( 'body' ).hasClass( 'wp-admin' ) && $( '#content_ifr' ).length;
this.data = options.metadata || $.parseJSON( this.$('script').html() );
this.playerNode = this.$( this.data.type );
@ -27,9 +26,7 @@
this.renderTracks();
}
if ( this.isCompatibleSrc() ) {
this.playerNode.attr( 'src', this.current.get( 'src' ) );
}
this.playerNode.attr( 'src', this.current.get( 'src' ) );
_.bindAll( this, 'bindPlayer', 'bindResetPlayer', 'setPlayer', 'ended', 'clickTrack' );
@ -47,25 +44,7 @@
bindResetPlayer : function (mejs) {
this.bindPlayer( mejs );
if ( this.isCompatibleSrc() ) {
this.playCurrentSrc();
}
},
isCompatibleSrc: function () {
var testNode;
if ( this.compatMode ) {
testNode = $( '<span><source type="' + this.current.get( 'type' ) + '" /></span>' );
if ( ! wp.media.mixin.isCompatible( testNode ) ) {
this.playerNode.removeAttr( 'src' );
this.playerNode.removeAttr( 'poster' );
return;
}
}
return true;
this.playCurrentSrc();
},
setPlayer: function (force) {
@ -76,9 +55,7 @@
}
if (force) {
if ( this.isCompatibleSrc() ) {
this.playerNode.attr( 'src', this.current.get( 'src' ) );
}
this.playerNode.attr( 'src', this.current.get( 'src' ) );
this.settings.success = this.bindResetPlayer;
}
@ -188,11 +165,9 @@
});
$(document).ready(function () {
if ( ! $( 'body' ).hasClass( 'wp-admin' ) || $( 'body' ).hasClass( 'about-php' ) ) {
$('.wp-playlist').each(function () {
return new WPPlaylistView({ el: this });
});
}
$('.wp-playlist').each( function() {
return new WPPlaylistView({ el: this });
} );
});
window.WPPlaylistView = WPPlaylistView;

View File

@ -1216,35 +1216,6 @@ function wp_print_media_templates() {
<# } #>
</script>
<script type="text/html" id="tmpl-editor-audio">
<?php wp_underscore_audio_template() ?>
</script>
<script type="text/html" id="tmpl-editor-video">
<?php wp_underscore_video_template() ?>
</script>
<?php wp_underscore_playlist_templates() ?>
<script type="text/html" id="tmpl-editor-playlist">
<# if ( data.tracks ) { #>
<div class="wp-playlist wp-{{ data.type }}-playlist wp-playlist-{{ data.style }}">
<# if ( 'audio' === data.type ){ #>
<div class="wp-playlist-current-item"></div>
<# } #>
<{{ data.type }} controls="controls" preload="none" <#
if ( data.width ) { #> width="{{ data.width }}"<# }
#><# if ( data.height ) { #> height="{{ data.height }}"<# } #>></{{ data.type }}>
<div class="wp-playlist-next"></div>
<div class="wp-playlist-prev"></div>
</div>
<# } else { #>
<div class="wpview-error">
<div class="dashicons dashicons-video-alt3"></div><p><?php _e( 'No items found.' ); ?></p>
</div>
<# } #>
</script>
<script type="text/html" id="tmpl-crop-content">
<img class="crop-image" src="{{ data.url }}">
<div class="upload-errors"></div>