Ticket #29841: 29841.4.patch
File 29841.4.patch, 36.0 KB (added by , 10 years ago) |
---|
-
src/wp-includes/js/mce-view.js
1 1 /* global tinymce */ 2 /** 2 3 /* 4 * The TinyMCE view API. 5 * 3 6 * Note: this API is "experimental" meaning that it will probably change 4 7 * in the next few releases based on feedback from 3.9.0. 5 8 * If you decide to use it, please follow the development closely. 6 9 */ 7 8 // Ensure the global `wp` object exists.9 window.wp = window.wp || {};10 11 10 ( function( $ ) { 12 11 'use strict'; 13 12 14 13 var views = {}, 15 14 instances = {}, 16 media = wp.media, 17 mediaWindows = [], 18 windowIdx = 0, 19 waitInterval = 50, 20 viewOptions = ['encodedText']; 15 viewOptions = [ 'encodedText' ]; 21 16 22 // Create the `wp.mce` object if necessary.17 window.wp = window.wp || {}; 23 18 wp.mce = wp.mce || {}; 24 19 25 20 /** 26 * wp.mce.View27 *28 * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is29 * that the TinyMCE View is not tied to a particular DOM node.30 *31 * @param {Object} [options={}]32 */33 wp.mce.View = function( options ) {34 options = options || {};35 this.type = options.type;36 _.extend( this, _.pick( options, viewOptions ) );37 this.initialize.apply( this, arguments );38 };39 40 _.extend( wp.mce.View.prototype, {41 initialize: function() {},42 getHtml: function() {43 return '';44 },45 loadingPlaceholder: function() {46 return '' +47 '<div class="loading-placeholder">' +48 '<div class="dashicons dashicons-admin-media"></div>' +49 '<div class="wpview-loading"><ins></ins></div>' +50 '</div>';51 },52 render: function( force ) {53 if ( force || ! this.rendered() ) {54 this.unbind();55 56 this.setContent(57 '<p class="wpview-selection-before">\u00a0</p>' +58 '<div class="wpview-body" contenteditable="false">' +59 '<div class="toolbar mce-arrow-down">' +60 ( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +61 '<div class="dashicons dashicons-no remove"></div>' +62 '</div>' +63 '<div class="wpview-content wpview-type-' + this.type + '">' +64 ( this.getHtml() || this.loadingPlaceholder() ) +65 '</div>' +66 ( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) +67 '</div>' +68 '<p class="wpview-selection-after">\u00a0</p>',69 'wrap'70 );71 72 $( this ).trigger( 'ready' );73 74 this.rendered( true );75 }76 },77 unbind: function() {},78 getEditors: function( callback ) {79 var editors = [];80 81 _.each( tinymce.editors, function( editor ) {82 if ( editor.plugins.wpview ) {83 if ( callback ) {84 callback( editor );85 }86 87 editors.push( editor );88 }89 }, this );90 91 return editors;92 },93 getNodes: function( callback ) {94 var nodes = [],95 self = this;96 97 this.getEditors( function( editor ) {98 $( editor.getBody() )99 .find( '[data-wpview-text="' + self.encodedText + '"]' )100 .each( function ( i, node ) {101 if ( callback ) {102 callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );103 }104 105 nodes.push( node );106 } );107 } );108 109 return nodes;110 },111 setContent: function( html, option ) {112 this.getNodes( function ( editor, node, content ) {113 var el = ( option === 'wrap' || option === 'replace' ) ? node : content,114 insert = html;115 116 if ( _.isString( insert ) ) {117 insert = editor.dom.createFragment( insert );118 }119 120 if ( option === 'replace' ) {121 editor.dom.replace( insert, el );122 } else {123 el.innerHTML = '';124 el.appendChild( insert );125 }126 } );127 },128 /* jshint scripturl: true */129 setIframes: function ( head, body ) {130 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,131 importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';132 133 if ( head || body.indexOf( '<script' ) !== -1 ) {134 this.getNodes( function ( editor, node, content ) {135 var dom = editor.dom,136 styles = '',137 bodyClasses = editor.getBody().className || '',138 iframe, iframeDoc, i, resize;139 140 content.innerHTML = '';141 head = head || '';142 143 if ( importStyles ) {144 if ( ! wp.mce.views.sandboxStyles ) {145 tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {146 if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&147 link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {148 149 styles += dom.getOuterHTML( link ) + '\n';150 }151 });152 153 wp.mce.views.sandboxStyles = styles;154 } else {155 styles = wp.mce.views.sandboxStyles;156 }157 }158 159 // Seems Firefox needs a bit of time to insert/set the view nodes, or the iframe will fail160 // especially when switching Text => Visual.161 setTimeout( function() {162 iframe = dom.add( content, 'iframe', {163 src: tinymce.Env.ie ? 'javascript:""' : '',164 frameBorder: '0',165 allowTransparency: 'true',166 scrolling: 'no',167 'class': 'wpview-sandbox',168 style: {169 width: '100%',170 display: 'block'171 }172 } );173 174 iframeDoc = iframe.contentWindow.document;175 176 iframeDoc.open();177 iframeDoc.write(178 '<!DOCTYPE html>' +179 '<html>' +180 '<head>' +181 '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +182 head +183 styles +184 '<style>' +185 'html {' +186 'background: transparent;' +187 'padding: 0;' +188 'margin: 0;' +189 '}' +190 'body#wpview-iframe-sandbox {' +191 'background: transparent;' +192 'padding: 1px 0 !important;' +193 'margin: -1px 0 0 !important;' +194 '}' +195 'body#wpview-iframe-sandbox:before,' +196 'body#wpview-iframe-sandbox:after {' +197 'display: none;' +198 'content: "";' +199 '}' +200 '</style>' +201 '</head>' +202 '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +203 body +204 '</body>' +205 '</html>'206 );207 iframeDoc.close();208 209 resize = function() {210 var $iframe, iframeDocHeight;211 212 // Make sure the iframe still exists.213 if ( iframe.contentWindow ) {214 $iframe = $( iframe );215 iframeDocHeight = $( iframeDoc.body ).height();216 217 if ( $iframe.height() !== iframeDocHeight ) {218 $iframe.height( iframeDocHeight );219 editor.nodeChanged();220 }221 }222 };223 224 if ( MutationObserver ) {225 new MutationObserver( _.debounce( function() {226 resize();227 }, 100 ) )228 .observe( iframeDoc.body, {229 attributes: true,230 childList: true,231 subtree: true232 } );233 } else {234 for ( i = 1; i < 6; i++ ) {235 setTimeout( resize, i * 700 );236 }237 }238 239 if ( importStyles ) {240 editor.on( 'wp-body-class-change', function() {241 iframeDoc.body.className = editor.getBody().className;242 });243 }244 }, waitInterval );245 });246 } else {247 this.setContent( body );248 }249 },250 setError: function( message, dashicon ) {251 this.setContent(252 '<div class="wpview-error">' +253 '<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +254 '<p>' + message + '</p>' +255 '</div>'256 );257 },258 rendered: function( value ) {259 var notRendered;260 261 this.getNodes( function( editor, node ) {262 if ( value != null ) {263 $( node ).data( 'rendered', value === true );264 } else {265 notRendered = notRendered || ! $( node ).data( 'rendered' );266 }267 } );268 269 return ! notRendered;270 }271 } );272 273 // take advantage of the Backbone extend method274 wp.mce.View.extend = Backbone.View.extend;275 276 /**277 21 * wp.mce.views 278 22 * 279 23 * A set of utilities that simplifies adding custom UI within a TinyMCE editor. … … 283 27 wp.mce.views = { 284 28 285 29 /** 286 * wp.mce.views.register( type, view )287 *288 30 * Registers a new TinyMCE view. 289 31 * 290 * @param type 291 * @param constructor 292 * 32 * @param {String} The view type. 33 * @param {Object} A constructor. 293 34 */ 294 35 register: function( type, constructor ) { 295 36 var defaultConstructor = { … … 319 60 }, 320 61 321 62 /** 322 * wp.mce.views.get( id ) 323 * 324 * Returns a TinyMCE view constructor. 63 * Unregisters a TinyMCE view. 325 64 * 326 * @param type65 * @param {String} The view type. 327 66 */ 328 get: function( type ) {329 returnviews[ type ];67 unregister: function( type ) { 68 delete views[ type ]; 330 69 }, 331 70 332 71 /** 333 * wp.mce.views.unregister( type ) 334 * 335 * Unregisters a TinyMCE view. 72 * Returns a TinyMCE view constructor. 336 73 * 337 * @param type74 * @param {String} The view type. 338 75 */ 339 unregister: function( type ) {340 deleteviews[ type ];76 get: function( type ) { 77 return views[ type ]; 341 78 }, 342 79 343 80 /** 344 * wp.mce.views.unbind( editor )345 *346 81 * The editor DOM is being rebuilt, run cleanup. 347 82 */ 348 83 unbind: function() { … … 352 87 }, 353 88 354 89 /** 355 * toViews( content )356 * Scans a `content` string for each view's pattern, replacing any357 * matches with wrapper elements, and creates a new instance for358 * every match,which triggers the related data to be fetched.90 * Scans a given string for each view's pattern, 91 * replacing any matches with markers, 92 * and creates a new instance for every match, 93 * which triggers the related data to be fetched. 359 94 * 360 * @param content95 * @param {String} String to scan. 361 96 */ 362 97 toViews: function( content ) { 363 98 var pieces = [ { content: content } ], 364 99 current; 365 100 366 _.each( views, function( view, viewType ) {101 _.each( views, function( view, type ) { 367 102 current = pieces.slice(); 368 103 pieces = []; 369 104 … … 379 114 380 115 // Iterate through the string progressively matching views 381 116 // and slicing the string as we go. 382 while ( remaining && ( result = view.toView( remaining )) ) {117 while ( remaining && ( result = view.toView( remaining ) ) ) { 383 118 // Any text before the match becomes an unprocessed piece. 384 119 if ( result.index ) { 385 pieces.push( { content: remaining.substring( 0, result.index ) });120 pieces.push( { content: remaining.substring( 0, result.index ) } ); 386 121 } 387 122 388 123 // Add the processed piece for the match. 389 pieces.push( {390 content: wp.mce.views.toView( viewType, result.content, result.options ),124 pieces.push( { 125 content: wp.mce.views.toView( type, result.content, result.options ), 391 126 processed: true 392 } );127 } ); 393 128 394 129 // Update the remaining content. 395 130 remaining = remaining.slice( result.index + result.content.length ); … … 398 133 // There are no additional matches. If any content remains, 399 134 // add it as an unprocessed piece. 400 135 if ( remaining ) { 401 pieces.push( { content: remaining });136 pieces.push( { content: remaining } ); 402 137 } 403 } );404 } );138 } ); 139 } ); 405 140 406 return _.pluck( pieces, 'content' ).join( '');141 return _.pluck( pieces, 'content' ).join( '' ); 407 142 }, 408 143 409 144 /** 410 * Create a placeholder for a particular view type 411 * 412 * @param viewType 413 * @param text 414 * @param options 145 * Returns the marked text for a particular view type, 146 * and creates a new instance for that view type if it does not exist. 147 * The text will be returned unmarked if no view of that type is registered. 148 * 149 * @param {String} The view type. 150 * @param {String} The textual representation of the view. 151 * @param {Object} Options. 415 152 * 153 * @return {String} The markered text. 416 154 */ 417 toView: function( viewType, text, options ) { 418 var view = wp.mce.views.get( viewType ), 419 encodedText = window.encodeURIComponent( text ), 420 instance, viewOptions; 421 155 toView: function( type, text, options ) { 156 var view = this.get( type ), 157 encodedText = window.encodeURIComponent( text ); 422 158 423 159 if ( ! view ) { 424 160 return text; 425 161 } 426 162 427 if ( ! wp.mce.views.getInstance( encodedText ) ) { 428 viewOptions = options; 429 viewOptions.type = viewType; 430 viewOptions.encodedText = encodedText; 431 instance = new view.View( viewOptions ); 432 instances[ encodedText ] = instance; 163 if ( ! this.getInstance( encodedText ) ) { 164 instances[ encodedText ] = new view.View( _.extend( options, { 165 encodedText: encodedText, 166 type: type 167 } ) ); 433 168 } 434 169 435 return wp.html.string({ 436 tag: 'div', 437 438 attrs: { 439 'class': 'wpview-wrap', 440 'data-wpview-text': encodedText, 441 'data-wpview-type': viewType 442 }, 443 444 content: '\u00a0' 445 }); 170 return '<p data-wpview-marker="' + encodedText + '">' + text + '</p>'; 446 171 }, 447 172 448 173 /** 449 * Refresh views after an update is made 174 * Refresh views after an update is made. 450 175 * 451 * @param view {object} being refreshed452 * @param text {string} textual representation of the view453 * @param force {Boolean} whether to force rendering176 * @param {Object} The view to refresh. 177 * @param {String} The textual representation of the view. 178 * @param {Boolean} Whether or not to force rendering. 454 179 */ 455 180 refreshView: function( view, text, force ) { 456 var encodedText = window.encodeURIComponent( text ), 457 viewOptions, 458 result, instance; 459 460 instance = wp.mce.views.getInstance( encodedText ); 461 462 if ( ! instance ) { 463 result = view.toView( text ); 464 viewOptions = result.options; 465 viewOptions.type = view.type; 466 viewOptions.encodedText = encodedText; 467 instance = new view.View( viewOptions ); 468 instances[ encodedText ] = instance; 181 var encodedText = encodeURIComponent( text ); 182 183 if ( ! this.getInstance( encodedText ) ) { 184 instances[ encodedText ] = new view.View( _.extend( view.toView( text ).options, { 185 encodedText: encodedText, 186 type: view.type 187 } ) ); 469 188 } 470 189 471 instance .render( force );190 instances[ encodedText ].render( force ); 472 191 }, 473 192 193 /** 194 * @param {String} The textual representation of the view, encoded. 195 */ 474 196 getInstance: function( encodedText ) { 475 197 return instances[ encodedText ]; 476 198 }, 477 199 200 getInstances: function() { 201 return instances; 202 }, 203 478 204 /** 479 * render( scope )205 * Renders any view instances. 480 206 * 481 * Renders any view instances inside a DOM node `scope`. 207 * View instances are detected by the presence of markers. 208 * To generate markers, pass your content through `toViews`. 482 209 * 483 * View instances are detected by the presence of wrapper elements. 484 * To generate wrapper elements, pass your content through 485 * `wp.mce.view.toViews( content )`. 210 * @param {Boolean} Whether or not to force rendering. 486 211 */ 487 212 render: function( force ) { 488 213 _.each( instances, function( instance ) { … … 490 215 } ); 491 216 }, 492 217 218 /** 219 * @param {HTMLElement} A view node. 220 */ 493 221 edit: function( node ) { 494 var viewType = $( node ).data('wpview-type'),495 view = wp.mce.views.get( viewType );222 var type = $( node ).data( 'wpview-type' ), 223 view = this.get( type ); 496 224 497 225 if ( view ) { 498 226 view.edit( node ); … … 500 228 } 501 229 }; 502 230 503 wp.mce.views.register( 'gallery', { 231 /** 232 * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is 233 * that the TinyMCE View is not tied to a particular DOM node. 234 * 235 * @param {Object} Options. 236 */ 237 wp.mce.View = function( options ) { 238 options = options || {}; 239 this.type = options.type; 240 _.extend( this, _.pick( options, viewOptions ) ); 241 this.initialize.apply( this, arguments ); 242 }; 243 244 _.extend( wp.mce.View.prototype, { 245 246 /** 247 * Whether or not to display a loader. 248 * 249 * @type {Boolean} 250 */ 251 loader: true, 252 253 /** 254 * Runs after the view instance is created. 255 */ 256 initialize: function() {}, 257 258 /** 259 * Retuns the HTML string to render in the view. 260 * 261 * @return {*} 262 */ 263 getHtml: function() {}, 264 265 /** 266 * Retuns the HTML string to render in the view. 267 * 268 * @param {Boolean} Whether or not to rerender. 269 */ 270 render: function( rerender ) { 271 var i = 0; 272 273 // If there's nothing to render an no loader needs to be shown, stop. 274 if ( ! this.loader && ! this.getHtml() ) { 275 return; 276 } 277 278 // We're about to rerender all views of this instance, so unbind rendered views. 279 rerender && this.unbind(); 280 281 // Replace any left over markers. 282 this.replaceMarkers(); 283 284 if ( this.getHtml() ) { 285 this.setContent( this.getHtml(), function( editor, node ) { 286 $( node ).data( 'rendered', true ); 287 this.bindNodes.apply( this, arguments ); 288 i++; 289 }, rerender ? null : false ); 290 291 // Use `bindNodes` if possible. 292 i && $( this ).trigger( 'ready' ); 293 } else { 294 this.setLoader(); 295 } 296 }, 297 298 /** 299 * Runs after a view is added to the DOM. 300 * 301 * @param {tinymce.Editor} The editor instance the view node is in. 302 * @param {HTMLElement} The view node. 303 * @param {HTMLElement} The view's content node. 304 */ 305 bindNodes: function() {}, 306 307 /** 308 * Runs before a view is removed from the DOM. 309 */ 310 unbind: function() { 311 this.getNodes( function() { 312 this.unbindNodes.apply( this, arguments ); 313 }, true ); 314 }, 315 316 /** 317 * Runs before a view is removed from the DOM. 318 * 319 * @param {tinymce.Editor} The editor instance the view node is in. 320 * @param {HTMLElement} The view node. 321 * @param {HTMLElement} The view's content node. 322 */ 323 unbindNodes: function() {}, 324 325 /** 326 * Gets all the TinyMCE editors that support views. 327 * 328 * @param {Function} A callback (optional). 329 * 330 * @return {tinymce.Editor[]} An array of TinyMCE editor instances. 331 */ 332 getEditors: function( callback ) { 333 var editors = []; 334 335 _.each( tinymce.editors, function( editor ) { 336 if ( editor.plugins.wpview ) { 337 editors.push( editor ); 338 callback && callback.call( this, editor ); 339 } 340 }, this ); 341 342 return editors; 343 }, 344 345 /** 346 * Gets all the view nodes in each editor. 347 * 348 * @param {Function} A callback (optional). 349 * @param {Boolean} Get (un)rendered nodes (optional). 350 * 351 * @return {HTMLElement[]} An array of view nodes. 352 */ 353 getNodes: function( callback, rendered ) { 354 var nodes = []; 355 356 this.getEditors( function( editor ) { 357 var self = this; 358 359 $( editor.getBody() ) 360 .find( '[data-wpview-text="' + self.encodedText + '"]' ) 361 .filter( function() { 362 var data; 363 364 if ( rendered == null ) { 365 return true; 366 } 367 368 data = $( this ).data( 'rendered' ) === true; 369 370 return rendered ? data : ! data; 371 } ) 372 .each( function ( i, node ) { 373 nodes.push( node ); 374 callback && callback.call( self, editor, node, $( node ).find( '.wpview-content' ).get( 0 ) ); 375 } ); 376 } ); 377 378 return nodes; 379 }, 380 381 /** 382 * Gets all the markers in each editor. 383 * 384 * @param {Function} A callback (optional). 385 * 386 * @return {HTMLElement[]} An array of marker nodes. 387 */ 388 getMarkers: function( callback ) { 389 var markers = []; 390 391 this.getEditors( function( editor ) { 392 var self = this; 393 394 $( editor.getBody() ) 395 .find( '[data-wpview-marker="' + this.encodedText + '"]' ) 396 .each( function( i, node ) { 397 markers.push( node ); 398 callback && callback.call( self, editor, node ); 399 } ); 400 } ); 401 402 return markers; 403 }, 404 405 /** 406 * Replaces all markers with view nodes. 407 */ 408 replaceMarkers: function() { 409 this.getMarkers( function( editor, node ) { 410 if ( $( node ).text() !== decodeURIComponent( this.encodedText ) ) { 411 editor.dom.setAttrib( node, 'data-wpview-marker', null ); 412 return; 413 } 414 415 editor.dom.replace( 416 editor.dom.createFragment( 417 '<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' + 418 '<p class="wpview-selection-before">\u00a0</p>' + 419 '<div class="wpview-body" contenteditable="false">' + 420 '<div class="toolbar mce-arrow-down">' + 421 ( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) + 422 '<div class="dashicons dashicons-no remove"></div>' + 423 '</div>' + 424 '<div class="wpview-content wpview-type-' + this.type + '"></div>' + 425 ( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) + 426 '</div>' + 427 '<p class="wpview-selection-after">\u00a0</p>' + 428 '</div>' 429 ), 430 node 431 ); 432 } ); 433 }, 434 435 /** 436 * Removes all markers. 437 */ 438 removeMarkers: function() { 439 this.getMarkers( function( editor, node ) { 440 editor.dom.setAttrib( node, 'data-wpview-marker', null ); 441 } ); 442 }, 443 444 /** 445 * Gets all the markers in each editor. 446 * 447 * @param {(String|HTMLElement)} The content to set. 448 * @param {Function} A callback (optional). 449 * @param {Boolean} Only set for (un)rendered nodes (optional). 450 */ 451 setContent: function( content, callback, rendered ) { 452 if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) { 453 this.setIframes( content.head, content.body, callback, rendered ); 454 } else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) { 455 this.setIframes( null, content, callback, rendered ); 456 } else { 457 this.getNodes( function( editor, node, contentNode ) { 458 content = content.body || content; 459 460 contentNode.innerHTML = ''; 461 contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content ); 462 463 callback && callback.apply( this, arguments ); 464 }, rendered ); 465 } 466 }, 467 468 /** 469 * Set an iframe as the view content. 470 */ 471 setIframes: function( head, body, callback, rendered ) { 472 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, 473 importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist'; 474 475 this.getNodes( function( editor, node, content ) { 476 var dom = editor.dom, 477 styles = '', 478 bodyClasses = editor.getBody().className || '', 479 iframe, iframeDoc, i, resize; 480 481 content.innerHTML = ''; 482 head = head || ''; 483 484 if ( importStyles ) { 485 if ( ! wp.mce.views.sandboxStyles ) { 486 tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) { 487 if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 && 488 link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) { 489 490 styles += dom.getOuterHTML( link ) + '\n'; 491 } 492 }); 493 494 wp.mce.views.sandboxStyles = styles; 495 } else { 496 styles = wp.mce.views.sandboxStyles; 497 } 498 } 499 500 // Seems Firefox needs a bit of time to insert/set the view nodes, 501 // or the iframe will fail especially when switching Text => Visual. 502 setTimeout( function() { 503 iframe = dom.add( content, 'iframe', { 504 /* jshint scripturl: true */ 505 src: tinymce.Env.ie ? 'javascript:""' : '', 506 frameBorder: '0', 507 allowTransparency: 'true', 508 scrolling: 'no', 509 'class': 'wpview-sandbox', 510 style: { 511 width: '100%', 512 display: 'block' 513 } 514 } ); 515 516 iframeDoc = iframe.contentWindow.document; 517 518 iframeDoc.open(); 519 520 iframeDoc.write( 521 '<!DOCTYPE html>' + 522 '<html>' + 523 '<head>' + 524 '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' + 525 head + 526 styles + 527 '<style>' + 528 'html {' + 529 'background: transparent;' + 530 'padding: 0;' + 531 'margin: 0;' + 532 '}' + 533 'body#wpview-iframe-sandbox {' + 534 'background: transparent;' + 535 'padding: 1px 0 !important;' + 536 'margin: -1px 0 0 !important;' + 537 '}' + 538 'body#wpview-iframe-sandbox:before,' + 539 'body#wpview-iframe-sandbox:after {' + 540 'display: none;' + 541 'content: "";' + 542 '}' + 543 '</style>' + 544 '</head>' + 545 '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' + 546 body + 547 '</body>' + 548 '</html>' 549 ); 550 551 iframeDoc.close(); 552 553 resize = function() { 554 var $iframe, iframeDocHeight; 555 556 // Make sure the iframe still exists. 557 if ( iframe.contentWindow ) { 558 $iframe = $( iframe ); 559 iframeDocHeight = $( iframeDoc.body ).height(); 560 561 if ( $iframe.height() !== iframeDocHeight ) { 562 $iframe.height( iframeDocHeight ); 563 editor.nodeChanged(); 564 } 565 } 566 }; 567 568 if ( MutationObserver ) { 569 new MutationObserver( _.debounce( function() { 570 resize(); 571 }, 100 ) ) 572 .observe( iframeDoc.body, { 573 attributes: true, 574 childList: true, 575 subtree: true 576 } ); 577 } else { 578 for ( i = 1; i < 6; i++ ) { 579 setTimeout( resize, i * 700 ); 580 } 581 } 582 583 if ( importStyles ) { 584 editor.on( 'wp-body-class-change', function() { 585 iframeDoc.body.className = editor.getBody().className; 586 } ); 587 } 588 }, 50 ); 589 } ); 590 }, 591 592 /** 593 * Set a loader. 594 */ 595 setLoader: function() { 596 this.setContent( 597 '<div class="loading-placeholder">' + 598 '<div class="dashicons dashicons-admin-media"></div>' + 599 '<div class="wpview-loading"><ins></ins></div>' + 600 '</div>' 601 ); 602 }, 603 604 /** 605 * Set an error. 606 * 607 * @param {String} The error message to set. 608 * @param {String} A dashicon ID (optional). {@link https://developer.wordpress.org/resource/dashicons/} 609 */ 610 setError: function( message, dashicon ) { 611 this.setContent( 612 '<div class="wpview-error">' + 613 '<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' + 614 '<p>' + message + '</p>' + 615 '</div>' 616 ); 617 } 618 } ); 619 620 // Take advantage of the Backbone extend method. 621 wp.mce.View.extend = Backbone.View.extend; 622 } )( jQuery ); 623 624 /* 625 * The WordPress core TinyMCE views. 626 * Views for the gallery, audio, video, playlist and embed shortcodes, 627 * and a view for embeddable URLs. 628 */ 629 ( function( $, views ) { 630 views.register( 'gallery', { 504 631 View: { 505 template: media.template( 'editor-gallery' ),506 632 507 // The fallback post ID to use as a parent for galleries that don't 508 // specify the `ids` or `include` parameters. 509 // 510 // Uses the hidden input on the edit posts page by default. 511 postID: $('#post_ID').val(), 633 template: wp.media.template( 'editor-gallery' ), 634 635 postID: $( '#post_ID' ).val(), 512 636 513 637 initialize: function( options ) { 514 638 this.shortcode = options.shortcode; … … 520 644 521 645 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID ); 522 646 this.dfd = this.attachments.more().done( function() { 523 self.render( true);647 self.render(); 524 648 } ); 525 649 }, 526 650 … … 564 688 self = this, 565 689 frame, data; 566 690 567 data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );691 data = decodeURIComponent( $( node ).attr( 'data-wpview-text' ) ); 568 692 frame = gallery.edit( data ); 569 693 570 frame.state( 'gallery-edit').on( 'update', function( selection ) {694 frame.state( 'gallery-edit' ).on( 'update', function( selection ) { 571 695 var shortcode = gallery.shortcode( selection ).string(); 572 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );696 $( node ).attr( 'data-wpview-text', encodeURIComponent( shortcode ) ); 573 697 wp.mce.views.refreshView( self, shortcode, true ); 574 } );698 } ); 575 699 576 700 frame.on( 'close', function() { 577 701 frame.detach(); 578 } );702 } ); 579 703 } 580 704 } ); 581 705 … … 593 717 initialize: function( options ) { 594 718 var self = this; 595 719 596 this.shortcode = options.shortcode; 597 598 _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' ); 599 $( this ).on( 'ready', this.setNodes ); 600 601 $( document ).on( 'media:edit', this.stopPlayers ); 720 if ( options.url ) { 721 this.loader = false; 722 this.shortcode = wp.media.embed.shortcode( { 723 url: options.url 724 } ); 725 } else { 726 this.shortcode = options.shortcode; 727 } 602 728 603 729 this.fetch(); 604 730 605 731 this.getEditors( function( editor ) { 606 editor.on( 'hide', function () { 607 mediaWindows = []; 608 windowIdx = 0; 732 editor.on( 'wpview-selected', function() { 609 733 self.stopPlayers(); 610 734 } ); 611 });612 },613 614 pauseOtherWindows: function ( win ) {615 _.each( mediaWindows, function ( mediaWindow ) {616 if ( mediaWindow.sandboxId !== win.sandboxId ) {617 _.each( mediaWindow.mejs.players, function ( player ) {618 player.pause();619 } );620 }621 735 } ); 622 736 }, 623 737 624 iframeLoaded: function (win) {625 return _.bind( function () {626 var callback;627 if ( ! win.mejs || _.isEmpty( win.mejs.players ) ) {628 return;629 }630 631 win.sandboxId = windowIdx;632 windowIdx++;633 mediaWindows.push( win );634 635 callback = _.bind( function () {636 this.pauseOtherWindows( win );637 }, this );638 639 if ( ! _.isEmpty( win.mejs.MediaPluginBridge.pluginMediaElements ) ) {640 _.each( win.mejs.MediaPluginBridge.pluginMediaElements, function ( mediaElement ) {641 mediaElement.addEventListener( 'play', callback );642 } );643 }644 645 _.each( win.mejs.players, function ( player ) {646 $( player.node ).on( 'play', callback );647 }, this );648 }, this );649 },650 651 listenToSandboxes: function () {652 _.each( this.getNodes(), function ( node ) {653 var win, iframe = $( '.wpview-sandbox', node ).get( 0 );654 if ( iframe && ( win = iframe.contentWindow ) ) {655 $( win ).load( _.bind( this.iframeLoaded( win ), this ) );656 }657 }, this );658 },659 660 deferredListen: function () {661 window.setTimeout( _.bind( this.listenToSandboxes, this ), this.getNodes().length * waitInterval );662 },663 664 setNodes: function () {665 if ( this.parsed ) {666 this.setIframes( this.parsed.head, this.parsed.body );667 this.deferredListen();668 } else {669 this.fail();670 }671 },672 673 738 fetch: function () { 674 739 var self = this; 675 740 … … 681 746 } 682 747 } ) 683 748 .done( function( response ) { 684 if ( response ) { 685 self.parsed = response; 686 self.setIframes( response.head, response.body ); 687 self.deferredListen(); 688 } else { 689 self.fail( true ); 690 } 749 self.parsed = response; 750 self.render(); 691 751 } ) 692 752 .fail( function( response ) { 693 self.fail( response || true ); 694 } ); 695 }, 696 697 fail: function( error ) { 698 if ( ! this.error ) { 699 if ( error ) { 700 this.error = error; 701 } else { 702 return; 703 } 704 } 705 706 if ( this.error.message ) { 707 if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' || 708 this.error.type === 'no-items' ) { 709 710 this.setError( this.error.message, 'admin-media' ); 753 if ( self.type === 'embedURL' ) { 754 self.removeMarkers(); 711 755 } else { 712 this.setContent( '<p>' + this.original + '</p>', 'replace' );756 self.setError( response.message || response.statusText, 'admin-media' ); 713 757 } 714 } else if ( this.error.statusText ) { 715 this.setError( this.error.statusText, 'admin-media' ); 716 } else if ( this.original ) { 717 this.setContent( '<p>' + this.original + '</p>', 'replace' ); 718 } 758 } ); 719 759 }, 720 760 721 761 stopPlayers: function( remove ) { 722 var rem = remove === 'remove';723 724 762 this.getNodes( function( editor, node, content ) { 725 var p, win, 726 iframe = $( 'iframe.wpview-sandbox', content ).get(0); 763 var win = $( 'iframe.wpview-sandbox', content ).get( 0 ); 727 764 728 if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) { 729 // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there. 730 try { 731 for ( p in win.mejs.players ) { 732 win.mejs.players[p].pause(); 733 734 if ( rem ) { 735 win.mejs.players[p].remove(); 736 } 737 } 738 } catch( er ) {} 765 if ( win && ( win = win.contentWindow ) && win.mejs ) { 766 _.each( win.mejs.players, function( player ) { 767 try { player[ remove ? 'remove' : 'pause' ]() } catch ( e ) {} 768 } ); 739 769 } 740 } );770 } ); 741 771 }, 742 772 743 unbind: function() {744 this.stopPlayers( 'remove' );773 getHtml: function() { 774 return this.parsed; 745 775 } 746 776 }, 747 777 … … 760 790 self = this, 761 791 frame, data, callback; 762 792 763 $( document ).trigger( 'media:edit' ); 793 _.each( views.getInstances(), function( instance ) { 794 instance.stopPlayers && instance.stopPlayers(); 795 } ); 764 796 765 data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );797 data = decodeURIComponent( $( node ).attr('data-wpview-text') ); 766 798 frame = media.edit( data ); 799 767 800 frame.on( 'close', function() { 768 801 frame.detach(); 769 802 } ); 770 803 771 804 callback = function( selection ) { 772 805 var shortcode = wp.media[ self.type ].shortcode( selection ).string(); 773 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );806 $( node ).attr( 'data-wpview-text', encodeURIComponent( shortcode ) ); 774 807 wp.mce.views.refreshView( self, shortcode ); 775 808 frame.detach(); 776 809 }; 810 777 811 if ( _.isArray( self.state ) ) { 778 812 _.each( self.state, function (state) { 779 813 frame.state( state ).on( 'update', callback ); … … 781 815 } else { 782 816 frame.state( self.state ).on( 'update', callback ); 783 817 } 818 784 819 frame.open(); 785 820 } 786 821 }; … … 790 825 * 791 826 * @mixes wp.mce.av 792 827 */ 793 wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {828 views.register( 'video', _.extend( {}, wp.mce.av, { 794 829 state: 'video-details' 795 830 } ) ); 796 831 … … 799 834 * 800 835 * @mixes wp.mce.av 801 836 */ 802 wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {837 views.register( 'audio', _.extend( {}, wp.mce.av, { 803 838 state: 'audio-details' 804 839 } ) ); 805 840 … … 808 843 * 809 844 * @mixes wp.mce.av 810 845 */ 811 wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {846 views.register( 'playlist', _.extend( {}, wp.mce.av, { 812 847 state: [ 'playlist-edit', 'video-playlist-edit' ] 813 848 } ) ); 814 849 … … 818 853 wp.mce.embedMixin = { 819 854 View: _.extend( {}, wp.mce.av.View, { 820 855 overlay: true, 821 action: 'parse-embed', 822 initialize: function( options ) { 823 this.content = options.content; 824 this.original = options.url || options.shortcode.string(); 825 826 if ( options.url ) { 827 this.shortcode = media.embed.shortcode( { 828 url: options.url 829 } ); 830 } else { 831 this.shortcode = options.shortcode; 832 } 833 834 _.bindAll( this, 'setIframes', 'setNodes', 'fetch' ); 835 $( this ).on( 'ready', this.setNodes ); 836 837 this.fetch(); 838 } 856 action: 'parse-embed' 839 857 } ), 840 858 edit: function( node ) { 841 var embed = media.embed,859 var embed = wp.media.embed, 842 860 self = this, 843 861 frame, 844 862 data, 845 863 isURL = 'embedURL' === this.type; 846 864 847 $( document ).trigger( 'media:edit' ); 848 849 data = window.decodeURIComponent( $( node ).attr('data-wpview-text') ); 865 data = decodeURIComponent( $( node ).attr('data-wpview-text') ); 850 866 frame = embed.edit( data, isURL ); 867 851 868 frame.on( 'close', function() { 852 869 frame.detach(); 853 870 } ); 871 854 872 frame.state( 'embed' ).props.on( 'change:url', function (model, url) { 855 873 if ( ! url ) { 856 874 return; 857 875 } 858 876 frame.state( 'embed' ).metadata = model.toJSON(); 859 877 } ); 878 860 879 frame.state( 'embed' ).on( 'select', function() { 861 880 var shortcode; 862 881 … … 865 884 } else { 866 885 shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string(); 867 886 } 868 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );887 $( node ).attr( 'data-wpview-text', encodeURIComponent( shortcode ) ); 869 888 wp.mce.views.refreshView( self, shortcode ); 870 889 frame.detach(); 871 890 } ); 891 872 892 frame.open(); 873 893 } 874 894 }; 875 895 876 wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );896 views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) ); 877 897 878 wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {898 views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, { 879 899 toView: function( content ) { 880 900 var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi, 881 901 match = re.exec( tinymce.trim( content ) ); … … 893 913 }; 894 914 } 895 915 } ) ); 896 897 }(jQuery)); 916 } )( jQuery, wp.mce.views );