wp/wp-includes/js/mce-view.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
     1 /* global tinymce */
     1 /* global tinymce */
     2 
       
     3 window.wp = window.wp || {};
       
     4 
     2 
     5 /*
     3 /*
     6  * The TinyMCE view API.
     4  * The TinyMCE view API.
     7  *
     5  *
     8  * Note: this API is "experimental" meaning that it will probably change
     6  * Note: this API is "experimental" meaning that it will probably change
    22  * |  |- view instance
    20  * |  |- view instance
    23  * |  |  |- ...
    21  * |  |  |- ...
    24  * |- registered view
    22  * |- registered view
    25  * |  |- ...
    23  * |  |- ...
    26  */
    24  */
    27 ( function( window, wp, $ ) {
    25 ( function( window, wp, shortcode, $ ) {
    28 	'use strict';
    26 	'use strict';
    29 
    27 
    30 	var views = {},
    28 	var views = {},
    31 		instances = {};
    29 		instances = {};
    32 
    30 
    85 		 * Scans a given string for each view's pattern,
    83 		 * Scans a given string for each view's pattern,
    86 		 * replacing any matches with markers,
    84 		 * replacing any matches with markers,
    87 		 * and creates a new instance for every match.
    85 		 * and creates a new instance for every match.
    88 		 *
    86 		 *
    89 		 * @param {String} content The string to scan.
    87 		 * @param {String} content The string to scan.
       
    88 		 * @param {tinymce.Editor} editor The editor.
    90 		 *
    89 		 *
    91 		 * @return {String} The string with markers.
    90 		 * @return {String} The string with markers.
    92 		 */
    91 		 */
    93 		setMarkers: function( content ) {
    92 		setMarkers: function( content, editor ) {
    94 			var pieces = [ { content: content } ],
    93 			var pieces = [ { content: content } ],
    95 				self = this,
    94 				self = this,
    96 				instance,
    95 				instance, current;
    97 				current;
       
    98 
    96 
    99 			_.each( views, function( view, type ) {
    97 			_.each( views, function( view, type ) {
   100 				current = pieces.slice();
    98 				current = pieces.slice();
   101 				pieces  = [];
    99 				pieces  = [];
   102 
   100 
   103 				_.each( current, function( piece ) {
   101 				_.each( current, function( piece ) {
   104 					var remaining = piece.content,
   102 					var remaining = piece.content,
   105 						result;
   103 						result, text;
   106 
   104 
   107 					// Ignore processed pieces, but retain their location.
   105 					// Ignore processed pieces, but retain their location.
   108 					if ( piece.processed ) {
   106 					if ( piece.processed ) {
   109 						pieces.push( piece );
   107 						pieces.push( piece );
   110 						return;
   108 						return;
   116 						// Any text before the match becomes an unprocessed piece.
   114 						// Any text before the match becomes an unprocessed piece.
   117 						if ( result.index ) {
   115 						if ( result.index ) {
   118 							pieces.push( { content: remaining.substring( 0, result.index ) } );
   116 							pieces.push( { content: remaining.substring( 0, result.index ) } );
   119 						}
   117 						}
   120 
   118 
       
   119 						result.options.editor = editor;
   121 						instance = self.createInstance( type, result.content, result.options );
   120 						instance = self.createInstance( type, result.content, result.options );
       
   121 						text = instance.loader ? '.' : instance.text;
   122 
   122 
   123 						// Add the processed piece for the match.
   123 						// Add the processed piece for the match.
   124 						pieces.push( {
   124 						pieces.push( {
   125 							content: '<p data-wpview-marker="' + instance.encodedText + '">' + instance.text + '</p>',
   125 							content: instance.ignore ? text : '<p data-wpview-marker="' + instance.encodedText + '">' + text + '</p>',
   126 							processed: true
   126 							processed: true
   127 						} );
   127 						} );
   128 
   128 
   129 						// Update the remaining content.
   129 						// Update the remaining content.
   130 						remaining = remaining.slice( result.index + result.content.length );
   130 						remaining = remaining.slice( result.index + result.content.length );
   136 						pieces.push( { content: remaining } );
   136 						pieces.push( { content: remaining } );
   137 					}
   137 					}
   138 				} );
   138 				} );
   139 			} );
   139 			} );
   140 
   140 
   141 			return _.pluck( pieces, 'content' ).join( '' );
   141 			content = _.pluck( pieces, 'content' ).join( '' );
       
   142 			return content.replace( /<p>\s*<p data-wpview-marker=/g, '<p data-wpview-marker=' ).replace( /<\/p>\s*<\/p>/g, '</p>' );
   142 		},
   143 		},
   143 
   144 
   144 		/**
   145 		/**
   145 		 * Create a view instance.
   146 		 * Create a view instance.
   146 		 *
   147 		 *
   147 		 * @param {String} type    The view type.
   148 		 * @param {String}  type    The view type.
   148 		 * @param {String} text    The textual representation of the view.
   149 		 * @param {String}  text    The textual representation of the view.
   149 		 * @param {Object} options Options.
   150 		 * @param {Object}  options Options.
       
   151 		 * @param {Boolean} force   Recreate the instance. Optional.
   150 		 *
   152 		 *
   151 		 * @return {wp.mce.View} The view instance.
   153 		 * @return {wp.mce.View} The view instance.
   152 		 */
   154 		 */
   153 		createInstance: function( type, text, options ) {
   155 		createInstance: function( type, text, options, force ) {
   154 			var View = this.get( type ),
   156 			var View = this.get( type ),
   155 				encodedText,
   157 				encodedText,
   156 				instance;
   158 				instance;
   157 
   159 
   158 			text = tinymce.DOM.decode( text ),
   160 			if ( text.indexOf( '[' ) !== -1 && text.indexOf( ']' ) !== -1 ) {
   159 			encodedText = encodeURIComponent( text ),
   161 				// Looks like a shortcode? Remove any line breaks from inside of shortcodes
   160 			instance = this.getInstance( encodedText );
   162 				// or autop will replace them with <p> and <br> later and the string won't match.
   161 
   163 				text = text.replace( /\[[^\]]+\]/g, function( match ) {
   162 			if ( instance ) {
   164 					return match.replace( /[\r\n]/g, '' );
   163 				return instance;
   165 				});
   164 			}
   166 			}
       
   167 
       
   168 			if ( ! force ) {
       
   169 				instance = this.getInstance( text );
       
   170 
       
   171 				if ( instance ) {
       
   172 					return instance;
       
   173 				}
       
   174 			}
       
   175 
       
   176 			encodedText = encodeURIComponent( text );
   165 
   177 
   166 			options = _.extend( options || {}, {
   178 			options = _.extend( options || {}, {
   167 				text: text,
   179 				text: text,
   168 				encodedText: encodedText
   180 				encodedText: encodedText
   169 			} );
   181 			} );
   202 		 *
   214 		 *
   203 		 * @param {Boolean} force Rerender all view nodes.
   215 		 * @param {Boolean} force Rerender all view nodes.
   204 		 */
   216 		 */
   205 		render: function( force ) {
   217 		render: function( force ) {
   206 			_.each( instances, function( instance ) {
   218 			_.each( instances, function( instance ) {
   207 				instance.render( force );
   219 				instance.render( null, force );
   208 			} );
   220 			} );
   209 		},
   221 		},
   210 
   222 
   211 		/**
   223 		/**
   212 		 * Update the text of a given view node.
   224 		 * Update the text of a given view node.
   213 		 *
   225 		 *
   214 		 * @param {String}         text   The new text.
   226 		 * @param {String}         text   The new text.
   215 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   227 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   216 		 * @param {HTMLElement}    node   The view node to update.
   228 		 * @param {HTMLElement}    node   The view node to update.
   217 		 */
   229 		 * @param {Boolean}        force  Recreate the instance. Optional.
   218 		update: function( text, editor, node ) {
   230 		 */
       
   231 		update: function( text, editor, node, force ) {
   219 			var instance = this.getInstance( node );
   232 			var instance = this.getInstance( node );
   220 
   233 
   221 			if ( instance ) {
   234 			if ( instance ) {
   222 				instance.update( text, editor, node );
   235 				instance.update( text, editor, node, force );
   223 			}
   236 			}
   224 		},
   237 		},
   225 
   238 
   226 		/**
   239 		/**
   227 		 * Renders any editing interface based on the view type.
   240 		 * Renders any editing interface based on the view type.
   231 		 */
   244 		 */
   232 		edit: function( editor, node ) {
   245 		edit: function( editor, node ) {
   233 			var instance = this.getInstance( node );
   246 			var instance = this.getInstance( node );
   234 
   247 
   235 			if ( instance && instance.edit ) {
   248 			if ( instance && instance.edit ) {
   236 				instance.edit( instance.text, function( text ) {
   249 				instance.edit( instance.text, function( text, force ) {
   237 					instance.update( text, editor, node );
   250 					instance.update( text, editor, node, force );
   238 				} );
   251 				} );
   239 			}
   252 			}
   240 		},
   253 		},
   241 
   254 
   242 		/**
   255 		/**
   265 		this.initialize();
   278 		this.initialize();
   266 	};
   279 	};
   267 
   280 
   268 	wp.mce.View.extend = Backbone.View.extend;
   281 	wp.mce.View.extend = Backbone.View.extend;
   269 
   282 
   270 	_.extend( wp.mce.View.prototype, {
   283 	_.extend( wp.mce.View.prototype, /** @lends wp.mce.View.prototype */{
   271 
   284 
   272 		/**
   285 		/**
   273 		 * The content.
   286 		 * The content.
   274 		 *
   287 		 *
   275 		 * @type {*}
   288 		 * @type {*}
   287 		 * Runs after the view instance is created.
   300 		 * Runs after the view instance is created.
   288 		 */
   301 		 */
   289 		initialize: function() {},
   302 		initialize: function() {},
   290 
   303 
   291 		/**
   304 		/**
   292 		 * Retuns the content to render in the view node.
   305 		 * Returns the content to render in the view node.
   293 		 *
   306 		 *
   294 		 * @return {*}
   307 		 * @return {*}
   295 		 */
   308 		 */
   296 		getContent: function() {
   309 		getContent: function() {
   297 			return this.content;
   310 			return this.content;
   298 		},
   311 		},
   299 
   312 
   300 		/**
   313 		/**
   301 		 * Renders all view nodes tied to this view instance that are not yet rendered.
   314 		 * Renders all view nodes tied to this view instance that are not yet rendered.
   302 		 *
   315 		 *
   303 		 * @param {String} content The content to render. Optional.
   316 		 * @param {String}  content The content to render. Optional.
   304 		 * @param {Boolean} force Rerender all view nodes tied to this view instance.
   317 		 * @param {Boolean} force   Rerender all view nodes tied to this view instance. Optional.
   305 		 */
   318 		 */
   306 		render: function( content, force ) {
   319 		render: function( content, force ) {
   307 			if ( content != null ) {
   320 			if ( content != null ) {
   308 				this.content = content;
   321 				this.content = content;
   309 			}
   322 			}
   320 
   333 
   321 			// Replace any left over markers.
   334 			// Replace any left over markers.
   322 			this.replaceMarkers();
   335 			this.replaceMarkers();
   323 
   336 
   324 			if ( content ) {
   337 			if ( content ) {
   325 				this.setContent( content, function( editor, node, contentNode ) {
   338 				this.setContent( content, function( editor, node ) {
   326 					$( node ).data( 'rendered', true );
   339 					$( node ).data( 'rendered', true );
   327 					this.bindNode.call( this, editor, node, contentNode );
   340 					this.bindNode.call( this, editor, node );
   328 				}, force ? null : false );
   341 				}, force ? null : false );
   329 			} else {
   342 			} else {
   330 				this.setLoader();
   343 				this.setLoader();
   331 			}
   344 			}
   332 		},
   345 		},
   344 		/**
   357 		/**
   345 		 * Unbinds all view nodes tied to this view instance.
   358 		 * Unbinds all view nodes tied to this view instance.
   346 		 * Runs before their content is removed from the DOM.
   359 		 * Runs before their content is removed from the DOM.
   347 		 */
   360 		 */
   348 		unbind: function() {
   361 		unbind: function() {
   349 			this.getNodes( function( editor, node, contentNode ) {
   362 			this.getNodes( function( editor, node ) {
   350 				this.unbindNode.call( this, editor, node, contentNode );
   363 				this.unbindNode.call( this, editor, node );
   351 				$( node ).trigger( 'wp-mce-view-unbind' );
       
   352 			}, true );
   364 			}, true );
   353 		},
   365 		},
   354 
   366 
   355 		/**
   367 		/**
   356 		 * Gets all the TinyMCE editor instances that support views.
   368 		 * Gets all the TinyMCE editor instances that support views.
   387 						data = $( this ).data( 'rendered' ) === true;
   399 						data = $( this ).data( 'rendered' ) === true;
   388 
   400 
   389 						return rendered ? data : ! data;
   401 						return rendered ? data : ! data;
   390 					} )
   402 					} )
   391 					.each( function() {
   403 					.each( function() {
   392 						callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) );
   404 						callback.call( self, editor, this, this /* back compat */ );
   393 					} );
   405 					} );
   394 			} );
   406 			} );
   395 		},
   407 		},
   396 
   408 
   397 		/**
   409 		/**
   414 		/**
   426 		/**
   415 		 * Replaces all marker nodes tied to this view instance.
   427 		 * Replaces all marker nodes tied to this view instance.
   416 		 */
   428 		 */
   417 		replaceMarkers: function() {
   429 		replaceMarkers: function() {
   418 			this.getMarkers( function( editor, node ) {
   430 			this.getMarkers( function( editor, node ) {
   419 				if ( $( node ).text() !== this.text ) {
   431 				var selected = node === editor.selection.getNode();
       
   432 				var $viewNode;
       
   433 
       
   434 				if ( ! this.loader && $( node ).text() !== tinymce.DOM.decode( this.text ) ) {
   420 					editor.dom.setAttrib( node, 'data-wpview-marker', null );
   435 					editor.dom.setAttrib( node, 'data-wpview-marker', null );
   421 					return;
   436 					return;
   422 				}
   437 				}
   423 
   438 
   424 				editor.dom.replace(
   439 				$viewNode = editor.$(
   425 					editor.dom.createFragment(
   440 					'<div class="wpview wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '" contenteditable="false"></div>'
   426 						'<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' +
       
   427 							'<p class="wpview-selection-before">\u00a0</p>' +
       
   428 							'<div class="wpview-body" contenteditable="false">' +
       
   429 								'<div class="wpview-content wpview-type-' + this.type + '"></div>' +
       
   430 							'</div>' +
       
   431 							'<p class="wpview-selection-after">\u00a0</p>' +
       
   432 						'</div>'
       
   433 					),
       
   434 					node
       
   435 				);
   441 				);
       
   442 
       
   443 				editor.$( node ).replaceWith( $viewNode );
       
   444 
       
   445 				if ( selected ) {
       
   446 					setTimeout( function() {
       
   447 						editor.selection.select( $viewNode[0] );
       
   448 						editor.selection.collapse();
       
   449 					} );
       
   450 				}
   436 			} );
   451 			} );
   437 		},
   452 		},
   438 
   453 
   439 		/**
   454 		/**
   440 		 * Removes all marker nodes tied to this view instance.
   455 		 * Removes all marker nodes tied to this view instance.
   451 		 * @param {*}        content  The content to set.
   466 		 * @param {*}        content  The content to set.
   452 		 * @param {Function} callback A callback. Optional.
   467 		 * @param {Function} callback A callback. Optional.
   453 		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
   468 		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
   454 		 */
   469 		 */
   455 		setContent: function( content, callback, rendered ) {
   470 		setContent: function( content, callback, rendered ) {
   456 			if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) {
   471 			if ( _.isObject( content ) && ( content.sandbox || content.head || content.body.indexOf( '<script' ) !== -1 ) ) {
   457 				this.setIframes( content.head || '', content.body, callback, rendered );
   472 				this.setIframes( content.head || '', content.body, callback, rendered );
   458 			} else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
   473 			} else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
   459 				this.setIframes( '', content, callback, rendered );
   474 				this.setIframes( '', content, callback, rendered );
   460 			} else {
   475 			} else {
   461 				this.getNodes( function( editor, node, contentNode ) {
   476 				this.getNodes( function( editor, node ) {
   462 					content = content.body || content;
   477 					content = content.body || content;
   463 
   478 
   464 					if ( content.indexOf( '<iframe' ) !== -1 ) {
   479 					if ( content.indexOf( '<iframe' ) !== -1 ) {
   465 						content += '<div class="wpview-overlay"></div>';
   480 						content += '<span class="mce-shim"></span>';
   466 					}
   481 					}
   467 
   482 
   468 					contentNode.innerHTML = '';
   483 					editor.undoManager.transact( function() {
   469 					contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
   484 						node.innerHTML = '';
   470 
   485 						node.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
   471 					callback && callback.call( this, editor, node, contentNode );
   486 						editor.dom.add( node, 'span', { 'class': 'wpview-end' } );
       
   487 					} );
       
   488 
       
   489 					callback && callback.call( this, editor, node );
   472 				}, rendered );
   490 				}, rendered );
   473 			}
   491 			}
   474 		},
   492 		},
   475 
   493 
   476 		/**
   494 		/**
   480 		 * @param {String}   body     HTML string to be added to the body of the document.
   498 		 * @param {String}   body     HTML string to be added to the body of the document.
   481 		 * @param {Function} callback A callback. Optional.
   499 		 * @param {Function} callback A callback. Optional.
   482 		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
   500 		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
   483 		 */
   501 		 */
   484 		setIframes: function( head, body, callback, rendered ) {
   502 		setIframes: function( head, body, callback, rendered ) {
   485 			var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
   503 			var self = this;
   486 				self = this;
   504 
   487 
   505 			if ( body.indexOf( '[' ) !== -1 && body.indexOf( ']' ) !== -1 ) {
   488 			this.getNodes( function( editor, node, contentNode ) {
   506 				var shortcodesRegExp = new RegExp( '\\[\\/?(?:' + window.mceViewL10n.shortcodes.join( '|' ) + ')[^\\]]*?\\]', 'g' );
       
   507 				// Escape tags inside shortcode previews.
       
   508 				body = body.replace( shortcodesRegExp, function( match ) {
       
   509 					return match.replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
       
   510 				} );
       
   511 			}
       
   512 
       
   513 			this.getNodes( function( editor, node ) {
   489 				var dom = editor.dom,
   514 				var dom = editor.dom,
   490 					styles = '',
   515 					styles = '',
   491 					bodyClasses = editor.getBody().className || '',
   516 					bodyClasses = editor.getBody().className || '',
   492 					editorHead = editor.getDoc().getElementsByTagName( 'head' )[0];
   517 					editorHead = editor.getDoc().getElementsByTagName( 'head' )[0],
       
   518 					iframe, iframeWin, iframeDoc, MutationObserver, observer, i, block;
   493 
   519 
   494 				tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( link ) {
   520 				tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( link ) {
   495 					if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
   521 					if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
   496 						link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
   522 						link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
   497 
   523 
   498 						styles += dom.getOuterHTML( link );
   524 						styles += dom.getOuterHTML( link );
   499 					}
   525 					}
   500 				} );
   526 				} );
   501 
   527 
   502 				// Seems the browsers need a bit of time to insert/set the view nodes,
   528 				if ( self.iframeHeight ) {
   503 				// or the iframe will fail especially when switching Text => Visual.
   529 					dom.add( node, 'span', {
   504 				setTimeout( function() {
   530 						'data-mce-bogus': 1,
   505 					var iframe, iframeDoc, observer, i;
   531 						style: {
   506 
   532 							display: 'block',
   507 					contentNode.innerHTML = '';
   533 							width: '100%',
   508 
   534 							height: self.iframeHeight
   509 					iframe = dom.add( contentNode, 'iframe', {
   535 						}
       
   536 					}, '\u200B' );
       
   537 				}
       
   538 
       
   539 				editor.undoManager.transact( function() {
       
   540 					node.innerHTML = '';
       
   541 
       
   542 					iframe = dom.add( node, 'iframe', {
   510 						/* jshint scripturl: true */
   543 						/* jshint scripturl: true */
   511 						src: tinymce.Env.ie ? 'javascript:""' : '',
   544 						src: tinymce.Env.ie ? 'javascript:""' : '',
   512 						frameBorder: '0',
   545 						frameBorder: '0',
   513 						allowTransparency: 'true',
   546 						allowTransparency: 'true',
   514 						scrolling: 'no',
   547 						scrolling: 'no',
   515 						'class': 'wpview-sandbox',
   548 						'class': 'wpview-sandbox',
   516 						style: {
   549 						style: {
   517 							width: '100%',
   550 							width: '100%',
   518 							display: 'block'
   551 							display: 'block'
       
   552 						},
       
   553 						height: self.iframeHeight
       
   554 					} );
       
   555 
       
   556 					dom.add( node, 'span', { 'class': 'mce-shim' } );
       
   557 					dom.add( node, 'span', { 'class': 'wpview-end' } );
       
   558 				} );
       
   559 
       
   560 				// Bail if the iframe node is not attached to the DOM.
       
   561 				// Happens when the view is dragged in the editor.
       
   562 				// There is a browser restriction when iframes are moved in the DOM. They get emptied.
       
   563 				// The iframe will be rerendered after dropping the view node at the new location.
       
   564 				if ( ! iframe.contentWindow ) {
       
   565 					return;
       
   566 				}
       
   567 
       
   568 				iframeWin = iframe.contentWindow;
       
   569 				iframeDoc = iframeWin.document;
       
   570 				iframeDoc.open();
       
   571 
       
   572 				iframeDoc.write(
       
   573 					'<!DOCTYPE html>' +
       
   574 					'<html>' +
       
   575 						'<head>' +
       
   576 							'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
       
   577 							head +
       
   578 							styles +
       
   579 							'<style>' +
       
   580 								'html {' +
       
   581 									'background: transparent;' +
       
   582 									'padding: 0;' +
       
   583 									'margin: 0;' +
       
   584 								'}' +
       
   585 								'body#wpview-iframe-sandbox {' +
       
   586 									'background: transparent;' +
       
   587 									'padding: 1px 0 !important;' +
       
   588 									'margin: -1px 0 0 !important;' +
       
   589 								'}' +
       
   590 								'body#wpview-iframe-sandbox:before,' +
       
   591 								'body#wpview-iframe-sandbox:after {' +
       
   592 									'display: none;' +
       
   593 									'content: "";' +
       
   594 								'}' +
       
   595 								'iframe {' +
       
   596 									'max-width: 100%;' +
       
   597 								'}' +
       
   598 							'</style>' +
       
   599 						'</head>' +
       
   600 						'<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
       
   601 							body +
       
   602 						'</body>' +
       
   603 					'</html>'
       
   604 				);
       
   605 
       
   606 				iframeDoc.close();
       
   607 
       
   608 				function resize() {
       
   609 					var $iframe;
       
   610 
       
   611 					if ( block ) {
       
   612 						return;
       
   613 					}
       
   614 
       
   615 					// Make sure the iframe still exists.
       
   616 					if ( iframe.contentWindow ) {
       
   617 						$iframe = $( iframe );
       
   618 						self.iframeHeight = $( iframeDoc.body ).height();
       
   619 
       
   620 						if ( $iframe.height() !== self.iframeHeight ) {
       
   621 							$iframe.height( self.iframeHeight );
       
   622 							editor.nodeChanged();
   519 						}
   623 						}
       
   624 					}
       
   625 				}
       
   626 
       
   627 				if ( self.iframeHeight ) {
       
   628 					block = true;
       
   629 
       
   630 					setTimeout( function() {
       
   631 						block = false;
       
   632 						resize();
       
   633 					}, 3000 );
       
   634 				}
       
   635 
       
   636 				function reload() {
       
   637 					if ( ! editor.isHidden() ) {
       
   638 						$( node ).data( 'rendered', null );
       
   639 
       
   640 						setTimeout( function() {
       
   641 							wp.mce.views.render();
       
   642 						} );
       
   643 					}
       
   644 				}
       
   645 
       
   646 				function addObserver() {
       
   647 					observer = new MutationObserver( _.debounce( resize, 100 ) );
       
   648 
       
   649 					observer.observe( iframeDoc.body, {
       
   650 						attributes: true,
       
   651 						childList: true,
       
   652 						subtree: true
   520 					} );
   653 					} );
   521 
   654 				}
   522 					dom.add( contentNode, 'div', { 'class': 'wpview-overlay' } );
   655 
   523 
   656 				$( iframeWin ).on( 'load', resize ).on( 'unload', reload );
   524 					iframeDoc = iframe.contentWindow.document;
   657 
   525 
   658 				MutationObserver = iframeWin.MutationObserver || iframeWin.WebKitMutationObserver || iframeWin.MozMutationObserver;
   526 					iframeDoc.open();
   659 
   527 
   660 				if ( MutationObserver ) {
   528 					iframeDoc.write(
   661 					if ( ! iframeDoc.body ) {
   529 						'<!DOCTYPE html>' +
   662 						iframeDoc.addEventListener( 'DOMContentLoaded', addObserver, false );
   530 						'<html>' +
       
   531 							'<head>' +
       
   532 								'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
       
   533 								head +
       
   534 								styles +
       
   535 								'<style>' +
       
   536 									'html {' +
       
   537 										'background: transparent;' +
       
   538 										'padding: 0;' +
       
   539 										'margin: 0;' +
       
   540 									'}' +
       
   541 									'body#wpview-iframe-sandbox {' +
       
   542 										'background: transparent;' +
       
   543 										'padding: 1px 0 !important;' +
       
   544 										'margin: -1px 0 0 !important;' +
       
   545 									'}' +
       
   546 									'body#wpview-iframe-sandbox:before,' +
       
   547 									'body#wpview-iframe-sandbox:after {' +
       
   548 										'display: none;' +
       
   549 										'content: "";' +
       
   550 									'}' +
       
   551 								'</style>' +
       
   552 							'</head>' +
       
   553 							'<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
       
   554 								body +
       
   555 							'</body>' +
       
   556 						'</html>'
       
   557 					);
       
   558 
       
   559 					iframeDoc.close();
       
   560 
       
   561 					function resize() {
       
   562 						var $iframe, iframeDocHeight;
       
   563 
       
   564 						// Make sure the iframe still exists.
       
   565 						if ( iframe.contentWindow ) {
       
   566 							$iframe = $( iframe );
       
   567 							iframeDocHeight = $( iframeDoc.body ).height();
       
   568 
       
   569 							if ( $iframe.height() !== iframeDocHeight ) {
       
   570 								$iframe.height( iframeDocHeight );
       
   571 								editor.nodeChanged();
       
   572 							}
       
   573 						}
       
   574 					}
       
   575 
       
   576 					$( iframe.contentWindow ).on( 'load', resize );
       
   577 
       
   578 					if ( MutationObserver ) {
       
   579 						observer = new MutationObserver( _.debounce( resize, 100 ) );
       
   580 
       
   581 						observer.observe( iframeDoc.body, {
       
   582 							attributes: true,
       
   583 							childList: true,
       
   584 							subtree: true
       
   585 						} );
       
   586 
       
   587 						$( node ).one( 'wp-mce-view-unbind', function() {
       
   588 							observer.disconnect();
       
   589 						} );
       
   590 					} else {
   663 					} else {
   591 						for ( i = 1; i < 6; i++ ) {
   664 						addObserver();
   592 							setTimeout( resize, i * 700 );
   665 					}
   593 						}
   666 				} else {
   594 					}
   667 					for ( i = 1; i < 6; i++ ) {
   595 
   668 						setTimeout( resize, i * 700 );
   596 					function classChange() {
   669 					}
   597 						iframeDoc.body.className = editor.getBody().className;
   670 				}
   598 					}
   671 
   599 
   672 				callback && callback.call( self, editor, node );
   600 					editor.on( 'wp-body-class-change', classChange );
       
   601 
       
   602 					$( node ).one( 'wp-mce-view-unbind', function() {
       
   603 						editor.off( 'wp-body-class-change', classChange );
       
   604 					} );
       
   605 
       
   606 					callback && callback.call( self, editor, node, contentNode );
       
   607 				}, 50 );
       
   608 			}, rendered );
   673 			}, rendered );
   609 		},
   674 		},
   610 
   675 
   611 		/**
   676 		/**
   612 		 * Sets a loader for all view nodes tied to this view instance.
   677 		 * Sets a loader for all view nodes tied to this view instance.
   613 		 */
   678 		 */
   614 		setLoader: function() {
   679 		setLoader: function( dashicon ) {
   615 			this.setContent(
   680 			this.setContent(
   616 				'<div class="loading-placeholder">' +
   681 				'<div class="loading-placeholder">' +
   617 					'<div class="dashicons dashicons-admin-media"></div>' +
   682 					'<div class="dashicons dashicons-' + ( dashicon || 'admin-media' ) + '"></div>' +
   618 					'<div class="wpview-loading"><ins></ins></div>' +
   683 					'<div class="wpview-loading"><ins></ins></div>' +
   619 				'</div>'
   684 				'</div>'
   620 			);
   685 			);
   621 		},
   686 		},
   622 
   687 
   623 		/**
   688 		/**
   624 		 * Sets an error for all view nodes tied to this view instance.
   689 		 * Sets an error for all view nodes tied to this view instance.
   625 		 *
   690 		 *
   626 		 * @param {String} message  The error message to set.
   691 		 * @param {String} message  The error message to set.
   627 		 * @param {String} dashicon A dashicon ID (optional). {@link https://developer.wordpress.org/resource/dashicons/}
   692 		 * @param {String} dashicon A dashicon ID. Optional. {@link https://developer.wordpress.org/resource/dashicons/}
   628 		 */
   693 		 */
   629 		setError: function( message, dashicon ) {
   694 		setError: function( message, dashicon ) {
   630 			this.setContent(
   695 			this.setContent(
   631 				'<div class="wpview-error">' +
   696 				'<div class="wpview-error">' +
   632 					'<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
   697 					'<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
   641 		 * @param {String} content The string to scan.
   706 		 * @param {String} content The string to scan.
   642 		 *
   707 		 *
   643 		 * @return {Object}
   708 		 * @return {Object}
   644 		 */
   709 		 */
   645 		match: function( content ) {
   710 		match: function( content ) {
   646 			var match = wp.shortcode.next( this.type, content );
   711 			var match = shortcode.next( this.type, content );
   647 
   712 
   648 			if ( match ) {
   713 			if ( match ) {
   649 				return {
   714 				return {
   650 					index: match.index,
   715 					index: match.index,
   651 					content: match.content,
   716 					content: match.content,
   660 		 * Update the text of a given view node.
   725 		 * Update the text of a given view node.
   661 		 *
   726 		 *
   662 		 * @param {String}         text   The new text.
   727 		 * @param {String}         text   The new text.
   663 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   728 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   664 		 * @param {HTMLElement}    node   The view node to update.
   729 		 * @param {HTMLElement}    node   The view node to update.
   665 		 */
   730 		 * @param {Boolean}        force  Recreate the instance. Optional.
   666 		update: function( text, editor, node ) {
   731 		 */
       
   732 		update: function( text, editor, node, force ) {
   667 			_.find( views, function( view, type ) {
   733 			_.find( views, function( view, type ) {
   668 				var match = view.prototype.match( text );
   734 				var match = view.prototype.match( text );
   669 
   735 
   670 				if ( match ) {
   736 				if ( match ) {
   671 					$( node ).data( 'rendered', false );
   737 					$( node ).data( 'rendered', false );
   672 					editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
   738 					editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
   673 					wp.mce.views.createInstance( type, text, match.options ).render();
   739 					wp.mce.views.createInstance( type, text, match.options, force ).render();
       
   740 
       
   741 					editor.selection.select( node );
       
   742 					editor.nodeChanged();
   674 					editor.focus();
   743 					editor.focus();
   675 
   744 
   676 					return true;
   745 					return true;
   677 				}
   746 				}
   678 			} );
   747 			} );
   683 		 *
   752 		 *
   684 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   753 		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
   685 		 * @param {HTMLElement}    node   The view node to remove.
   754 		 * @param {HTMLElement}    node   The view node to remove.
   686 		 */
   755 		 */
   687 		remove: function( editor, node ) {
   756 		remove: function( editor, node ) {
   688 			this.unbindNode.call( this, editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
   757 			this.unbindNode.call( this, editor, node );
   689 			$( node ).trigger( 'wp-mce-view-unbind' );
       
   690 			editor.dom.remove( node );
   758 			editor.dom.remove( node );
   691 			editor.focus();
   759 			editor.focus();
   692 		}
   760 		}
   693 	} );
   761 	} );
   694 } )( window, window.wp, window.jQuery );
   762 } )( window, window.wp, window.wp.shortcode, window.jQuery );
   695 
   763 
   696 /*
   764 /*
   697  * The WordPress core TinyMCE views.
   765  * The WordPress core TinyMCE views.
   698  * Views for the gallery, audio, video, playlist and embed shortcodes,
   766  * Views for the gallery, audio, video, playlist and embed shortcodes,
   699  * and a view for embeddable URLs.
   767  * and a view for embeddable URLs.
   700  */
   768  */
   701 ( function( window, views, $ ) {
   769 ( function( window, views, media, $ ) {
   702 	var postID = $( '#post_ID' ).val() || 0,
   770 	var base, gallery, av, embed,
   703 		media, gallery, av, embed;
   771 		schema, parser, serializer;
   704 
   772 
   705 	media = {
   773 	function verifyHTML( string ) {
       
   774 		var settings = {};
       
   775 
       
   776 		if ( ! window.tinymce ) {
       
   777 			return string.replace( /<[^>]+>/g, '' );
       
   778 		}
       
   779 
       
   780 		if ( ! string || ( string.indexOf( '<' ) === -1 && string.indexOf( '>' ) === -1 ) ) {
       
   781 			return string;
       
   782 		}
       
   783 
       
   784 		schema = schema || new window.tinymce.html.Schema( settings );
       
   785 		parser = parser || new window.tinymce.html.DomParser( settings, schema );
       
   786 		serializer = serializer || new window.tinymce.html.Serializer( settings, schema );
       
   787 
       
   788 		return serializer.serialize( parser.parse( string, { forced_root_block: false } ) );
       
   789 	}
       
   790 
       
   791 	base = {
   706 		state: [],
   792 		state: [],
   707 
   793 
   708 		edit: function( text, update ) {
   794 		edit: function( text, update ) {
   709 			var media = wp.media[ this.type ],
   795 			var type = this.type,
   710 				frame = media.edit( text );
   796 				frame = media[ type ].edit( text );
   711 
   797 
   712 			this.pausePlayers && this.pausePlayers();
   798 			this.pausePlayers && this.pausePlayers();
   713 
   799 
   714 			_.each( this.state, function( state ) {
   800 			_.each( this.state, function( state ) {
   715 				frame.state( state ).on( 'update', function( selection ) {
   801 				frame.state( state ).on( 'update', function( selection ) {
   716 					update( media.shortcode( selection ).string() );
   802 					update( media[ type ].shortcode( selection ).string(), type === 'gallery' );
   717 				} );
   803 				} );
   718 			} );
   804 			} );
   719 
   805 
   720 			frame.on( 'close', function() {
   806 			frame.on( 'close', function() {
   721 				frame.detach();
   807 				frame.detach();
   723 
   809 
   724 			frame.open();
   810 			frame.open();
   725 		}
   811 		}
   726 	};
   812 	};
   727 
   813 
   728 	gallery = _.extend( {}, media, {
   814 	gallery = _.extend( {}, base, {
   729 		state: [ 'gallery-edit' ],
   815 		state: [ 'gallery-edit' ],
   730 		template: wp.media.template( 'editor-gallery' ),
   816 		template: media.template( 'editor-gallery' ),
   731 
   817 
   732 		initialize: function() {
   818 		initialize: function() {
   733 			var attachments = wp.media.gallery.attachments( this.shortcode, postID ),
   819 			var attachments = media.gallery.attachments( this.shortcode, media.view.settings.post.id ),
   734 				attrs = this.shortcode.attrs.named,
   820 				attrs = this.shortcode.attrs.named,
   735 				self = this;
   821 				self = this;
   736 
   822 
   737 			attachments.more()
   823 			attachments.more()
   738 			.done( function() {
   824 			.done( function() {
   749 						}
   835 						}
   750 					}
   836 					}
   751 				} );
   837 				} );
   752 
   838 
   753 				self.render( self.template( {
   839 				self.render( self.template( {
       
   840 					verifyHTML: verifyHTML,
   754 					attachments: attachments,
   841 					attachments: attachments,
   755 					columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
   842 					columns: attrs.columns ? parseInt( attrs.columns, 10 ) : media.galleryDefaults.columns
   756 				} ) );
   843 				} ) );
   757 			} )
   844 			} )
   758 			.fail( function( jqXHR, textStatus ) {
   845 			.fail( function( jqXHR, textStatus ) {
   759 				self.setError( textStatus );
   846 				self.setError( textStatus );
   760 			} );
   847 			} );
   761 		}
   848 		}
   762 	} );
   849 	} );
   763 
   850 
   764 	av = _.extend( {}, media, {
   851 	av = _.extend( {}, base, {
   765 		action: 'parse-media-shortcode',
   852 		action: 'parse-media-shortcode',
   766 
   853 
   767 		initialize: function() {
   854 		initialize: function() {
   768 			var self = this;
   855 			var self = this, maxwidth = null;
   769 
   856 
   770 			if ( this.url ) {
   857 			if ( this.url ) {
   771 				this.loader = false;
   858 				this.loader = false;
   772 				this.shortcode = wp.media.embed.shortcode( {
   859 				this.shortcode = media.embed.shortcode( {
   773 					url: this.text
   860 					url: this.text
   774 				} );
   861 				} );
   775 			}
   862 			}
   776 
   863 
       
   864 			// Obtain the target width for the embed.
       
   865 			if ( self.editor ) {
       
   866 				maxwidth = self.editor.getBody().clientWidth;
       
   867 			}
       
   868 
   777 			wp.ajax.post( this.action, {
   869 			wp.ajax.post( this.action, {
   778 				post_ID: postID,
   870 				post_ID: media.view.settings.post.id,
   779 				type: this.shortcode.tag,
   871 				type: this.shortcode.tag,
   780 				shortcode: this.shortcode.string()
   872 				shortcode: this.shortcode.string(),
       
   873 				maxwidth: maxwidth
   781 			} )
   874 			} )
   782 			.done( function( response ) {
   875 			.done( function( response ) {
   783 				self.render( response );
   876 				self.render( response );
   784 			} )
   877 			} )
   785 			.fail( function( response ) {
   878 			.fail( function( response ) {
   786 				if ( self.url ) {
   879 				if ( self.url ) {
       
   880 					self.ignore = true;
   787 					self.removeMarkers();
   881 					self.removeMarkers();
   788 				} else {
   882 				} else {
   789 					self.setError( response.message || response.statusText, 'admin-media' );
   883 					self.setError( response.message || response.statusText, 'admin-media' );
   790 				}
   884 				}
   791 			} );
   885 			} );
   814 
   908 
   815 	embed = _.extend( {}, av, {
   909 	embed = _.extend( {}, av, {
   816 		action: 'parse-embed',
   910 		action: 'parse-embed',
   817 
   911 
   818 		edit: function( text, update ) {
   912 		edit: function( text, update ) {
   819 			var media = wp.media.embed,
   913 			var frame = media.embed.edit( text, this.url ),
   820 				frame = media.edit( text, this.url ),
       
   821 				self = this;
   914 				self = this;
   822 
   915 
   823 			this.pausePlayers();
   916 			this.pausePlayers();
   824 
   917 
   825 			frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
   918 			frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
   832 				var data = frame.state( 'embed' ).metadata;
   925 				var data = frame.state( 'embed' ).metadata;
   833 
   926 
   834 				if ( self.url ) {
   927 				if ( self.url ) {
   835 					update( data.url );
   928 					update( data.url );
   836 				} else {
   929 				} else {
   837 					update( media.shortcode( data ).string() );
   930 					update( media.embed.shortcode( data ).string() );
   838 				}
   931 				}
   839 			} );
   932 			} );
   840 
   933 
   841 			frame.on( 'close', function() {
   934 			frame.on( 'close', function() {
   842 				frame.detach();
   935 				frame.detach();
   876 					}
   969 					}
   877 				};
   970 				};
   878 			}
   971 			}
   879 		}
   972 		}
   880 	} ) );
   973 	} ) );
   881 } )( window, window.wp.mce.views, window.jQuery );
   974 } )( window, window.wp.mce.views, window.wp.media, window.jQuery );