Path: blob/trunk/third_party/closure/goog/editor/plugins/enterhandler.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Plugin to handle enter keys.16*17* @author [email protected] (Robby Walker)18*/1920goog.provide('goog.editor.plugins.EnterHandler');2122goog.require('goog.dom');23goog.require('goog.dom.NodeOffset');24goog.require('goog.dom.NodeType');25goog.require('goog.dom.Range');26goog.require('goog.dom.TagName');27goog.require('goog.editor.BrowserFeature');28goog.require('goog.editor.Plugin');29goog.require('goog.editor.node');30goog.require('goog.editor.plugins.Blockquote');31goog.require('goog.editor.range');32goog.require('goog.editor.style');33goog.require('goog.events.KeyCodes');34goog.require('goog.functions');35goog.require('goog.object');36goog.require('goog.string');37goog.require('goog.userAgent');38394041/**42* Plugin to handle enter keys. This does all the crazy to normalize (as much as43* is reasonable) what happens when you hit enter. This also handles the44* special casing of hitting enter in a blockquote.45*46* In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In47* Firefox, the resulting HTML uses BR tags at the end of each line.48*49* @constructor50* @extends {goog.editor.Plugin}51*/52goog.editor.plugins.EnterHandler = function() {53goog.editor.Plugin.call(this);54};55goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);565758/**59* The type of block level tag to add on enter, for browsers that support60* specifying the default block-level tag. Can be overriden by subclasses; must61* be either DIV or P.62* @type {!goog.dom.TagName}63* @protected64*/65goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;666768/** @override */69goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {70return 'EnterHandler';71};727374/** @override */75goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {76goog.editor.plugins.EnterHandler.base(this, 'enable', fieldObject);7778if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&79(this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {80var doc = this.getFieldDomHelper().getDocument();81doc.execCommand('opera-defaultBlock', false, this.tag);82}83};848586/**87* If the contents are empty, return the 'default' html for the field.88* The 'default' contents depend on the enter handling mode, so it89* makes the most sense in this plugin.90* @param {string} html The html to prepare.91* @return {string} The original HTML, or default contents if that92* html is empty.93* @override94*/95goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(96html) {97if (!html || goog.string.isBreakingWhitespace(html)) {98return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?99this.getNonCollapsingBlankHtml() :100'';101}102return html;103};104105106/**107* Gets HTML with no contents that won't collapse, for browsers that108* collapse the empty string.109* @return {string} Blank html.110* @protected111*/112goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =113goog.functions.constant('<br>');114115116/**117* Internal backspace handler.118* @param {goog.events.Event} e The keypress event.119* @param {goog.dom.AbstractRange} range The closure range object.120* @protected121*/122goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(123e, range) {124var field = this.getFieldObject().getElement();125var container = range && range.getStartNode();126127if (field.firstChild == container && goog.editor.node.isEmpty(container)) {128e.preventDefault();129// TODO(user): I think we probably don't need to stopPropagation here130e.stopPropagation();131}132};133134135/**136* Fix paragraphs to be the correct type of node.137* @param {goog.events.Event} e The `<enter>` key event.138* @param {boolean} split Whether we already split up a blockquote by139* manually inserting elements.140* @protected141*/142goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =143function(e, split) {144// Force IE to turn the node we are leaving into a DIV. If we do turn145// it into a DIV, the node IE creates in response to ENTER will also be146// a DIV. If we don't, it will be a P. We handle that case147// in handleKeyUpIE_148if (goog.userAgent.IE || goog.userAgent.OPERA) {149this.ensureBlockIeOpera(goog.dom.TagName.DIV);150} else if (!split && goog.userAgent.WEBKIT) {151// WebKit duplicates a blockquote when the user hits enter. Let's cancel152// this and insert a BR instead, to make it more consistent with the other153// browsers.154var range = this.getFieldObject().getRange();155if (!range ||156!goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(157range.getContainerElement())) {158return;159}160161var dh = this.getFieldDomHelper();162var br = dh.createElement(goog.dom.TagName.BR);163range.insertNode(br, true);164165// If the BR is at the end of a block element, Safari still thinks there is166// only one line instead of two, so we need to add another BR in that case.167if (goog.editor.node.isBlockTag(br.parentNode) &&168!goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {169goog.dom.insertSiblingBefore(dh.createElement(goog.dom.TagName.BR), br);170}171172goog.editor.range.placeCursorNextTo(br, false);173e.preventDefault();174}175};176177178/**179* Determines whether the lowest containing block node is a blockquote.180* @param {Node} n The node.181* @return {boolean} Whether the deepest block ancestor of n is a blockquote.182*/183goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {184for (var current = n; current; current = current.parentNode) {185if (goog.editor.node.isBlockTag(current)) {186return /** @type {!Element} */ (current).tagName ==187goog.dom.TagName.BLOCKQUOTE;188}189}190191return false;192};193194195/**196* Internal delete key handler.197* @param {goog.events.Event} e The keypress event.198* @protected199*/200goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {201this.deleteBrGecko(e);202};203204205/**206* Deletes the element at the cursor if it is a BR node, and if it does, calls207* e.preventDefault to stop the browser from deleting. Only necessary in Gecko208* as a workaround for mozilla bug 205350 where deleting a BR that is followed209* by a block element doesn't work (the BR gets immediately replaced). We also210* need to account for an ill-formed cursor which occurs from us trying to211* stop the browser from deleting.212*213* @param {goog.events.Event} e The DELETE keypress event.214* @protected215*/216goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {217var range = this.getFieldObject().getRange();218if (range.isCollapsed()) {219var container = range.getEndNode();220if (container.nodeType == goog.dom.NodeType.ELEMENT) {221var nextNode = container.childNodes[range.getEndOffset()];222if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {223// We want to retrieve the first non-whitespace previous sibling224// as we could have added an empty text node below and want to225// properly handle deleting a sequence of BR's.226var previousSibling = goog.editor.node.getPreviousSibling(nextNode);227var nextSibling = nextNode.nextSibling;228229container.removeChild(nextNode);230e.preventDefault();231232// When we delete a BR followed by a block level element, the cursor233// has a line-height which spans the height of the block level element.234// e.g. If we delete a BR followed by a UL, the resulting HTML will235// appear to the end user like:-236//237// | * one238// | * two239// | * three240//241// There are a couple of cases that we have to account for in order to242// properly conform to what the user expects when DELETE is pressed.243//244// 1. If the BR has a previous sibling and the previous sibling is245// not a block level element or a BR, we place the cursor at the246// end of that.247// 2. If the BR doesn't have a previous sibling or the previous sibling248// is a block level element or a BR, we place the cursor at the249// beginning of the leftmost leaf of its next sibling.250if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {251if (previousSibling &&252!(previousSibling.tagName == goog.dom.TagName.BR ||253goog.editor.node.isBlockTag(previousSibling))) {254goog.dom.Range255.createCaret(256previousSibling,257goog.editor.node.getLength(previousSibling))258.select();259} else {260var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);261goog.dom.Range.createCaret(leftMostLeaf, 0).select();262}263}264}265}266}267};268269270/** @override */271goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {272// If a dialog doesn't have selectable field, Gecko grabs the event and273// performs actions in editor window. This solves that problem and allows274// the event to be passed on to proper handlers.275if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {276return false;277}278279// Firefox will allow the first node in an iframe to be deleted280// on a backspace. Disallow it if the node is empty.281if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {282this.handleBackspaceInternal(e, this.getFieldObject().getRange());283284} else if (e.keyCode == goog.events.KeyCodes.ENTER) {285if (goog.userAgent.GECKO) {286if (!e.shiftKey) {287// Behave similarly to IE's content editable return carriage:288// If the shift key is down or specified by the application, insert a289// BR, otherwise split paragraphs290this.handleEnterGecko_(e);291}292} else {293// In Gecko-based browsers, this is handled in the handleEnterGecko_294// method.295this.getFieldObject().dispatchBeforeChange();296var cursorPosition = this.deleteCursorSelection_();297298var split = !!this.getFieldObject().execCommand(299goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);300if (split) {301// TODO(user): I think we probably don't need to stopPropagation here302e.preventDefault();303e.stopPropagation();304}305306this.releasePositionObject_(cursorPosition);307308if (goog.userAgent.WEBKIT) {309this.handleEnterWebkitInternal(e);310}311312this.processParagraphTagsInternal(e, split);313this.getFieldObject().dispatchChange();314}315316} else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {317this.handleDeleteGecko(e);318}319320return false;321};322323324/** @override */325goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {326// If a dialog doesn't have selectable field, Gecko grabs the event and327// performs actions in editor window. This solves that problem and allows328// the event to be passed on to proper handlers.329if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {330return false;331}332this.handleKeyUpInternal(e);333return false;334};335336337/**338* Internal handler for keyup events.339* @param {goog.events.Event} e The key event.340* @protected341*/342goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {343if ((goog.userAgent.IE || goog.userAgent.OPERA) &&344e.keyCode == goog.events.KeyCodes.ENTER) {345this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);346}347};348349350/**351* Handles an enter keypress event on fields in Gecko.352* @param {goog.events.BrowserEvent} e The key event.353* @private354*/355goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {356// Retrieve whether the selection is collapsed before we delete it.357var range = this.getFieldObject().getRange();358var wasCollapsed = !range || range.isCollapsed();359var cursorPosition = this.deleteCursorSelection_();360361var handled = this.getFieldObject().execCommand(362goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);363if (handled) {364// TODO(user): I think we probably don't need to stopPropagation here365e.preventDefault();366e.stopPropagation();367}368369this.releasePositionObject_(cursorPosition);370if (!handled) {371this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);372}373};374375376/**377* Handle an enter key press in WebKit.378* @param {goog.events.BrowserEvent} e The key press event.379* @protected380*/381goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =382goog.nullFunction;383384385/**386* Handle an enter key press on collapsed selection. handleEnterGecko_ ensures387* the selection is collapsed by deleting its contents if it is not. The388* default implementation does nothing.389* @param {goog.events.BrowserEvent} e The key press event.390* @param {boolean} wasCollapsed Whether the selection was collapsed before391* the key press. If it was not, code before this function has already392* cleared the contents of the selection.393* @param {goog.dom.AbstractRange} range Object representing the selection.394* @protected395*/396goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =397goog.nullFunction;398399400/**401* Names of all the nodes that we don't want to turn into block nodes in IE when402* the user hits enter.403* @type {Object}404* @private405*/406goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =407goog.object.createSet(408goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,409goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,410goog.dom.TagName.H5, goog.dom.TagName.H6);411412413/**414* Whether this is a node that contains a single BR tag and non-nbsp415* whitespace.416* @param {Node} node Node to check.417* @return {boolean} Whether this is an element that only contains a BR.418* @protected419*/420goog.editor.plugins.EnterHandler.isBrElem = function(node) {421return goog.editor.node.isEmpty(node) &&422goog.dom.getElementsByTagName(423goog.dom.TagName.BR, /** @type {!Element} */ (node)).length == 1;424};425426427/**428* Ensures all text in IE and Opera to be in the given tag in order to control429* Enter spacing. Call this when Enter is pressed if desired.430*431* We want to make sure the user is always inside of a block (or other nodes432* listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_). We433* listen to keypress to force nodes that the user is leaving to turn into434* blocks, but we also need to listen to keyup to force nodes that the user is435* entering to turn into blocks.436* Example: html is: `<h2>foo[cursor]</h2>`, and the user hits enter. We437* don't want to format the h2, but we do want to format the P that is438* created on enter. The P node is not available until keyup.439* @param {!goog.dom.TagName} tag The tag name to convert to.440* @param {boolean=} opt_keyUp Whether the function is being called on key up.441* When called on key up, the cursor is in the newly created node, so the442* semantics for when to change it to a block are different. Specifically,443* if the resulting node contains only a BR, it is converted to `<tag>`.444* @protected445*/446goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(447tag, opt_keyUp) {448var range = this.getFieldObject().getRange();449var container = range.getContainer();450var field = this.getFieldObject().getElement();451452var paragraph;453while (container && container != field) {454// We don't need to ensure a block if we are already in the same block, or455// in another block level node that we don't want to change the format of456// (unless we're handling keyUp and that block node just contains a BR).457var nodeName = container.nodeName;458// Due to @bug 2455389, the call to isBrElem needs to be inlined in the if459// instead of done before and saved in a variable, so that it can be460// short-circuited and avoid a weird IE edge case.461if (nodeName == tag ||462(goog.editor.plugins.EnterHandler463.DO_NOT_ENSURE_BLOCK_NODES_[nodeName] &&464!(opt_keyUp &&465goog.editor.plugins.EnterHandler.isBrElem(container)))) {466// Opera can create a <p> inside of a <div> in some situations,467// such as when breaking out of a list that is contained in a <div>.468if (goog.userAgent.OPERA && paragraph) {469if (nodeName == tag && paragraph == container.lastChild &&470goog.editor.node.isEmpty(paragraph)) {471goog.dom.insertSiblingAfter(paragraph, container);472goog.dom.Range.createFromNodeContents(paragraph).select();473}474break;475}476return;477}478if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&479nodeName != tag) {480paragraph = container;481}482483container = container.parentNode;484}485486487if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {488// IE (before IE9) has a bug where if the cursor is directly before a block489// node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),490// the FormatBlock command actually formats the "bar" instead of the "foo".491// This is just wrong. To work-around this, we want to move the492// selection back one character, and then restore it to its prior position.493// NOTE: We use the following "range math" to detect this situation because494// using Closure ranges here triggers a bug in IE that causes a crash.495// parent2 != parent3 ensures moving the cursor forward one character496// crosses at least 1 element boundary, and therefore tests if the cursor is497// at such a boundary. The second check, parent3 != range.parentElement()498// weeds out some cases where the elements are siblings instead of cousins.499var needsHelp = false;500range = range.getBrowserRangeObject();501var range2 = range.duplicate();502range2.moveEnd('character', 1);503// In whitebox mode, when the cursor is at the end of the field, trying to504// move the end of the range will do nothing, and hence the range's text505// will be empty. In this case, the cursor clearly isn't sitting just506// before a block node, since it isn't before anything.507if (range2.text.length) {508var parent2 = range2.parentElement();509510var range3 = range2.duplicate();511range3.collapse(false);512var parent3 = range3.parentElement();513514if ((needsHelp =515parent2 != parent3 && parent3 != range.parentElement())) {516range.move('character', -1);517range.select();518}519}520}521522this.getFieldObject().getEditableDomHelper().getDocument().execCommand(523'FormatBlock', false, '<' + tag + '>');524525if (needsHelp) {526range.move('character', 1);527range.select();528}529};530531532/**533* Deletes the content at the current cursor position.534* @return {!Node|!Object} Something representing the current cursor position.535* See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.536* Should be passed to releasePositionObject_ when no longer in use.537* @private538*/539goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {540return goog.editor.BrowserFeature.HAS_W3C_RANGES ?541this.deleteCursorSelectionW3C_() :542this.deleteCursorSelectionIE_();543};544545546/**547* Releases the object returned by deleteCursorSelection_.548* @param {Node|Object} position The object returned by deleteCursorSelection_.549* @private550*/551goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ = function(552position) {553if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {554(/** @type {Node} */ (position)).removeNode(true);555}556};557558559/**560* Delete the selection at the current cursor position, then returns a temporary561* node at the current position.562* @return {!Node} A temporary node marking the current cursor position. This563* node should eventually be removed from the DOM.564* @private565*/566goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =567function() {568var doc = this.getFieldDomHelper().getDocument();569var range = doc.selection.createRange();570571var id = goog.string.createUniqueString();572range.pasteHTML('<span id="' + id + '"></span>');573var splitNode = doc.getElementById(id);574splitNode.id = '';575return splitNode;576};577578579/**580* Delete the selection at the current cursor position, then returns the node581* at the current position.582* @return {!goog.editor.range.Point} The current cursor position. Note that583* unlike simulateEnterIE_, this should not be removed from the DOM.584* @private585*/586goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =587function() {588var range = this.getFieldObject().getRange();589590// Delete the current selection if it's is non-collapsed.591// Although this is redundant in FF, it's necessary for Safari592if (range && !range.isCollapsed()) {593var shouldDelete = true;594// Opera selects the <br> in an empty block if there is no text node595// preceding it. To preserve inline formatting when pressing [enter] inside596// an empty block, don't delete the selection if it only selects a <br> at597// the end of the block.598// TODO(user): Move this into goog.dom.Range. It should detect this state599// when creating a range from the window selection and fix it in the created600// range.601if (goog.userAgent.OPERA) {602var startNode = range.getStartNode();603var startOffset = range.getStartOffset();604if (startNode == range.getEndNode() &&605// This weeds out cases where startNode is a text node.606startNode.lastChild &&607/** @type {!Element} */ (startNode.lastChild).tagName ==608goog.dom.TagName.BR &&609// If this check is true, then endOffset is implied to be610// startOffset + 1, because the selection is not collapsed and611// it starts and ends within the same element.612startOffset == startNode.childNodes.length - 1) {613shouldDelete = false;614}615}616if (shouldDelete) {617goog.editor.plugins.EnterHandler.deleteW3cRange_(range);618}619}620621return goog.editor.range.getDeepEndPoint(range, true);622};623624625/**626* Deletes the contents of the selection from the DOM.627* @param {goog.dom.AbstractRange} range The range to remove contents from.628* @return {goog.dom.AbstractRange} The resulting range. Used for testing.629* @private630*/631goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {632if (range && !range.isCollapsed()) {633var reselect = true;634var baseNode = range.getContainerElement();635var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);636var rangeOffset = range.getStartOffset();637638// Whether the selection crosses no container boundaries.639var isInOneContainer =640goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);641642// Whether the selection ends in a container it doesn't fully select.643var isPartialEnd = !isInOneContainer &&644goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);645646// Remove The range contents, and ensure the correct content stays selected.647range.removeContents();648var node = nodeOffset.findTargetNode(baseNode);649if (node) {650range = goog.dom.Range.createCaret(node, rangeOffset);651} else {652// This occurs when the node that would have been referenced has now been653// deleted and there are no other nodes in the baseNode. Thus need to654// set the caret to the end of the base node.655range = goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);656reselect = false;657}658range.select();659660// If we just deleted everything from the container, add an nbsp661// to the container, and leave the cursor inside of it662if (isInOneContainer) {663var container = goog.editor.style.getContainer(range.getStartNode());664if (goog.editor.node.isEmpty(container, true)) {665var html = ' ';666if (goog.userAgent.OPERA && container.tagName == goog.dom.TagName.LI) {667// Don't break Opera's native break-out-of-lists behavior.668html = '<br>';669}670goog.editor.node.replaceInnerHtml(container, html);671goog.editor.range.selectNodeStart(container.firstChild);672reselect = false;673}674}675676if (isPartialEnd) {677/*678This code handles the following, where | is the cursor:679<div>a|b</div><div>c|d</div>680After removeContents, the remaining HTML is681<div>a</div><div>d</div>682which means the line break between the two divs remains. This block683moves children of the second div in to the first div to get the correct684result:685<div>ad</div>686687TODO(robbyw): Should we wrap the second div's contents in a span if they688have inline style?689*/690var rangeStart = goog.editor.style.getContainer(range.getStartNode());691var redundantContainer = goog.editor.node.getNextSibling(rangeStart);692if (rangeStart && redundantContainer) {693goog.dom.append(rangeStart, redundantContainer.childNodes);694goog.dom.removeNode(redundantContainer);695}696}697698if (reselect) {699// The contents of the original range are gone, so restore the cursor700// position at the start of where the range once was.701range = goog.dom.Range.createCaret(702nodeOffset.findTargetNode(baseNode), rangeOffset);703range.select();704}705}706707return range;708};709710711/**712* Checks whether the whole range is in a single block-level element.713* @param {goog.dom.AbstractRange} range The range to check.714* @return {boolean} Whether the whole range is in a single block-level element.715* @private716*/717goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {718// Find the block element containing the start of the selection.719var startContainer = range.getStartNode();720if (goog.editor.style.isContainer(startContainer)) {721startContainer =722startContainer.childNodes[range.getStartOffset()] || startContainer;723}724startContainer = goog.editor.style.getContainer(startContainer);725726// Find the block element containing the end of the selection.727var endContainer = range.getEndNode();728if (goog.editor.style.isContainer(endContainer)) {729endContainer =730endContainer.childNodes[range.getEndOffset()] || endContainer;731}732endContainer = goog.editor.style.getContainer(endContainer);733734// Compare the two.735return startContainer == endContainer;736};737738739/**740* Checks whether the end of the range is not at the end of a block-level741* element.742* @param {goog.dom.AbstractRange} range The range to check.743* @return {boolean} Whether the end of the range is not at the end of a744* block-level element.745* @private746*/747goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {748var endContainer = range.getEndNode();749var endOffset = range.getEndOffset();750var node = endContainer;751if (goog.editor.style.isContainer(node)) {752var child = node.childNodes[endOffset];753// Child is null when end offset is >= length, which indicates the entire754// container is selected. Otherwise, we also know the entire container755// is selected if the selection ends at a new container.756if (!child ||757child.nodeType == goog.dom.NodeType.ELEMENT &&758goog.editor.style.isContainer(child)) {759return false;760}761}762763var container = goog.editor.style.getContainer(node);764while (container != node) {765if (goog.editor.node.getNextSibling(node)) {766return true;767}768node = node.parentNode;769}770771return endOffset != goog.editor.node.getLength(endContainer);772};773774775