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