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