WordPress/wp-includes/js/mce-view.js
Scott Taylor ba84f57083 Add MCE Views for audio and video. Please clear your browser cache so that you get the latest TinyMCE stylesheet.
* Move TinyMCE shortcode handling from `wpgallery` plugin to `mce-view.js`
* Force `preload="none"` when rendering media in the editor to ensure fast loading (I realize this sounds illogical)
* Move audio and video tag builder logic in `media-template.php` into PHP funcs that can be reused by any code passing `data.model` to an Underscore template
* Pause all players when moving between editor tabs and when moving from the editor to editing in the media modal.
* Rename `wp.media.audio|video.shortcode()` to the more appropriate `wp.media.audio|video.update()` that now returns a `wp.shortcode` object instead of a string.
* Add necessary MediaElement css files to `$mce_css`
* In `wp.mce.View.render()`, support multiple instances of the same shortcode
* When rendering `wp.mce.View`s, fire a ready event that passes the current MCE View root element as context 

See #27389.


Built from https://develop.svn.wordpress.org/trunk@27528


git-svn-id: http://core.svn.wordpress.org/trunk@27371 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2014-03-13 23:10:14 +00:00

416 lines
9.8 KiB
JavaScript

/* global tinymce, _wpmejsSettings, MediaElementPlayer */
// 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() {},
html: 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 );
node.html( html );
$( self ).trigger( 'ready', elem );
});
}
}, this );
}
} );
// 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 ];
},
/**
* 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.attachments.more().done( _.bind( this.render, this ) );
},
getHtml: function() {
var attrs = this.shortcode.attrs.named,
options;
if ( ! this.attachments.length ) {
return;
}
options = {
attachments: this.attachments.toJSON(),
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 ).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 );
wp.mce.media = {
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
}
};
},
edit: function( node ) {
var p,
media = wp.media[ this.shortcode ],
self = this,
frame, data;
for (p in window.mejs.players) {
window.mejs.players[p].pause();
}
data = window.decodeURIComponent( $( node ).data('wpview-text') );
frame = media.edit( data );
frame.on( 'close', function () {
frame.detach();
} );
frame.state( self.shortcode + '-details' ).on( 'update', function( selection ) {
var shortcode = wp.media[ self.shortcode ].update( selection ).string();
$( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
wp.mce.views.refreshView( self, shortcode );
frame.detach();
} );
frame.open();
}
};
wp.mce.media.ViewMixin = wp.mce.View.extend({
initialize: function( options ) {
this.shortcode = options.shortcode;
_.bindAll( this, 'setPlayer' );
$(this).on( 'ready', this.setPlayer );
},
setPlayer: function (e, node) {
// if the ready event fires on an empty node
if ( ! node ) {
return;
}
var id,
media,
settings = {},
className = '.wp-' + this.shortcode.tag + '-shortcode';
media = $( node ).find( className );
if ( media.hasClass( 'rendered' ) ) {
id = media.closest( className ).attr( 'id' );
window.mejs.players[ id ].remove();
} else {
media.addClass( 'rendered' );
}
media.prop( 'preload', 'none' );
media = media.get(0);
if ( ! _.isUndefined( window._wpmejsSettings ) ) {
settings.pluginPath = _wpmejsSettings.pluginPath;
}
media = wp.media.view.MediaDetails.prepareSrc( media );
new MediaElementPlayer( media, settings );
},
getHtml: function() {
var attrs = this.shortcode.attrs.named;
return this.template({ model: attrs });
}
});
wp.mce.video = _.extend( {}, wp.mce.media, {
shortcode: 'video',
View: wp.mce.media.ViewMixin.extend({
className: 'editor-video',
template: media.template('editor-video')
})
} );
wp.mce.views.register( 'video', wp.mce.video );
wp.mce.audio = _.extend( {}, wp.mce.media, {
shortcode: 'audio',
View: wp.mce.media.ViewMixin.extend({
className: 'editor-audio',
template: media.template('editor-audio')
})
} );
wp.mce.views.register( 'audio', wp.mce.audio );
}(jQuery));