714 lines
17 KiB
JavaScript
714 lines
17 KiB
JavaScript
/* global tinymce, MediaElementPlayer, WPPlaylistView */
|
|
/**
|
|
* Note: this API is "experimental" meaning that it will probably change
|
|
* in the next few releases based on feedback from 3.9.0.
|
|
* If you decide to use it, please follow the development closely.
|
|
*/
|
|
|
|
// Ensure the global `wp` object exists.
|
|
window.wp = window.wp || {};
|
|
|
|
(function($){
|
|
var views = {},
|
|
instances = {},
|
|
media = wp.media,
|
|
viewOptions = ['encodedText'];
|
|
|
|
// Create the `wp.mce` object if necessary.
|
|
wp.mce = wp.mce || {};
|
|
|
|
/**
|
|
* wp.mce.View
|
|
*
|
|
* A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
|
|
* that the TinyMCE View is not tied to a particular DOM node.
|
|
*/
|
|
wp.mce.View = function( options ) {
|
|
options || (options = {});
|
|
_.extend(this, _.pick(options, viewOptions));
|
|
this.initialize.apply(this, arguments);
|
|
};
|
|
|
|
_.extend( wp.mce.View.prototype, {
|
|
initialize: function() {},
|
|
getHtml: function() {},
|
|
render: function() {
|
|
var html = this.getHtml();
|
|
// Search all tinymce editor instances and update the placeholders
|
|
_.each( tinymce.editors, function( editor ) {
|
|
var doc, self = this;
|
|
if ( editor.plugins.wpview ) {
|
|
doc = editor.getDoc();
|
|
$( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).each(function (i, elem) {
|
|
var node = $( elem );
|
|
// The <ins> is used to mark the end of the wrapper div. Needed when comparing
|
|
// the content as string for preventing extra undo levels.
|
|
node.html( html ).append( '<ins data-wpview-end="1"></ins>' );
|
|
$( self ).trigger( 'ready', elem );
|
|
});
|
|
}
|
|
}, this );
|
|
},
|
|
unbind: function() {}
|
|
} );
|
|
|
|
// take advantage of the Backbone extend method
|
|
wp.mce.View.extend = Backbone.View.extend;
|
|
|
|
/**
|
|
* wp.mce.views
|
|
*
|
|
* A set of utilities that simplifies adding custom UI within a TinyMCE editor.
|
|
* At its core, it serves as a series of converters, transforming text to a
|
|
* custom UI, and back again.
|
|
*/
|
|
wp.mce.views = {
|
|
|
|
/**
|
|
* wp.mce.views.register( type, view )
|
|
*
|
|
* Registers a new TinyMCE view.
|
|
*
|
|
* @param type
|
|
* @param constructor
|
|
*
|
|
*/
|
|
register: function( type, constructor ) {
|
|
views[ type ] = constructor;
|
|
},
|
|
|
|
/**
|
|
* wp.mce.views.get( id )
|
|
*
|
|
* Returns a TinyMCE view constructor.
|
|
*/
|
|
get: function( type ) {
|
|
return views[ type ];
|
|
},
|
|
|
|
/**
|
|
* wp.mce.views.unregister( type )
|
|
*
|
|
* Unregisters a TinyMCE view.
|
|
*/
|
|
unregister: function( type ) {
|
|
delete views[ type ];
|
|
},
|
|
|
|
/**
|
|
* wp.mce.views.unbind( editor )
|
|
*
|
|
* The editor DOM is being rebuilt, run cleanup.
|
|
*/
|
|
unbind: function() {
|
|
_.each( instances, function( instance ) {
|
|
instance.unbind();
|
|
} );
|
|
},
|
|
|
|
/**
|
|
* toViews( content )
|
|
* Scans a `content` string for each view's pattern, replacing any
|
|
* matches with wrapper elements, and creates a new instance for
|
|
* every match, which triggers the related data to be fetched.
|
|
*
|
|
*/
|
|
toViews: function( content ) {
|
|
var pieces = [ { content: content } ],
|
|
current;
|
|
|
|
_.each( views, function( view, viewType ) {
|
|
current = pieces.slice();
|
|
pieces = [];
|
|
|
|
_.each( current, function( piece ) {
|
|
var remaining = piece.content,
|
|
result;
|
|
|
|
// Ignore processed pieces, but retain their location.
|
|
if ( piece.processed ) {
|
|
pieces.push( piece );
|
|
return;
|
|
}
|
|
|
|
// Iterate through the string progressively matching views
|
|
// and slicing the string as we go.
|
|
while ( remaining && (result = view.toView( remaining )) ) {
|
|
// Any text before the match becomes an unprocessed piece.
|
|
if ( result.index ) {
|
|
pieces.push({ content: remaining.substring( 0, result.index ) });
|
|
}
|
|
|
|
// Add the processed piece for the match.
|
|
pieces.push({
|
|
content: wp.mce.views.toView( viewType, result.content, result.options ),
|
|
processed: true
|
|
});
|
|
|
|
// Update the remaining content.
|
|
remaining = remaining.slice( result.index + result.content.length );
|
|
}
|
|
|
|
// There are no additional matches. If any content remains,
|
|
// add it as an unprocessed piece.
|
|
if ( remaining ) {
|
|
pieces.push({ content: remaining });
|
|
}
|
|
});
|
|
});
|
|
|
|
return _.pluck( pieces, 'content' ).join('');
|
|
},
|
|
|
|
/**
|
|
* Create a placeholder for a particular view type
|
|
*
|
|
* @param viewType
|
|
* @param text
|
|
* @param options
|
|
*
|
|
*/
|
|
toView: function( viewType, text, options ) {
|
|
var view = wp.mce.views.get( viewType ),
|
|
encodedText = window.encodeURIComponent( text ),
|
|
instance, viewOptions;
|
|
|
|
|
|
if ( ! view ) {
|
|
return text;
|
|
}
|
|
|
|
if ( ! wp.mce.views.getInstance( encodedText ) ) {
|
|
viewOptions = options;
|
|
viewOptions.encodedText = encodedText;
|
|
instance = new view.View( viewOptions );
|
|
instances[ encodedText ] = instance;
|
|
}
|
|
|
|
return wp.html.string({
|
|
tag: 'div',
|
|
|
|
attrs: {
|
|
'class': 'wpview-wrap wpview-type-' + viewType,
|
|
'data-wpview-text': encodedText,
|
|
'data-wpview-type': viewType,
|
|
'contenteditable': 'false'
|
|
},
|
|
|
|
content: '\u00a0'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Refresh views after an update is made
|
|
*
|
|
* @param view {object} being refreshed
|
|
* @param text {string} textual representation of the view
|
|
*/
|
|
refreshView: function( view, text ) {
|
|
var encodedText = window.encodeURIComponent( text ),
|
|
viewOptions,
|
|
result, instance;
|
|
|
|
instance = wp.mce.views.getInstance( encodedText );
|
|
|
|
if ( ! instance ) {
|
|
result = view.toView( text );
|
|
viewOptions = result.options;
|
|
viewOptions.encodedText = encodedText;
|
|
instance = new view.View( viewOptions );
|
|
instances[ encodedText ] = instance;
|
|
}
|
|
|
|
wp.mce.views.render();
|
|
},
|
|
|
|
getInstance: function( encodedText ) {
|
|
return instances[ encodedText ];
|
|
},
|
|
|
|
/**
|
|
* render( scope )
|
|
*
|
|
* Renders any view instances inside a DOM node `scope`.
|
|
*
|
|
* View instances are detected by the presence of wrapper elements.
|
|
* To generate wrapper elements, pass your content through
|
|
* `wp.mce.view.toViews( content )`.
|
|
*/
|
|
render: function() {
|
|
_.each( instances, function( instance ) {
|
|
instance.render();
|
|
} );
|
|
},
|
|
|
|
edit: function( node ) {
|
|
var viewType = $( node ).data('wpview-type'),
|
|
view = wp.mce.views.get( viewType );
|
|
|
|
if ( view ) {
|
|
view.edit( node );
|
|
}
|
|
}
|
|
};
|
|
|
|
wp.mce.gallery = {
|
|
shortcode: 'gallery',
|
|
toView: function( content ) {
|
|
var match = wp.shortcode.next( this.shortcode, content );
|
|
|
|
if ( ! match ) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
index: match.index,
|
|
content: match.content,
|
|
options: {
|
|
shortcode: match.shortcode
|
|
}
|
|
};
|
|
},
|
|
View: wp.mce.View.extend({
|
|
className: 'editor-gallery',
|
|
template: media.template('editor-gallery'),
|
|
|
|
// The fallback post ID to use as a parent for galleries that don't
|
|
// specify the `ids` or `include` parameters.
|
|
//
|
|
// Uses the hidden input on the edit posts page by default.
|
|
postID: $('#post_ID').val(),
|
|
|
|
initialize: function( options ) {
|
|
this.shortcode = options.shortcode;
|
|
this.fetch();
|
|
},
|
|
|
|
fetch: function() {
|
|
this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
|
|
this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
|
|
},
|
|
|
|
getHtml: function() {
|
|
var attrs = this.shortcode.attrs.named,
|
|
attachments = false,
|
|
options;
|
|
|
|
// Don't render errors while still fetching attachments
|
|
if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
|
|
return;
|
|
}
|
|
|
|
if ( this.attachments.length ) {
|
|
attachments = this.attachments.toJSON();
|
|
|
|
_.each( attachments, function( attachment ) {
|
|
if ( attachment.sizes ) {
|
|
if ( attachment.sizes.thumbnail ) {
|
|
attachment.thumbnail = attachment.sizes.thumbnail;
|
|
} else if ( attachment.sizes.full ) {
|
|
attachment.thumbnail = attachment.sizes.full;
|
|
}
|
|
}
|
|
} );
|
|
}
|
|
|
|
options = {
|
|
attachments: attachments,
|
|
columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3
|
|
};
|
|
|
|
return this.template( options );
|
|
|
|
}
|
|
}),
|
|
|
|
edit: function( node ) {
|
|
var gallery = wp.media.gallery,
|
|
self = this,
|
|
frame, data;
|
|
|
|
data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
|
|
frame = gallery.edit( data );
|
|
|
|
frame.state('gallery-edit').on( 'update', function( selection ) {
|
|
var shortcode = gallery.shortcode( selection ).string();
|
|
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
|
|
wp.mce.views.refreshView( self, shortcode );
|
|
frame.detach();
|
|
});
|
|
}
|
|
|
|
};
|
|
wp.mce.views.register( 'gallery', wp.mce.gallery );
|
|
|
|
/**
|
|
* Tiny MCE Views for Audio / Video
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* These are base methods that are shared by each shortcode's MCE controller
|
|
*
|
|
* @mixin
|
|
*/
|
|
wp.mce.media = {
|
|
loaded: false,
|
|
/**
|
|
* @global wp.shortcode
|
|
*
|
|
* @param {string} content
|
|
* @returns {Object}
|
|
*/
|
|
toView: function( content ) {
|
|
var match = wp.shortcode.next( this.shortcode, content );
|
|
|
|
if ( ! match ) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
index: match.index,
|
|
content: match.content,
|
|
options: {
|
|
shortcode: match.shortcode
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Called when a TinyMCE view is clicked for editing.
|
|
* - Parses the shortcode out of the element's data attribute
|
|
* - Calls the `edit` method on the shortcode model
|
|
* - Launches the model window
|
|
* - Bind's an `update` callback which updates the element's data attribute
|
|
* re-renders the view
|
|
*
|
|
* @param {HTMLElement} node
|
|
*/
|
|
edit: function( node ) {
|
|
var media = wp.media[ this.shortcode ],
|
|
self = this,
|
|
frame, data, callback;
|
|
|
|
wp.media.mixin.pauseAllPlayers();
|
|
|
|
data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
|
|
frame = media.edit( data );
|
|
frame.on( 'close', function() {
|
|
frame.detach();
|
|
} );
|
|
|
|
callback = function( selection ) {
|
|
var shortcode = wp.media[ self.shortcode ].shortcode( selection ).string();
|
|
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
|
|
wp.mce.views.refreshView( self, shortcode );
|
|
frame.detach();
|
|
};
|
|
if ( _.isArray( self.state ) ) {
|
|
_.each( self.state, function (state) {
|
|
frame.state( state ).on( 'update', callback );
|
|
} );
|
|
} else {
|
|
frame.state( self.state ).on( 'update', callback );
|
|
}
|
|
frame.open();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Base View class for audio and video shortcodes
|
|
*
|
|
* @constructor
|
|
* @augments wp.mce.View
|
|
* @mixes wp.media.mixin
|
|
*/
|
|
wp.mce.media.View = wp.mce.View.extend({
|
|
initialize: function( options ) {
|
|
this.players = [];
|
|
this.shortcode = options.shortcode;
|
|
_.bindAll( this, 'setPlayer' );
|
|
$(this).on( 'ready', this.setPlayer );
|
|
},
|
|
|
|
/**
|
|
* Creates the player instance for the current node
|
|
*
|
|
* @global MediaElementPlayer
|
|
* @global _wpmejsSettings
|
|
*
|
|
* @param {Event} e
|
|
* @param {HTMLElement} node
|
|
*/
|
|
setPlayer: function(e, node) {
|
|
// if the ready event fires on an empty node
|
|
if ( ! node ) {
|
|
return;
|
|
}
|
|
|
|
var self = this,
|
|
media,
|
|
firefox = this.ua.is( 'ff' ),
|
|
className = '.wp-' + this.shortcode.tag + '-shortcode';
|
|
|
|
if ( this.player ) {
|
|
this.unsetPlayer();
|
|
}
|
|
|
|
media = $( node ).find( className );
|
|
|
|
if ( ! this.isCompatible( media ) ) {
|
|
media.closest( '.wpview-wrap' ).addClass( 'wont-play' );
|
|
if ( ! media.parent().hasClass( 'wpview-wrap' ) ) {
|
|
media.parent().replaceWith( media );
|
|
}
|
|
media.replaceWith( '<p>' + media.find( 'source' ).eq(0).prop( 'src' ) + '</p>' );
|
|
return;
|
|
} else {
|
|
media.closest( '.wpview-wrap' ).removeClass( 'wont-play' );
|
|
if ( firefox ) {
|
|
media.prop( 'preload', 'metadata' );
|
|
} else {
|
|
media.prop( 'preload', 'none' );
|
|
}
|
|
}
|
|
|
|
media = wp.media.view.MediaDetails.prepareSrc( media.get(0) );
|
|
|
|
setTimeout( function() {
|
|
wp.mce.media.loaded = true;
|
|
self.players.push( new MediaElementPlayer( media, self.mejsSettings ) );
|
|
}, wp.mce.media.loaded ? 10 : 500 );
|
|
},
|
|
|
|
/**
|
|
* Pass data to the View's Underscore template and return the compiled output
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getHtml: function() {
|
|
var attrs = _.defaults(
|
|
this.shortcode.attrs.named,
|
|
wp.media[ this.shortcode.tag ].defaults
|
|
);
|
|
return this.template({ model: attrs });
|
|
},
|
|
|
|
unbind: function() {
|
|
var self = this;
|
|
this.pauseAllPlayers();
|
|
_.each( this.players, function (player) {
|
|
self.removePlayer( player );
|
|
} );
|
|
this.players = [];
|
|
}
|
|
});
|
|
_.extend( wp.mce.media.View.prototype, wp.media.mixin );
|
|
|
|
/**
|
|
* TinyMCE handler for the video shortcode
|
|
*
|
|
* @mixes wp.mce.media
|
|
*/
|
|
wp.mce.video = _.extend( {}, wp.mce.media, {
|
|
shortcode: 'video',
|
|
state: 'video-details',
|
|
View: wp.mce.media.View.extend({
|
|
className: 'editor-video',
|
|
template: media.template('editor-video')
|
|
})
|
|
} );
|
|
wp.mce.views.register( 'video', wp.mce.video );
|
|
|
|
/**
|
|
* TinyMCE handler for the audio shortcode
|
|
*
|
|
* @mixes wp.mce.media
|
|
*/
|
|
wp.mce.audio = _.extend( {}, wp.mce.media, {
|
|
shortcode: 'audio',
|
|
state: 'audio-details',
|
|
View: wp.mce.media.View.extend({
|
|
className: 'editor-audio',
|
|
template: media.template('editor-audio')
|
|
})
|
|
} );
|
|
wp.mce.views.register( 'audio', wp.mce.audio );
|
|
|
|
/**
|
|
* Base View class for playlist shortcodes
|
|
*
|
|
* @constructor
|
|
* @augments wp.mce.View
|
|
* @mixes wp.media.mixin
|
|
*/
|
|
wp.mce.media.PlaylistView = wp.mce.View.extend({
|
|
className: 'editor-playlist',
|
|
template: media.template('editor-playlist'),
|
|
|
|
initialize: function( options ) {
|
|
this.data = {};
|
|
this.attachments = [];
|
|
this.shortcode = options.shortcode;
|
|
_.bindAll( this, 'setPlayer' );
|
|
$(this).on('ready', this.setNode);
|
|
},
|
|
|
|
/**
|
|
* Set the element context for the view, and then fetch the playlist's
|
|
* associated attachments.
|
|
*
|
|
* @param {Event} e
|
|
* @param {HTMLElement} node
|
|
*/
|
|
setNode: function(e, node) {
|
|
this.node = node;
|
|
this.fetch();
|
|
},
|
|
|
|
/**
|
|
* Asynchronously fetch the shortcode's attachments
|
|
*/
|
|
fetch: function() {
|
|
this.attachments = wp.media.playlist.attachments( this.shortcode );
|
|
this.attachments.more().done( this.setPlayer );
|
|
},
|
|
|
|
/**
|
|
* Get the HTML for the view (which also set's the data), replace the
|
|
* current HTML, and then invoke the WPPlaylistView instance to render
|
|
* the playlist in the editor
|
|
*
|
|
* @global WPPlaylistView
|
|
* @global tinymce.editors
|
|
*/
|
|
setPlayer: function() {
|
|
var p,
|
|
html = this.getHtml(),
|
|
t = this.encodedText,
|
|
self = this;
|
|
|
|
this.unsetPlayer();
|
|
|
|
_.each( tinymce.editors, function( editor ) {
|
|
var doc;
|
|
if ( editor.plugins.wpview ) {
|
|
doc = editor.getDoc();
|
|
$( doc ).find( '[data-wpview-text="' + t + '"]' ).each(function(i, elem) {
|
|
var node = $( elem );
|
|
node.html( html );
|
|
self.node = elem;
|
|
});
|
|
}
|
|
}, this );
|
|
|
|
if ( ! this.data.tracks ) {
|
|
return;
|
|
}
|
|
|
|
p = new WPPlaylistView({
|
|
el: $( self.node ).find( '.wp-playlist' ).get(0),
|
|
metadata: this.data
|
|
});
|
|
|
|
this.player = p._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 );
|
|
}
|
|
});
|
|
_.extend( wp.mce.media.PlaylistView.prototype, wp.media.mixin );
|
|
|
|
/**
|
|
* TinyMCE handler for the playlist shortcode
|
|
*
|
|
* @mixes wp.mce.media
|
|
*/
|
|
wp.mce.playlist = _.extend( {}, wp.mce.media, {
|
|
shortcode: 'playlist',
|
|
state: ['playlist-edit', 'video-playlist-edit'],
|
|
View: wp.mce.media.PlaylistView
|
|
} );
|
|
wp.mce.views.register( 'playlist', wp.mce.playlist );
|
|
}(jQuery));
|