X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/wordpress.git/blobdiff_plain/0461a5f2e55c8d5f1fde96ca2e83117152573c7d..9e77185fafaf4e60e2b73821e0e4b9b1a11fb85f:/wp-includes/js/mce-view.js diff --git a/wp-includes/js/mce-view.js b/wp-includes/js/mce-view.js index ec167ca4..85484ef4 100644 --- a/wp-includes/js/mce-view.js +++ b/wp-includes/js/mce-view.js @@ -1,4 +1,4 @@ -/* global tinymce, MediaElementPlayer, WPPlaylistView */ +/* global tinymce */ /** * Note: this API is "experimental" meaning that it will probably change * in the next few releases based on feedback from 3.9.0. @@ -8,7 +8,9 @@ // Ensure the global `wp` object exists. window.wp = window.wp || {}; -(function($){ +( function( $ ) { + 'use strict'; + var views = {}, instances = {}, media = wp.media, @@ -22,34 +24,237 @@ window.wp = window.wp || {}; * * 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. + * + * @param {Object} [options={}] */ wp.mce.View = function( options ) { - options || (options = {}); - _.extend(this, _.pick(options, viewOptions)); - this.initialize.apply(this, arguments); + options = options || {}; + this.type = options.type; + _.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 + getHtml: function() { + return ''; + }, + loadingPlaceholder: function() { + return '' + + '
' + + '
' + + '
' + + '
'; + }, + render: function( force ) { + if ( force || ! this.rendered() ) { + this.unbind(); + + this.setContent( + '

\u00a0

' + + '
' + + '
' + + ( _.isFunction( views[ this.type ].edit ) ? '
' : '' ) + + '
' + + '
' + + '
' + + ( this.getHtml() || this.loadingPlaceholder() ) + + '
' + + ( this.overlay ? '
' : '' ) + + '
' + + '

\u00a0

', + 'wrap' + ); + + $( this ).trigger( 'ready' ); + + this.rendered( true ); + } + }, + unbind: function() {}, + getEditors: function( callback ) { + var editors = []; + _.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 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( '' ); - $( self ).trigger( 'ready', elem ); - }); + if ( callback ) { + callback( editor ); + } + + editors.push( editor ); } }, this ); + + return editors; }, - unbind: function() {} + getNodes: function( callback ) { + var nodes = [], + self = this; + + this.getEditors( function( editor ) { + $( editor.getBody() ) + .find( '[data-wpview-text="' + self.encodedText + '"]' ) + .each( function ( i, node ) { + if ( callback ) { + callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) ); + } + + nodes.push( node ); + } ); + } ); + + return nodes; + }, + setContent: function( html, option ) { + this.getNodes( function ( editor, node, content ) { + var el = ( option === 'wrap' || option === 'replace' ) ? node : content, + insert = html; + + if ( _.isString( insert ) ) { + insert = editor.dom.createFragment( insert ); + } + + if ( option === 'replace' ) { + editor.dom.replace( insert, el ); + } else { + el.innerHTML = ''; + el.appendChild( insert ); + } + } ); + }, + /* jshint scripturl: true */ + setIframes: function ( head, body ) { + var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, + importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist'; + + if ( head || body.indexOf( ' Visual. + setTimeout( function() { + iframe = dom.add( content, 'iframe', { + src: tinymce.Env.ie ? 'javascript:""' : '', + frameBorder: '0', + allowTransparency: 'true', + scrolling: 'no', + 'class': 'wpview-sandbox', + style: { + width: '100%', + display: 'block' + } + } ); + + iframeDoc = iframe.contentWindow.document; + + iframeDoc.open(); + iframeDoc.write( + '' + + '' + + '' + + '' + + head + + styles + + '' + + '' + + '' + + body + + '' + + '' + ); + iframeDoc.close(); + + resize = function() { + // Make sure the iframe still exists. + iframe.contentWindow && $( 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 ); + } + } + + if ( importStyles ) { + editor.on( 'wp-body-class-change', function() { + iframeDoc.body.className = editor.getBody().className; + }); + } + }, 50 ); + }); + } else { + this.setContent( body ); + } + }, + setError: function( message, dashicon ) { + this.setContent( + '
' + + '
' + + '

' + message + '

' + + '
' + ); + }, + rendered: function( value ) { + var notRendered; + + this.getNodes( function( editor, node ) { + if ( value != null ) { + $( node ).data( 'rendered', value === true ); + } else { + notRendered = notRendered || ! $( node ).data( 'rendered' ); + } + } ); + + return ! notRendered; + } } ); // take advantage of the Backbone extend method @@ -74,6 +279,29 @@ window.wp = window.wp || {}; * */ register: function( type, constructor ) { + var defaultConstructor = { + type: type, + View: {}, + toView: function( content ) { + var match = wp.shortcode.next( this.type, content ); + + if ( ! match ) { + return; + } + + return { + index: match.index, + content: match.content, + options: { + shortcode: match.shortcode + } + }; + } + }; + + constructor = _.defaults( constructor, defaultConstructor ); + constructor.View = wp.mce.View.extend( constructor.View ); + views[ type ] = constructor; }, @@ -81,6 +309,8 @@ window.wp = window.wp || {}; * wp.mce.views.get( id ) * * Returns a TinyMCE view constructor. + * + * @param type */ get: function( type ) { return views[ type ]; @@ -90,6 +320,8 @@ window.wp = window.wp || {}; * wp.mce.views.unregister( type ) * * Unregisters a TinyMCE view. + * + * @param type */ unregister: function( type ) { delete views[ type ]; @@ -112,6 +344,7 @@ window.wp = window.wp || {}; * matches with wrapper elements, and creates a new instance for * every match, which triggers the related data to be fetched. * + * @param content */ toViews: function( content ) { var pieces = [ { content: content } ], @@ -180,6 +413,7 @@ window.wp = window.wp || {}; if ( ! wp.mce.views.getInstance( encodedText ) ) { viewOptions = options; + viewOptions.type = viewType; viewOptions.encodedText = encodedText; instance = new view.View( viewOptions ); instances[ encodedText ] = instance; @@ -189,10 +423,9 @@ window.wp = window.wp || {}; tag: 'div', attrs: { - 'class': 'wpview-wrap wpview-type-' + viewType, + 'class': 'wpview-wrap', 'data-wpview-text': encodedText, - 'data-wpview-type': viewType, - 'contenteditable': 'false' + 'data-wpview-type': viewType }, content: '\u00a0' @@ -215,12 +448,13 @@ window.wp = window.wp || {}; if ( ! instance ) { result = view.toView( text ); viewOptions = result.options; + viewOptions.type = view.type; viewOptions.encodedText = encodedText; instance = new view.View( viewOptions ); instances[ encodedText ] = instance; } - wp.mce.views.render(); + instance.render(); }, getInstance: function( encodedText ) { @@ -236,9 +470,9 @@ window.wp = window.wp || {}; * To generate wrapper elements, pass your content through * `wp.mce.view.toViews( content )`. */ - render: function() { + render: function( force ) { _.each( instances, function( instance ) { - instance.render(); + instance.render( force ); } ); }, @@ -252,26 +486,9 @@ window.wp = window.wp || {}; } }; - 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'), + wp.mce.views.register( 'gallery', { + View: { + 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. @@ -285,8 +502,12 @@ window.wp = window.wp || {}; }, fetch: function() { + var self = this; + this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID ); - this.dfd = this.attachments.more().done( _.bind( this.render, this ) ); + this.dfd = this.attachments.more().done( function() { + self.render( true ); + } ); }, getHtml: function() { @@ -296,7 +517,7 @@ window.wp = window.wp || {}; // Don't render errors while still fetching attachments if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) { - return; + return ''; } if ( this.attachments.length ) { @@ -315,13 +536,12 @@ window.wp = window.wp || {}; options = { attachments: attachments, - columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3 + columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns }; return this.template( options ); - } - }), + }, edit: function( node ) { var gallery = wp.media.gallery, @@ -335,45 +555,122 @@ window.wp = window.wp || {}; var shortcode = gallery.shortcode( selection ).string(); $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); wp.mce.views.refreshView( self, shortcode ); + }); + + frame.on( 'close', function() { 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 + * These are base methods that are shared by the audio and video 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 ); + wp.mce.av = { + View: { + overlay: true, - if ( ! match ) { - return; - } + action: 'parse-media-shortcode', - return { - index: match.index, - content: match.content, - options: { - shortcode: match.shortcode + initialize: function( options ) { + var self = this; + + this.shortcode = options.shortcode; + + _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' ); + $( this ).on( 'ready', this.setNodes ); + + $( document ).on( 'media:edit', this.stopPlayers ); + + this.fetch(); + + this.getEditors( function( editor ) { + editor.on( 'hide', self.stopPlayers ); + }); + }, + + setNodes: function () { + if ( this.parsed ) { + this.setIframes( this.parsed.head, this.parsed.body ); + } else { + this.fail(); } - }; + }, + + fetch: function () { + var self = this; + + wp.ajax.send( this.action, { + data: { + post_ID: $( '#post_ID' ).val() || 0, + type: this.shortcode.tag, + shortcode: this.shortcode.string() + } + } ) + .done( function( response ) { + if ( response ) { + self.parsed = response; + self.setIframes( response.head, response.body ); + } else { + self.fail( true ); + } + } ) + .fail( function( response ) { + self.fail( response || true ); + } ); + }, + + fail: function( error ) { + if ( ! this.error ) { + if ( error ) { + this.error = error; + } else { + return; + } + } + + if ( this.error.message ) { + if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' || + this.error.type === 'no-items' ) { + + this.setError( this.error.message, 'admin-media' ); + } else { + this.setContent( '

' + this.original + '

', 'replace' ); + } + } else if ( this.error.statusText ) { + this.setError( this.error.statusText, 'admin-media' ); + } else if ( this.original ) { + this.setContent( '

' + this.original + '

', 'replace' ); + } + }, + + stopPlayers: function( remove ) { + var rem = remove === 'remove'; + + this.getNodes( function( editor, node, content ) { + var p, win, + iframe = $( 'iframe.wpview-sandbox', content ).get(0); + + if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) { + // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there. + try { + for ( p in win.mejs.players ) { + win.mejs.players[p].pause(); + + if ( rem ) { + win.mejs.players[p].remove(); + } + } + } catch( er ) {} + } + }); + }, + + unbind: function() { + this.stopPlayers( 'remove' ); + } }, /** @@ -387,11 +684,11 @@ window.wp = window.wp || {}; * @param {HTMLElement} node */ edit: function( node ) { - var media = wp.media[ this.shortcode ], + var media = wp.media[ this.type ], self = this, frame, data, callback; - wp.media.mixin.pauseAllPlayers(); + $( document ).trigger( 'media:edit' ); data = window.decodeURIComponent( $( node ).attr('data-wpview-text') ); frame = media.edit( data ); @@ -400,7 +697,7 @@ window.wp = window.wp || {}; } ); callback = function( selection ) { - var shortcode = wp.media[ self.shortcode ].shortcode( selection ).string(); + var shortcode = wp.media[ self.type ].shortcode( selection ).string(); $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); wp.mce.views.refreshView( self, shortcode ); frame.detach(); @@ -416,278 +713,113 @@ window.wp = window.wp || {}; } }; - /** - * 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'; - - 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( '

' + media.find( 'source' ).eq(0).prop( 'src' ) + '

' ); - 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 = 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(); - } - }); - _.extend( wp.mce.media.View.prototype, wp.media.mixin ); - /** * TinyMCE handler for the video shortcode * - * @mixes wp.mce.media + * @mixes wp.mce.av */ - 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 ); + wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, { + state: 'video-details' + } ) ); /** * TinyMCE handler for the audio shortcode * - * @mixes wp.mce.media + * @mixes wp.mce.av */ - 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 ); + wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, { + state: 'audio-details' + } ) ); /** - * Base View class for playlist shortcodes + * TinyMCE handler for the playlist shortcode * - * @constructor - * @augments wp.mce.View - * @mixes wp.media.mixin + * @mixes wp.mce.av */ - wp.mce.media.PlaylistView = wp.mce.View.extend({ - className: 'editor-playlist', - template: media.template('editor-playlist'), - - initialize: function( options ) { - this.players = []; - this.data = {}; - this.attachments = []; - this.shortcode = options.shortcode; - this.fetch(); - }, - - /** - * 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 ) ); - }, - - /** - * 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 - */ - render: function() { - var html = this.getHtml(), self = this; - - _.each( tinymce.editors, function( editor ) { - var doc; - if ( editor.plugins.wpview ) { - doc = editor.getDoc(); - $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).each(function (i, elem) { - var node = $( elem ); - - // The 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( '' ); + wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, { + state: [ 'playlist-edit', 'video-playlist-edit' ] + } ) ); - if ( ! self.data.tracks ) { - return; - } + /** + * TinyMCE handler for the embed shortcode + */ + wp.mce.embedMixin = { + View: _.extend( {}, wp.mce.av.View, { + overlay: true, + action: 'parse-embed', + initialize: function( options ) { + this.content = options.content; + this.original = options.url || options.shortcode.string(); - self.players.push( new WPPlaylistView({ - el: $( elem ).find( '.wp-playlist' ).get(0), - metadata: self.data - }).player ); - }); + if ( options.url ) { + this.shortcode = media.embed.shortcode( { + url: options.url + } ); + } else { + this.shortcode = options.shortcode; } - }, this ); - }, - - /** - * 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 - }; + _.bindAll( this, 'setIframes', 'setNodes', 'fetch' ); + $( this ).on( 'ready', this.setNodes ); - if ( ! this.attachments.length ) { - return this.template( options ); + this.fetch(); } + } ), + edit: function( node ) { + var embed = media.embed, + self = this, + frame, + data, + isURL = 'embedURL' === this.type; - attachments = this.attachments.toJSON(); + $( document ).trigger( 'media:edit' ); - _.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 - }; + data = window.decodeURIComponent( $( node ).attr('data-wpview-text') ); + frame = embed.edit( data, isURL ); + frame.on( 'close', function() { + frame.detach(); + } ); + frame.state( 'embed' ).props.on( 'change:url', function (model, url) { + if ( ! url ) { + return; + } + frame.state( 'embed' ).metadata = model.toJSON(); + } ); + frame.state( 'embed' ).on( 'select', function() { + var shortcode; - 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 - }; + if ( isURL ) { + shortcode = frame.state( 'embed' ).metadata.url; } else { - options.width = 400; + shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string(); } - - track.image = attachment.image; - track.thumb = attachment.thumb; - - tracks.push( track ); + $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); + wp.mce.views.refreshView( self, shortcode ); + frame.detach(); } ); + frame.open(); + } + }; - options.tracks = tracks; - this.data = options; + wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) ); - return this.template( options ); - }, + wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, { + toView: function( content ) { + var re = /(?:^|

)(https?:\/\/[^\s"]+?)(?:<\/p>\s*|$)/gi, + match = re.exec( tinymce.trim( content ) ); - unbind: function() { - this.unsetPlayers(); + if ( ! match ) { + return; + } + + return { + index: match.index, + content: match[0], + options: { + url: match[1] + } + }; } - }); - _.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));