wp/wp-includes/js/tinymce/plugins/wpview/plugin.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
     1 /* global tinymce */
       
     2 
       
     3 /**
     1 /**
     4  * WordPress View plugin.
     2  * WordPress View plugin.
     5  */
     3  */
     6 tinymce.PluginManager.add( 'wpview', function( editor ) {
     4 ( function( tinymce, wp ) {
     7 	var $ = editor.$,
     5 	tinymce.PluginManager.add( 'wpview', function( editor ) {
     8 		selected,
     6 		function noop () {}
     9 		Env = tinymce.Env,
     7 
    10 		VK = tinymce.util.VK,
     8 		if ( ! wp || ! wp.mce || ! wp.mce.views ) {
    11 		TreeWalker = tinymce.dom.TreeWalker,
     9 			return {
    12 		toRemove = false,
    10 				getView: noop
    13 		firstFocus = true,
    11 			};
    14 		_noop = function() { return false; },
       
    15 		isios = /iPad|iPod|iPhone/.test( navigator.userAgent ),
       
    16 		cursorInterval,
       
    17 		lastKeyDownNode,
       
    18 		setViewCursorTries,
       
    19 		focus,
       
    20 		execCommandView,
       
    21 		execCommandBefore,
       
    22 		toolbar;
       
    23 
       
    24 	function getView( node ) {
       
    25 		return getParent( node, 'wpview-wrap' );
       
    26 	}
       
    27 
       
    28 	/**
       
    29 	 * Returns the node or a parent of the node that has the passed className.
       
    30 	 * Doing this directly is about 40% faster
       
    31 	 */
       
    32 	function getParent( node, className ) {
       
    33 		while ( node && node.parentNode ) {
       
    34 			if ( node.className && ( ' ' + node.className + ' ' ).indexOf( ' ' + className + ' ' ) !== -1 ) {
       
    35 				return node;
       
    36 			}
       
    37 
       
    38 			node = node.parentNode;
       
    39 		}
    12 		}
    40 
    13 
    41 		return false;
    14 		// Check if a node is a view or not.
    42 	}
    15 		function isView( node ) {
    43 
    16 			return editor.dom.hasClass( node, 'wpview' );
    44 	function _stop( event ) {
       
    45 		event.stopPropagation();
       
    46 	}
       
    47 
       
    48 	function setViewCursor( before, view ) {
       
    49 		var location = before ? 'before' : 'after',
       
    50 			offset = before ? 0 : 1;
       
    51 		deselect();
       
    52 		editor.selection.setCursorLocation( editor.dom.select( '.wpview-selection-' + location, view )[0], offset );
       
    53 		editor.nodeChanged();
       
    54 	}
       
    55 
       
    56 	function handleEnter( view, before, key ) {
       
    57 		var dom = editor.dom,
       
    58 			padNode = dom.create( 'p' );
       
    59 
       
    60 		if ( ! ( Env.ie && Env.ie < 11 ) ) {
       
    61 			padNode.innerHTML = '<br data-mce-bogus="1">';
       
    62 		}
    17 		}
    63 
    18 
    64 		if ( before ) {
    19 		// Replace view tags with their text.
    65 			view.parentNode.insertBefore( padNode, view );
    20 		function resetViews( content ) {
    66 		} else {
    21 			function callback( match, $1 ) {
    67 			dom.insertAfter( padNode, view );
    22 				return '<p>' + window.decodeURIComponent( $1 ) + '</p>';
       
    23 			}
       
    24 
       
    25 			if ( ! content ) {
       
    26 				return content;
       
    27 			}
       
    28 
       
    29 			return content
       
    30 				.replace( /<div[^>]+data-wpview-text="([^"]+)"[^>]*>(?:\.|[\s\S]+?wpview-end[^>]+>\s*<\/span>\s*)?<\/div>/g, callback )
       
    31 				.replace( /<p[^>]+data-wpview-marker="([^"]+)"[^>]*>[\s\S]*?<\/p>/g, callback );
    68 		}
    32 		}
    69 
    33 
    70 		deselect();
    34 		editor.on( 'init', function() {
    71 
    35 			var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
    72 		if ( before && key === VK.ENTER ) {
    36 
    73 			setViewCursor( before, view );
    37 			if ( MutationObserver ) {
    74 		} else {
    38 				new MutationObserver( function() {
    75 			editor.selection.setCursorLocation( padNode, 0 );
    39 					editor.fire( 'wp-body-class-change' );
    76 		}
    40 				} )
    77 
    41 				.observe( editor.getBody(), {
    78 		editor.nodeChanged();
    42 					attributes: true,
    79 	}
    43 					attributeFilter: ['class']
    80 
    44 				} );
    81 	function removeView( view ) {
    45 			}
    82 		editor.undoManager.transact( function() {
    46 
    83 			handleEnter( view );
    47 			// Pass on body class name changes from the editor to the wpView iframes.
    84 			wp.mce.views.remove( editor, view );
    48 			editor.on( 'wp-body-class-change', function() {
       
    49 				var className = editor.getBody().className;
       
    50 
       
    51 				editor.$( 'iframe[class="wpview-sandbox"]' ).each( function( i, iframe ) {
       
    52 					// Make sure it is a local iframe
       
    53 					// jshint scripturl: true
       
    54 					if ( ! iframe.src || iframe.src === 'javascript:""' ) {
       
    55 						try {
       
    56 							iframe.contentWindow.document.body.className = className;
       
    57 						} catch( er ) {}
       
    58 					}
       
    59 				});
       
    60 			} );
    85 		});
    61 		});
    86 	}
    62 
    87 
    63 		// Scan new content for matching view patterns and replace them with markers.
    88 	function select( viewNode ) {
    64 		editor.on( 'beforesetcontent', function( event ) {
    89 		var clipboard,
    65 			var node;
    90 			dom = editor.dom;
    66 
    91 
    67 			if ( ! event.selection ) {
    92 		if ( ! viewNode ) {
    68 				wp.mce.views.unbind();
    93 			return;
    69 			}
    94 		}
    70 
    95 
    71 			if ( ! event.content ) {
    96 		if ( viewNode !== selected ) {
    72 				return;
    97 			// Make sure that the editor is focused.
    73 			}
    98 			// It is possible that the editor is not focused when the mouse event fires
    74 
    99 			// without focus, the selection will not work properly.
    75 			if ( ! event.load ) {
   100 			editor.getBody().focus();
    76 				node = editor.selection.getNode();
   101 
    77 
   102 			deselect();
    78 				if ( node && node !== editor.getBody() && /^\s*https?:\/\/\S+\s*$/i.test( event.content ) ) {
   103 			selected = viewNode;
    79 					// When a url is pasted or inserted, only try to embed it when it is in an empty paragrapgh.
   104 			dom.setAttrib( viewNode, 'data-mce-selected', 1 );
    80 					node = editor.dom.getParent( node, 'p' );
   105 
    81 
   106 			clipboard = dom.create( 'div', {
    82 					if ( node && /^[\s\uFEFF\u00A0]*$/.test( editor.$( node ).text() || '' ) ) {
   107 				'class': 'wpview-clipboard',
    83 						// Make sure there are no empty inline elements in the <p>
   108 				'contenteditable': 'true'
    84 						node.innerHTML = '';
   109 			}, wp.mce.views.getText( viewNode ) );
    85 					} else {
   110 
    86 						return;
   111 			editor.dom.select( '.wpview-body', viewNode )[0].appendChild( clipboard );
    87 					}
   112 
    88 				}
   113 			// Both of the following are necessary to prevent manipulating the selection/focus
    89 			}
   114 			dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop );
    90 
   115 			dom.bind( selected, 'beforedeactivate focusin focusout', _stop );
    91 			event.content = wp.mce.views.setMarkers( event.content, editor );
   116 
    92 		} );
   117 			// select the hidden div
    93 
   118 			if ( isios ) {
    94 		// Replace any new markers nodes with views.
   119 				editor.selection.select( clipboard );
    95 		editor.on( 'setcontent', function() {
   120 			} else {
    96 			wp.mce.views.render();
   121 				editor.selection.select( clipboard, true );
    97 		} );
   122 			}
    98 
   123 		}
    99 		// Empty view nodes for easier processing.
   124 
   100 		editor.on( 'preprocess hide', function( event ) {
   125 		editor.nodeChanged();
   101 			editor.$( 'div[data-wpview-text], p[data-wpview-marker]', event.node ).each( function( i, node ) {
   126 		editor.fire( 'wpview-selected', viewNode );
   102 				node.innerHTML = '.';
   127 	}
   103 			} );
   128 
   104 		}, true );
   129 	/**
   105 
   130 	 * Deselect a selected view and remove clipboard
   106 		// Replace views with their text.
   131 	 */
   107 		editor.on( 'postprocess', function( event ) {
   132 	function deselect() {
   108 			event.content = resetViews( event.content );
   133 		var clipboard,
   109 		} );
   134 			dom = editor.dom;
   110 
   135 
   111 		// Replace views with their text inside undo levels.
   136 		if ( selected ) {
   112 		// This also prevents that new levels are added when there are changes inside the views.
   137 			clipboard = editor.dom.select( '.wpview-clipboard', selected )[0];
   113 		editor.on( 'beforeaddundo', function( event ) {
   138 			dom.unbind( clipboard );
   114 			event.level.content = resetViews( event.level.content );
   139 			dom.remove( clipboard );
   115 		} );
   140 
   116 
   141 			dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop );
   117 		// Make sure views are copied as their text.
   142 			dom.setAttrib( selected, 'data-mce-selected', null );
   118 		editor.on( 'drop objectselected', function( event ) {
   143 		}
   119 			if ( isView( event.targetClone ) ) {
   144 
   120 				event.targetClone = editor.getDoc().createTextNode(
   145 		selected = null;
   121 					window.decodeURIComponent( editor.dom.getAttrib( event.targetClone, 'data-wpview-text' ) )
   146 	}
   122 				);
   147 
   123 			}
   148 	// Check if the `wp.mce` API exists.
   124 		} );
   149 	if ( typeof wp === 'undefined' || ! wp.mce ) {
   125 
       
   126 		// Clean up URLs for easier processing.
       
   127 		editor.on( 'pastepreprocess', function( event ) {
       
   128 			var content = event.content;
       
   129 
       
   130 			if ( content ) {
       
   131 				content = tinymce.trim( content.replace( /<[^>]+>/g, '' ) );
       
   132 
       
   133 				if ( /^https?:\/\/\S+$/i.test( content ) ) {
       
   134 					event.content = content;
       
   135 				}
       
   136 			}
       
   137 		} );
       
   138 
       
   139 		// Show the view type in the element path.
       
   140 		editor.on( 'resolvename', function( event ) {
       
   141 			if ( isView( event.target ) ) {
       
   142 				event.name = editor.dom.getAttrib( event.target, 'data-wpview-type' ) || 'object';
       
   143 			}
       
   144 		} );
       
   145 
       
   146 		// See `media` plugin.
       
   147 		editor.on( 'click keyup', function() {
       
   148 			var node = editor.selection.getNode();
       
   149 
       
   150 			if ( isView( node ) ) {
       
   151 				if ( editor.dom.getAttrib( node, 'data-mce-selected' ) ) {
       
   152 					node.setAttribute( 'data-mce-selected', '2' );
       
   153 				}
       
   154 			}
       
   155 		} );
       
   156 
       
   157 		editor.addButton( 'wp_view_edit', {
       
   158 			tooltip: 'Edit|button', // '|button' is not displayed, only used for context
       
   159 			icon: 'dashicon dashicons-edit',
       
   160 			onclick: function() {
       
   161 				var node = editor.selection.getNode();
       
   162 
       
   163 				if ( isView( node ) ) {
       
   164 					wp.mce.views.edit( editor, node );
       
   165 				}
       
   166 			}
       
   167 		} );
       
   168 
       
   169 		editor.addButton( 'wp_view_remove', {
       
   170 			tooltip: 'Remove',
       
   171 			icon: 'dashicon dashicons-no',
       
   172 			onclick: function() {
       
   173 				editor.fire( 'cut' );
       
   174 			}
       
   175 		} );
       
   176 
       
   177 		editor.once( 'preinit', function() {
       
   178 			var toolbar;
       
   179 
       
   180 			if ( editor.wp && editor.wp._createToolbar ) {
       
   181 				toolbar = editor.wp._createToolbar( [
       
   182 					'wp_view_edit',
       
   183 					'wp_view_remove'
       
   184 				] );
       
   185 
       
   186 				editor.on( 'wptoolbar', function( event ) {
       
   187 					if ( ! event.collapsed && isView( event.element ) ) {
       
   188 						event.toolbar = toolbar;
       
   189 					}
       
   190 				} );
       
   191 			}
       
   192 		} );
       
   193 
       
   194 		editor.wp = editor.wp || {};
       
   195 		editor.wp.getView = noop;
       
   196 		editor.wp.setViewCursor = noop;
       
   197 
   150 		return {
   198 		return {
   151 			getView: _noop
   199 			getView: noop
   152 		};
   200 		};
   153 	}
       
   154 
       
   155 	// Remove the content of view wrappers from HTML string
       
   156 	function emptyViews( content ) {
       
   157 		content = content.replace( /<div[^>]+data-wpview-text="([^"]+)"[^>]*>[\s\S]+?wpview-selection-after[^>]+>[^<>]*<\/p>\s*<\/div>/g, function( all, match ) {
       
   158 			return '<p>' + window.decodeURIComponent( match ) + '</p>';
       
   159 		});
       
   160 
       
   161 		return content.replace( / data-wpview-marker="[^"]+"/g, '' );
       
   162 	}
       
   163 
       
   164 	// Prevent adding undo levels on changes inside a view wrapper
       
   165 	editor.on( 'BeforeAddUndo', function( event ) {
       
   166 		if ( event.level.content ) {
       
   167 			event.level.content = emptyViews( event.level.content );
       
   168 		}
       
   169 	});
       
   170 
       
   171 	// When the editor's content changes, scan the new content for
       
   172 	// matching view patterns, and transform the matches into
       
   173 	// view wrappers.
       
   174 	editor.on( 'BeforeSetContent', function( event ) {
       
   175 		var node;
       
   176 
       
   177 		if ( ! event.selection ) {
       
   178 			wp.mce.views.unbind();
       
   179 		}
       
   180 
       
   181 		if ( ! event.content ) {
       
   182 			return;
       
   183 		}
       
   184 
       
   185 		if ( ! event.load ) {
       
   186 			if ( selected ) {
       
   187 				removeView( selected );
       
   188 			}
       
   189 
       
   190 			node = editor.selection.getNode();
       
   191 
       
   192 			if ( node && node !== editor.getBody() && /^\s*https?:\/\/\S+\s*$/i.test( event.content ) ) {
       
   193 				// When a url is pasted or inserted, only try to embed it when it is in an empty paragrapgh.
       
   194 				node = editor.dom.getParent( node, 'p' );
       
   195 
       
   196 				if ( node && /^[\s\uFEFF\u00A0]*$/.test( $( node ).text() || '' ) ) {
       
   197 					// Make sure there are no empty inline elements in the <p>
       
   198 					node.innerHTML = '';
       
   199 				} else {
       
   200 					return;
       
   201 				}
       
   202 			}
       
   203 		}
       
   204 
       
   205 		event.content = wp.mce.views.setMarkers( event.content );
       
   206 	});
       
   207 
       
   208 	// When pasting strip all tags and check if the string is an URL.
       
   209 	// Then replace the pasted content with the cleaned URL.
       
   210 	editor.on( 'pastePreProcess', function( event ) {
       
   211 		var pastedStr = event.content;
       
   212 
       
   213 		if ( pastedStr ) {
       
   214 			pastedStr = tinymce.trim( pastedStr.replace( /<[^>]+>/g, '' ) );
       
   215 
       
   216 			if ( /^https?:\/\/\S+$/i.test( pastedStr ) ) {
       
   217 				event.content = pastedStr;
       
   218 			}
       
   219 		}
       
   220 	});
       
   221 
       
   222 	// When the editor's content has been updated and the DOM has been
       
   223 	// processed, render the views in the document.
       
   224 	editor.on( 'SetContent', function() {
       
   225 		wp.mce.views.render();
       
   226 	});
       
   227 
       
   228 	// Set the cursor before or after a view when clicking next to it.
       
   229 	editor.on( 'click', function( event ) {
       
   230 		var x = event.clientX,
       
   231 			y = event.clientY,
       
   232 			body = editor.getBody(),
       
   233 			bodyRect = body.getBoundingClientRect(),
       
   234 			first = body.firstChild,
       
   235 			last = body.lastChild,
       
   236 			firstRect, lastRect, view;
       
   237 
       
   238 		if ( ! first || ! last ) {
       
   239 			return;
       
   240 		}
       
   241 
       
   242 		firstRect = first.getBoundingClientRect();
       
   243 		lastRect = last.getBoundingClientRect();
       
   244 
       
   245 		if ( y < firstRect.top && ( view = getView( first ) ) ) {
       
   246 			setViewCursor( true, view );
       
   247 			event.preventDefault();
       
   248 		} else if ( y > lastRect.bottom && ( view = getView( last ) ) ) {
       
   249 			setViewCursor( false, view );
       
   250 			event.preventDefault();
       
   251 		} else if ( x < bodyRect.left || x > bodyRect.right ) {
       
   252 			tinymce.each( editor.dom.select( '.wpview-wrap' ), function( view ) {
       
   253 				var rect = view.getBoundingClientRect();
       
   254 
       
   255 				if ( y < rect.top ) {
       
   256 					return false;
       
   257 				}
       
   258 
       
   259 				if ( y >= rect.top && y <= rect.bottom ) {
       
   260 					if ( x < bodyRect.left ) {
       
   261 						setViewCursor( true, view );
       
   262 						event.preventDefault();
       
   263 					} else if ( x > bodyRect.right ) {
       
   264 						setViewCursor( false, view );
       
   265 						event.preventDefault();
       
   266 					}
       
   267 
       
   268 					return false;
       
   269 				}
       
   270 			});
       
   271 		}
       
   272 	});
       
   273 
       
   274 	editor.on( 'init', function() {
       
   275 		var scrolled = false,
       
   276 			selection = editor.selection,
       
   277 			MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
       
   278 
       
   279 		// When a view is selected, ensure content that is being pasted
       
   280 		// or inserted is added to a text node (instead of the view).
       
   281 		editor.on( 'BeforeSetContent', function() {
       
   282 			var walker, target,
       
   283 				view = getView( selection.getNode() );
       
   284 
       
   285 			// If the selection is not within a view, bail.
       
   286 			if ( ! view ) {
       
   287 				return;
       
   288 			}
       
   289 
       
   290 			if ( ! view.nextSibling || getView( view.nextSibling ) ) {
       
   291 				// If there are no additional nodes or the next node is a
       
   292 				// view, create a text node after the current view.
       
   293 				target = editor.getDoc().createTextNode('');
       
   294 				editor.dom.insertAfter( target, view );
       
   295 			} else {
       
   296 				// Otherwise, find the next text node.
       
   297 				walker = new TreeWalker( view.nextSibling, view.nextSibling );
       
   298 				target = walker.next();
       
   299 			}
       
   300 
       
   301 			// Select the `target` text node.
       
   302 			selection.select( target );
       
   303 			selection.collapse( true );
       
   304 		});
       
   305 
       
   306 		editor.dom.bind( editor.getDoc(), 'touchmove', function() {
       
   307 			scrolled = true;
       
   308 		});
       
   309 
       
   310 		editor.on( 'mousedown mouseup click touchend', function( event ) {
       
   311 			var view = getView( event.target );
       
   312 
       
   313 			firstFocus = false;
       
   314 
       
   315 			// Contain clicks inside the view wrapper
       
   316 			if ( view ) {
       
   317 				event.stopImmediatePropagation();
       
   318 				event.preventDefault();
       
   319 
       
   320 				if ( event.type === 'touchend' && scrolled ) {
       
   321 					scrolled = false;
       
   322 				} else {
       
   323 					select( view );
       
   324 				}
       
   325 
       
   326 				// Returning false stops the ugly bars from appearing in IE11 and stops the view being selected as a range in FF.
       
   327 				// Unfortunately, it also inhibits the dragging of views to a new location.
       
   328 				return false;
       
   329 			} else {
       
   330 				if ( event.type === 'touchend' || event.type === 'mousedown' ) {
       
   331 					deselect();
       
   332 				}
       
   333 			}
       
   334 
       
   335 			if ( event.type === 'touchend' && scrolled ) {
       
   336 				scrolled = false;
       
   337 			}
       
   338 		}, true );
       
   339 
       
   340 		if ( MutationObserver ) {
       
   341 			new MutationObserver( function() {
       
   342 				editor.fire( 'wp-body-class-change' );
       
   343 			} )
       
   344 			.observe( editor.getBody(), {
       
   345 				attributes: true,
       
   346 				attributeFilter: ['class']
       
   347 			} );
       
   348 		}
       
   349 	});
       
   350 
       
   351 	function resetViews( rootNode ) {
       
   352 		// Replace view nodes
       
   353 		$( 'div[data-wpview-text]', rootNode ).each( function( i, node ) {
       
   354 			var $node = $( node ),
       
   355 				text = window.decodeURIComponent( $node.attr( 'data-wpview-text' ) || '' );
       
   356 
       
   357 			if ( text && node.parentNode ) {
       
   358 				$node.replaceWith( $( editor.dom.create('p') ).text( text ) );
       
   359 			}
       
   360 		});
       
   361 
       
   362 		// Remove marker attributes
       
   363 		$( 'p[data-wpview-marker]', rootNode ).attr( 'data-wpview-marker', null );
       
   364 	}
       
   365 
       
   366 	editor.on( 'PreProcess', function( event ) {
       
   367 		// Replace the view nodes with their text in the DOM clone.
       
   368 		resetViews( event.node );
       
   369 	}, true );
       
   370 
       
   371 	editor.on( 'hide', function() {
       
   372 		// Replace the view nodes with their text directly in the editor body.
       
   373 		wp.mce.views.unbind();
       
   374 		deselect();
       
   375 		resetViews( editor.getBody() );
       
   376 	});
       
   377 
       
   378 	// Excludes arrow keys, delete, backspace, enter, space bar.
       
   379 	// Ref: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode
       
   380 	function isSpecialKey( key ) {
       
   381 		return ( ( key <= 47 && key !== VK.SPACEBAR && key !== VK.ENTER && key !== VK.DELETE && key !== VK.BACKSPACE && ( key < 37 || key > 40 ) ) ||
       
   382 			key >= 224 || // OEM or non-printable
       
   383 			( key >= 144 && key <= 150 ) || // Num Lock, Scroll Lock, OEM
       
   384 			( key >= 91 && key <= 93 ) || // Windows keys
       
   385 			( key >= 112 && key <= 135 ) ); // F keys
       
   386 	}
       
   387 
       
   388 	// (De)select views when arrow keys are used to navigate the content of the editor.
       
   389 	editor.on( 'keydown', function( event ) {
       
   390 		var key = event.keyCode,
       
   391 			dom = editor.dom,
       
   392 			selection = editor.selection,
       
   393 			node, view, cursorBefore, cursorAfter,
       
   394 			range, clonedRange, tempRange;
       
   395 
       
   396 		if ( selected ) {
       
   397 			// Ignore key presses that involve the command or control key, but continue when in combination with backspace or v.
       
   398 			// Also ignore the F# keys.
       
   399 			if ( ( ( event.metaKey || event.ctrlKey ) && key !== VK.BACKSPACE && key !== 86 ) || ( key >= 112 && key <= 123 ) ) {
       
   400 				// Remove the view when pressing cmd/ctrl+x on keyup, otherwise the browser can't copy the content.
       
   401 				if ( ( event.metaKey || event.ctrlKey ) && key === 88 ) {
       
   402 					toRemove = selected;
       
   403 				}
       
   404 				return;
       
   405 			}
       
   406 
       
   407 			view = getView( selection.getNode() );
       
   408 
       
   409 			// If the caret is not within the selected view, deselect the view and bail.
       
   410 			if ( view !== selected ) {
       
   411 				deselect();
       
   412 				return;
       
   413 			}
       
   414 
       
   415 			if ( key === VK.LEFT ) {
       
   416 				setViewCursor( true, view );
       
   417 				event.preventDefault();
       
   418 			} else if ( key === VK.UP ) {
       
   419 				if ( view.previousSibling ) {
       
   420 					if ( getView( view.previousSibling ) ) {
       
   421 						setViewCursor( true, view.previousSibling );
       
   422 					} else {
       
   423 						deselect();
       
   424 						selection.select( view.previousSibling, true );
       
   425 						selection.collapse();
       
   426 					}
       
   427 				} else {
       
   428 					setViewCursor( true, view );
       
   429 				}
       
   430 				event.preventDefault();
       
   431 			} else if ( key === VK.RIGHT ) {
       
   432 				setViewCursor( false, view );
       
   433 				event.preventDefault();
       
   434 			} else if ( key === VK.DOWN ) {
       
   435 				if ( view.nextSibling ) {
       
   436 					if ( getView( view.nextSibling ) ) {
       
   437 						setViewCursor( false, view.nextSibling );
       
   438 					} else {
       
   439 						deselect();
       
   440 						selection.setCursorLocation( view.nextSibling, 0 );
       
   441 					}
       
   442 				} else {
       
   443 					setViewCursor( false, view );
       
   444 				}
       
   445 
       
   446 				event.preventDefault();
       
   447 			// Ignore keys that don't insert anything.
       
   448 			} else if ( ! isSpecialKey( key ) ) {
       
   449 				removeView( selected );
       
   450 
       
   451 				if ( key === VK.ENTER || key === VK.DELETE || key === VK.BACKSPACE ) {
       
   452 					event.preventDefault();
       
   453 				}
       
   454 			}
       
   455 		} else {
       
   456 			if ( event.metaKey || event.ctrlKey || ( key >= 112 && key <= 123 ) ) {
       
   457 				return;
       
   458 			}
       
   459 
       
   460 			node = selection.getNode();
       
   461 			lastKeyDownNode = node;
       
   462 			view = getView( node );
       
   463 
       
   464 			// Make sure we don't delete part of a view.
       
   465 			// If the range ends or starts with the view, we'll need to trim it.
       
   466 			if ( ! selection.isCollapsed() ) {
       
   467 				range = selection.getRng();
       
   468 
       
   469 				if ( view = getView( range.endContainer ) ) {
       
   470 					clonedRange = range.cloneRange();
       
   471 					selection.select( view.previousSibling, true );
       
   472 					selection.collapse();
       
   473 					tempRange = selection.getRng();
       
   474 					clonedRange.setEnd( tempRange.endContainer, tempRange.endOffset );
       
   475 					selection.setRng( clonedRange );
       
   476 				} else if ( view = getView( range.startContainer ) ) {
       
   477 					clonedRange = range.cloneRange();
       
   478 					clonedRange.setStart( view.nextSibling, 0 );
       
   479 					selection.setRng( clonedRange );
       
   480 				}
       
   481 			}
       
   482 
       
   483 			if ( ! view ) {
       
   484 				// Make sure we don't eat any content.
       
   485 				if ( event.keyCode === VK.BACKSPACE ) {
       
   486 					if ( editor.dom.isEmpty( node ) ) {
       
   487 						if ( view = getView( node.previousSibling ) ) {
       
   488 							setViewCursor( false, view );
       
   489 							editor.dom.remove( node );
       
   490 							event.preventDefault();
       
   491 						}
       
   492 					} else if ( ( range = selection.getRng() ) &&
       
   493 							range.startOffset === 0 &&
       
   494 							range.endOffset === 0 &&
       
   495 							( view = getView( node.previousSibling ) ) ) {
       
   496 						setViewCursor( false, view );
       
   497 						event.preventDefault();
       
   498 					}
       
   499 				}
       
   500 				return;
       
   501 			}
       
   502 
       
   503 			if ( ! ( ( cursorBefore = dom.hasClass( view, 'wpview-selection-before' ) ) ||
       
   504 					( cursorAfter = dom.hasClass( view, 'wpview-selection-after' ) ) ) ) {
       
   505 				return;
       
   506 			}
       
   507 
       
   508 			if ( isSpecialKey( key ) ) {
       
   509 				// ignore
       
   510 				return;
       
   511 			}
       
   512 
       
   513 			if ( ( cursorAfter && key === VK.UP ) || ( cursorBefore && key === VK.BACKSPACE ) ) {
       
   514 				if ( view.previousSibling ) {
       
   515 					if ( getView( view.previousSibling ) ) {
       
   516 						setViewCursor( false, view.previousSibling );
       
   517 					} else {
       
   518 						if ( dom.isEmpty( view.previousSibling ) && key === VK.BACKSPACE ) {
       
   519 							dom.remove( view.previousSibling );
       
   520 						} else {
       
   521 							selection.select( view.previousSibling, true );
       
   522 							selection.collapse();
       
   523 						}
       
   524 					}
       
   525 				} else {
       
   526 					setViewCursor( true, view );
       
   527 				}
       
   528 				event.preventDefault();
       
   529 			} else if ( cursorAfter && ( key === VK.DOWN || key === VK.RIGHT ) ) {
       
   530 				if ( view.nextSibling ) {
       
   531 					if ( getView( view.nextSibling ) ) {
       
   532 						setViewCursor( key === VK.RIGHT, view.nextSibling );
       
   533 					} else {
       
   534 						selection.setCursorLocation( view.nextSibling, 0 );
       
   535 					}
       
   536 				}
       
   537 				event.preventDefault();
       
   538 			} else if ( cursorBefore && ( key === VK.UP || key ===  VK.LEFT ) ) {
       
   539 				if ( view.previousSibling ) {
       
   540 					if ( getView( view.previousSibling ) ) {
       
   541 						setViewCursor( key === VK.UP, view.previousSibling );
       
   542 					} else {
       
   543 						selection.select( view.previousSibling, true );
       
   544 						selection.collapse();
       
   545 					}
       
   546 				}
       
   547 				event.preventDefault();
       
   548 			} else if ( cursorBefore && key === VK.DOWN ) {
       
   549 				if ( view.nextSibling ) {
       
   550 					if ( getView( view.nextSibling ) ) {
       
   551 						setViewCursor( true, view.nextSibling );
       
   552 					} else {
       
   553 						selection.setCursorLocation( view.nextSibling, 0 );
       
   554 					}
       
   555 				} else {
       
   556 					setViewCursor( false, view );
       
   557 				}
       
   558 				event.preventDefault();
       
   559 			} else if ( ( cursorAfter && key === VK.LEFT ) || ( cursorBefore && key === VK.RIGHT ) ) {
       
   560 				select( view );
       
   561 				event.preventDefault();
       
   562 			} else if ( cursorAfter && key === VK.BACKSPACE ) {
       
   563 				removeView( view );
       
   564 				event.preventDefault();
       
   565 			} else if ( cursorAfter ) {
       
   566 				handleEnter( view );
       
   567 			} else if ( cursorBefore ) {
       
   568 				handleEnter( view , true, key );
       
   569 			}
       
   570 
       
   571 			if ( key === VK.ENTER ) {
       
   572 				event.preventDefault();
       
   573 			}
       
   574 		}
       
   575 	});
       
   576 
       
   577 	editor.on( 'keyup', function() {
       
   578 		if ( toRemove ) {
       
   579 			removeView( toRemove );
       
   580 			toRemove = false;
       
   581 		}
       
   582 	});
       
   583 
       
   584 	editor.on( 'focus', function() {
       
   585 		var view;
       
   586 
       
   587 		focus = true;
       
   588 		editor.dom.addClass( editor.getBody(), 'has-focus' );
       
   589 
       
   590 		// Edge case: show the fake caret when the editor is focused for the first time
       
   591 		// and the first element is a view.
       
   592 		if ( firstFocus && ( view = getView( editor.getBody().firstChild ) ) ) {
       
   593 			setViewCursor( true, view );
       
   594 		}
       
   595 
       
   596 		firstFocus = false;
       
   597 	} );
   201 	} );
   598 
   202 } )( window.tinymce, window.wp );
   599 	editor.on( 'blur', function() {
       
   600 		focus = false;
       
   601 		editor.dom.removeClass( editor.getBody(), 'has-focus' );
       
   602 	} );
       
   603 
       
   604 	editor.on( 'NodeChange', function( event ) {
       
   605 		var dom = editor.dom,
       
   606 			views = editor.dom.select( '.wpview-wrap' ),
       
   607 			className = event.element.className,
       
   608 			view = getView( event.element ),
       
   609 			lKDN = lastKeyDownNode;
       
   610 
       
   611 		lastKeyDownNode = false;
       
   612 
       
   613 		clearInterval( cursorInterval );
       
   614 
       
   615 		// This runs a lot and is faster than replacing each class separately
       
   616 		tinymce.each( views, function ( view ) {
       
   617 			if ( view.className ) {
       
   618 				view.className = view.className.replace( / ?\bwpview-(?:selection-before|selection-after|cursor-hide)\b/g, '' );
       
   619 			}
       
   620 		});
       
   621 
       
   622 		if ( focus && view ) {
       
   623 			if ( ( className === 'wpview-selection-before' || className === 'wpview-selection-after' ) &&
       
   624 				editor.selection.isCollapsed() ) {
       
   625 
       
   626 				setViewCursorTries = 0;
       
   627 
       
   628 				deselect();
       
   629 
       
   630 				// Make sure the cursor arrived in the right node.
       
   631 				// This is necessary for Firefox.
       
   632 				if ( lKDN === view.previousSibling ) {
       
   633 					setViewCursor( true, view );
       
   634 					return;
       
   635 				} else if ( lKDN === view.nextSibling ) {
       
   636 					setViewCursor( false, view );
       
   637 					return;
       
   638 				}
       
   639 
       
   640 				dom.addClass( view, className );
       
   641 
       
   642 				cursorInterval = setInterval( function() {
       
   643 					if ( dom.hasClass( view, 'wpview-cursor-hide' ) ) {
       
   644 						dom.removeClass( view, 'wpview-cursor-hide' );
       
   645 					} else {
       
   646 						dom.addClass( view, 'wpview-cursor-hide' );
       
   647 					}
       
   648 				}, 500 );
       
   649 			// If the cursor lands anywhere else in the view, set the cursor before it.
       
   650 			// Only try this once to prevent a loop. (You never know.)
       
   651 			} else if ( ! getParent( event.element, 'wpview-clipboard' ) && ! setViewCursorTries ) {
       
   652 				deselect();
       
   653 				setViewCursorTries++;
       
   654 				setViewCursor( true, view );
       
   655 			}
       
   656 		}
       
   657 	});
       
   658 
       
   659 	editor.on( 'BeforeExecCommand', function() {
       
   660 		var node = editor.selection.getNode(),
       
   661 			view;
       
   662 
       
   663 		if ( node && ( ( execCommandBefore = node.className === 'wpview-selection-before' ) || node.className === 'wpview-selection-after' ) && ( view = getView( node ) ) ) {
       
   664 			handleEnter( view, execCommandBefore );
       
   665 			execCommandView = view;
       
   666 		}
       
   667 	});
       
   668 
       
   669 	editor.on( 'ExecCommand', function() {
       
   670 		var toSelect, node;
       
   671 
       
   672 		if ( selected ) {
       
   673 			toSelect = selected;
       
   674 			deselect();
       
   675 			select( toSelect );
       
   676 		}
       
   677 
       
   678 		if ( execCommandView ) {
       
   679 			node = execCommandView[ execCommandBefore ? 'previousSibling' : 'nextSibling' ];
       
   680 
       
   681 			if ( node && node.nodeName === 'P' && editor.dom.isEmpty( node ) ) {
       
   682 				editor.dom.remove( node );
       
   683 				setViewCursor( execCommandBefore, execCommandView );
       
   684 			}
       
   685 
       
   686 			execCommandView = false;
       
   687 		}
       
   688 	});
       
   689 
       
   690 	editor.on( 'ResolveName', function( event ) {
       
   691 		if ( editor.dom.hasClass( event.target, 'wpview-wrap' ) ) {
       
   692 			event.name = editor.dom.getAttrib( event.target, 'data-wpview-type' ) || 'wpview';
       
   693 			event.stopPropagation();
       
   694 		} else if ( getView( event.target ) ) {
       
   695 			event.preventDefault();
       
   696 			event.stopPropagation();
       
   697 		}
       
   698 	});
       
   699 
       
   700 	editor.addButton( 'wp_view_edit', {
       
   701 		tooltip: 'Edit ', // trailing space is needed, used for context
       
   702 		icon: 'dashicon dashicons-edit',
       
   703 		onclick: function() {
       
   704 			selected && wp.mce.views.edit( editor, selected );
       
   705 		}
       
   706 	} );
       
   707 
       
   708 	editor.addButton( 'wp_view_remove', {
       
   709 		tooltip: 'Remove',
       
   710 		icon: 'dashicon dashicons-no',
       
   711 		onclick: function() {
       
   712 			selected && removeView( selected );
       
   713 		}
       
   714 	} );
       
   715 
       
   716 	editor.once( 'preinit', function() {
       
   717 		toolbar = editor.wp._createToolbar( [
       
   718 			'wp_view_edit',
       
   719 			'wp_view_remove'
       
   720 		] );
       
   721 	} );
       
   722 
       
   723 	editor.on( 'wptoolbar', function( event ) {
       
   724 		if ( selected ) {
       
   725 			event.element = selected;
       
   726 			event.toolbar = toolbar;
       
   727 		}
       
   728 	} );
       
   729 
       
   730 	// Add to editor.wp
       
   731 	editor.wp = editor.wp || {};
       
   732 	editor.wp.getView = getView;
       
   733 
       
   734 	// Keep for back-compat.
       
   735 	return {
       
   736 		getView: getView
       
   737 	};
       
   738 });