text: // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview Plugin to handle enter keys.
   */
  
  goog.provide('goog.editor.plugins.EnterHandler');
  
  goog.require('goog.dom');
  goog.require('goog.dom.NodeOffset');
  goog.require('goog.dom.NodeType');
  goog.require('goog.dom.Range');
  goog.require('goog.dom.TagName');
  goog.require('goog.editor.BrowserFeature');
  goog.require('goog.editor.Plugin');
  goog.require('goog.editor.node');
  goog.require('goog.editor.plugins.Blockquote');
  goog.require('goog.editor.range');
  goog.require('goog.editor.style');
  goog.require('goog.events.KeyCodes');
  goog.require('goog.functions');
  goog.require('goog.object');
  goog.require('goog.string');
  goog.require('goog.userAgent');
  
  
  
  /**
   * Plugin to handle enter keys. This does all the crazy to normalize (as much as
   * is reasonable) what happens when you hit enter. This also handles the
   * special casing of hitting enter in a blockquote.
   *
   * In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
   * Firefox, the resulting HTML uses BR tags at the end of each line.
   *
   * @constructor
   * @extends {goog.editor.Plugin}
   */
  goog.editor.plugins.EnterHandler = function() {
    goog.editor.Plugin.call(this);
  };
  goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
  
  
  /**
   * The type of block level tag to add on enter, for browsers that support
   * specifying the default block-level tag. Can be overriden by subclasses; must
   * be either DIV or P.
   * @type {!goog.dom.TagName}
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
  
  
  /** @override */
  goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
    return 'EnterHandler';
  };
  
  
  /** @override */
  goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
    goog.editor.plugins.EnterHandler.base(this, 'enable', fieldObject);
  
    if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
        (this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
      var doc = this.getFieldDomHelper().getDocument();
      doc.execCommand('opera-defaultBlock', false, this.tag);
    }
  };
  
  
  /**
   * If the contents are empty, return the 'default' html for the field.
   * The 'default' contents depend on the enter handling mode, so it
   * makes the most sense in this plugin.
   * @param {string} html The html to prepare.
   * @return {string} The original HTML, or default contents if that
   *    html is empty.
   * @override
   */
  goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
      html) {
    if (!html || goog.string.isBreakingWhitespace(html)) {
      return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
          this.getNonCollapsingBlankHtml() :
          '';
    }
    return html;
  };
  
  
  /**
   * Gets HTML with no contents that won't collapse, for browsers that
   * collapse the empty string.
   * @return {string} Blank html.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
      goog.functions.constant('<br>');
  
  
  /**
   * Internal backspace handler.
   * @param {goog.events.Event} e The keypress event.
   * @param {goog.dom.AbstractRange} range The closure range object.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(
      e, range) {
    var field = this.getFieldObject().getElement();
    var container = range && range.getStartNode();
  
    if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
      e.preventDefault();
      // TODO(user): I think we probably don't need to stopPropagation here
      e.stopPropagation();
    }
  };
  
  
  /**
   * Fix paragraphs to be the correct type of node.
   * @param {goog.events.Event} e The `<enter>` key event.
   * @param {boolean} split Whether we already split up a blockquote by
   *     manually inserting elements.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
      function(e, split) {
    // Force IE to turn the node we are leaving into a DIV.  If we do turn
    // it into a DIV, the node IE creates in response to ENTER will also be
    // a DIV.  If we don't, it will be a P.  We handle that case
    // in handleKeyUpIE_
    if (goog.userAgent.IE || goog.userAgent.OPERA) {
      this.ensureBlockIeOpera(goog.dom.TagName.DIV);
    } else if (!split && goog.userAgent.WEBKIT) {
      // WebKit duplicates a blockquote when the user hits enter. Let's cancel
      // this and insert a BR instead, to make it more consistent with the other
      // browsers.
      var range = this.getFieldObject().getRange();
      if (!range ||
          !goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
              range.getContainerElement())) {
        return;
      }
  
      var dh = this.getFieldDomHelper();
      var br = dh.createElement(goog.dom.TagName.BR);
      range.insertNode(br, true);
  
      // If the BR is at the end of a block element, Safari still thinks there is
      // only one line instead of two, so we need to add another BR in that case.
      if (goog.editor.node.isBlockTag(br.parentNode) &&
          !goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
        goog.dom.insertSiblingBefore(dh.createElement(goog.dom.TagName.BR), br);
      }
  
      goog.editor.range.placeCursorNextTo(br, false);
      e.preventDefault();
    }
  };
  
  
  /**
   * Determines whether the lowest containing block node is a blockquote.
   * @param {Node} n The node.
   * @return {boolean} Whether the deepest block ancestor of n is a blockquote.
   */
  goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
    for (var current = n; current; current = current.parentNode) {
      if (goog.editor.node.isBlockTag(current)) {
        return /** @type {!Element} */ (current).tagName ==
            goog.dom.TagName.BLOCKQUOTE;
      }
    }
  
    return false;
  };
  
  
  /**
   * Internal delete key handler.
   * @param {goog.events.Event} e The keypress event.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
    this.deleteBrGecko(e);
  };
  
  
  /**
   * Deletes the element at the cursor if it is a BR node, and if it does, calls
   * e.preventDefault to stop the browser from deleting. Only necessary in Gecko
   * as a workaround for mozilla bug 205350 where deleting a BR that is followed
   * by a block element doesn't work (the BR gets immediately replaced). We also
   * need to account for an ill-formed cursor which occurs from us trying to
   * stop the browser from deleting.
   *
   * @param {goog.events.Event} e The DELETE keypress event.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
    var range = this.getFieldObject().getRange();
    if (range.isCollapsed()) {
      var container = range.getEndNode();
      if (container.nodeType == goog.dom.NodeType.ELEMENT) {
        var nextNode = container.childNodes[range.getEndOffset()];
        if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
          // We want to retrieve the first non-whitespace previous sibling
          // as we could have added an empty text node below and want to
          // properly handle deleting a sequence of BR's.
          var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
          var nextSibling = nextNode.nextSibling;
  
          container.removeChild(nextNode);
          e.preventDefault();
  
          // When we delete a BR followed by a block level element, the cursor
          // has a line-height which spans the height of the block level element.
          // e.g. If we delete a BR followed by a UL, the resulting HTML will
          // appear to the end user like:-
          //
          // |  * one
          // |  * two
          // |  * three
          //
          // There are a couple of cases that we have to account for in order to
          // properly conform to what the user expects when DELETE is pressed.
          //
          // 1. If the BR has a previous sibling and the previous sibling is
          //    not a block level element or a BR, we place the cursor at the
          //    end of that.
          // 2. If the BR doesn't have a previous sibling or the previous sibling
          //    is a block level element or a BR, we place the cursor at the
          //    beginning of the leftmost leaf of its next sibling.
          if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
            if (previousSibling &&
                !(previousSibling.tagName == goog.dom.TagName.BR ||
                  goog.editor.node.isBlockTag(previousSibling))) {
              goog.dom.Range
                  .createCaret(
                      previousSibling,
                      goog.editor.node.getLength(previousSibling))
                  .select();
            } else {
              var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
              goog.dom.Range.createCaret(leftMostLeaf, 0).select();
            }
          }
        }
      }
    }
  };
  
  
  /** @override */
  goog.editor.plugins.EnterHandler.prototype.handleKeyDown = function(e) {
    if (goog.userAgent.GECKO) {
      // If a dialog doesn't have selectable field, Gecko grabs the event and
      // performs actions in editor window. This solves that problem and allows
      // the event to be passed on to proper handlers.
      if (this.getFieldObject().inModalMode()) {
        return false;
      }
  
      // Firefox will allow the first node in an iframe to be deleted
      // on a backspace.  Disallow it if the node is empty.
      if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
        this.handleBackspaceInternal(e, this.getFieldObject().getRange());
      } else if (e.keyCode == goog.events.KeyCodes.DELETE) {
        this.handleDeleteGecko(e);
      }
    }
  
    return false;
  };
  
  
  /** @override */
  goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
    // ENTER must be handled in keyPress as it requires a beforechange event,
    // which is fired in between keydown and keyup.
    if (e.keyCode == goog.events.KeyCodes.ENTER) {
      if (goog.userAgent.GECKO) {
        if (!e.shiftKey) {
          // Behave similarly to IE's content editable return carriage:
          // If the shift key is down or specified by the application, insert a
          // BR, otherwise split paragraphs
          this.handleEnterGecko_(e);
        }
      } else {
        // In Gecko-based browsers, this is handled in the handleEnterGecko_
        // method.
        this.getFieldObject().dispatchBeforeChange();
        var cursorPosition = this.deleteCursorSelection_();
  
        var split = !!this.getFieldObject().execCommand(
            goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
        if (split) {
          // TODO(user): I think we probably don't need to stopPropagation here
          e.preventDefault();
          e.stopPropagation();
        }
  
        this.releasePositionObject_(cursorPosition);
  
        if (goog.userAgent.WEBKIT) {
          this.handleEnterWebkitInternal(e);
        }
  
        this.processParagraphTagsInternal(e, split);
        this.getFieldObject().dispatchChange();
      }
    }
  
    return false;
  };
  
  
  /** @override */
  goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
    // If a dialog doesn't have selectable field, Gecko grabs the event and
    // performs actions in editor window. This solves that problem and allows
    // the event to be passed on to proper handlers.
    if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
      return false;
    }
    this.handleKeyUpInternal(e);
    return false;
  };
  
  
  /**
   * Internal handler for keyup events.
   * @param {goog.events.Event} e The key event.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
    if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
        e.keyCode == goog.events.KeyCodes.ENTER) {
      this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
    }
  };
  
  
  /**
   * Handles an enter keypress event on fields in Gecko.
   * @param {goog.events.BrowserEvent} e The key event.
   * @private
   */
  goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
    // Retrieve whether the selection is collapsed before we delete it.
    var range = this.getFieldObject().getRange();
    var wasCollapsed = !range || range.isCollapsed();
    var cursorPosition = this.deleteCursorSelection_();
  
    var handled = this.getFieldObject().execCommand(
        goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
    if (handled) {
      // TODO(user): I think we probably don't need to stopPropagation here
      e.preventDefault();
      e.stopPropagation();
    }
  
    this.releasePositionObject_(cursorPosition);
    if (!handled) {
      this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
    }
  };
  
  
  /**
   * Handle an enter key press in WebKit.
   * @param {goog.events.BrowserEvent} e The key press event.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
      goog.nullFunction;
  
  
  /**
   * Handle an enter key press on collapsed selection.  handleEnterGecko_ ensures
   * the selection is collapsed by deleting its contents if it is not.  The
   * default implementation does nothing.
   * @param {goog.events.BrowserEvent} e The key press event.
   * @param {boolean} wasCollapsed Whether the selection was collapsed before
   *     the key press.  If it was not, code before this function has already
   *     cleared the contents of the selection.
   * @param {goog.dom.AbstractRange} range Object representing the selection.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
      goog.nullFunction;
  
  
  /**
   * Names of all the nodes that we don't want to turn into block nodes in IE when
   * the user hits enter.
   * @type {Object}
   * @private
   */
  goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
      goog.object.createSet(
          goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
          goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
          goog.dom.TagName.H5, goog.dom.TagName.H6);
  
  
  /**
   * Whether this is a node that contains a single BR tag and non-nbsp
   * whitespace.
   * @param {Node} node Node to check.
   * @return {boolean} Whether this is an element that only contains a BR.
   * @protected
   */
  goog.editor.plugins.EnterHandler.isBrElem = function(node) {
    return goog.editor.node.isEmpty(node) &&
        goog.dom.getElementsByTagName(
            goog.dom.TagName.BR, /** @type {!Element} */ (node)).length == 1;
  };
  
  
  /**
   * Ensures all text in IE and Opera to be in the given tag in order to control
   * Enter spacing. Call this when Enter is pressed if desired.
   *
   * We want to make sure the user is always inside of a block (or other nodes
   * listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_).  We
   * listen to keypress to force nodes that the user is leaving to turn into
   * blocks, but we also need to listen to keyup to force nodes that the user is
   * entering to turn into blocks.
   * Example:  html is: `<h2>foo[cursor]</h2>`, and the user hits enter.  We
   * don't want to format the h2, but we do want to format the P that is
   * created on enter.  The P node is not available until keyup.
   * @param {!goog.dom.TagName} tag The tag name to convert to.
   * @param {boolean=} opt_keyUp Whether the function is being called on key up.
   *     When called on key up, the cursor is in the newly created node, so the
   *     semantics for when to change it to a block are different.  Specifically,
   *     if the resulting node contains only a BR, it is converted to `<tag>`.
   * @protected
   */
  goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(
      tag, opt_keyUp) {
    var range = this.getFieldObject().getRange();
    var container = range.getContainer();
    var field = this.getFieldObject().getElement();
  
    /** @type {!Node|undefined} */
    var paragraph = undefined;
    while (container && container != field) {
      // We don't need to ensure a block if we are already in the same block, or
      // in another block level node that we don't want to change the format of
      // (unless we're handling keyUp and that block node just contains a BR).
      var nodeName = container.nodeName;
      // Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
      // instead of done before and saved in a variable, so that it can be
      // short-circuited and avoid a weird IE edge case.
      if (nodeName == tag ||
          (goog.editor.plugins.EnterHandler
               .DO_NOT_ENSURE_BLOCK_NODES_[nodeName] &&
           !(opt_keyUp &&
             goog.editor.plugins.EnterHandler.isBrElem(container)))) {
        // Opera can create a <p> inside of a <div> in some situations,
        // such as when breaking out of a list that is contained in a <div>.
        if (goog.userAgent.OPERA && paragraph) {
          if (nodeName == tag && paragraph == container.lastChild &&
              goog.editor.node.isEmpty(paragraph)) {
            goog.dom.insertSiblingAfter(paragraph, container);
            goog.dom.Range.createFromNodeContents(paragraph).select();
          }
          break;
        }
        return;
      }
      if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
          nodeName != tag) {
        paragraph = container;
      }
  
      container = container.parentNode;
    }
  
  
    if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
      // IE (before IE9) has a bug where if the cursor is directly before a block
      // node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
      // the FormatBlock command actually formats the "bar" instead of the "foo".
      // This is just wrong. To work-around this, we want to move the
      // selection back one character, and then restore it to its prior position.
      // NOTE: We use the following "range math" to detect this situation because
      // using Closure ranges here triggers a bug in IE that causes a crash.
      // parent2 != parent3 ensures moving the cursor forward one character
      // crosses at least 1 element boundary, and therefore tests if the cursor is
      // at such a boundary.  The second check, parent3 != range.parentElement()
      // weeds out some cases where the elements are siblings instead of cousins.
      var needsHelp = false;
      range = range.getBrowserRangeObject();
      var range2 = range.duplicate();
      range2.moveEnd('character', 1);
      // In whitebox mode, when the cursor is at the end of the field, trying to
      // move the end of the range will do nothing, and hence the range's text
      // will be empty.  In this case, the cursor clearly isn't sitting just
      // before a block node, since it isn't before anything.
      if (range2.text.length) {
        var parent2 = range2.parentElement();
  
        var range3 = range2.duplicate();
        range3.collapse(false);
        var parent3 = range3.parentElement();
  
        if ((needsHelp =
                 parent2 != parent3 && parent3 != range.parentElement())) {
          range.move('character', -1);
          range.select();
        }
      }
    }
  
    this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
        'FormatBlock', false, '<' + tag + '>');
  
    if (needsHelp) {
      range.move('character', 1);
      range.select();
    }
  };
  
  
  /**
   * Deletes the content at the current cursor position.
   * @return {!Node|!Object} Something representing the current cursor position.
   *    See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
   *    Should be passed to releasePositionObject_ when no longer in use.
   * @private
   */
  goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
    return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
        this.deleteCursorSelectionW3C_() :
        this.deleteCursorSelectionIE_();
  };
  
  
  /**
   * Releases the object returned by deleteCursorSelection_.
   * @param {Node|Object} position The object returned by deleteCursorSelection_.
   * @private
   */
  goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ = function(
      position) {
    if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      (/** @type {Node} */ (position)).removeNode(true);
    }
  };
  
  
  /**
   * Delete the selection at the current cursor position, then returns a temporary
   * node at the current position.
   * @return {!Node} A temporary node marking the current cursor position. This
   *     node should eventually be removed from the DOM.
   * @private
   */
  goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
      function() {
    var doc = this.getFieldDomHelper().getDocument();
    var range = doc.selection.createRange();
  
    var id = goog.string.createUniqueString();
    range.pasteHTML('<span id="' + id + '"></span>');
    var splitNode = doc.getElementById(id);
    splitNode.id = '';
    return splitNode;
  };
  
  
  /**
   * Delete the selection at the current cursor position, then returns the node
   * at the current position.
   * @return {!goog.editor.range.Point} The current cursor position. Note that
   *    unlike simulateEnterIE_, this should not be removed from the DOM.
   * @private
   */
  goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
      function() {
    var range = this.getFieldObject().getRange();
  
    // Delete the current selection if it's is non-collapsed.
    // Although this is redundant in FF, it's necessary for Safari
    if (range && !range.isCollapsed()) {
      var shouldDelete = true;
      // Opera selects the <br> in an empty block if there is no text node
      // preceding it. To preserve inline formatting when pressing [enter] inside
      // an empty block, don't delete the selection if it only selects a <br> at
      // the end of the block.
      // TODO(user): Move this into goog.dom.Range. It should detect this state
      // when creating a range from the window selection and fix it in the created
      // range.
      if (goog.userAgent.OPERA) {
        var startNode = range.getStartNode();
        var startOffset = range.getStartOffset();
        if (startNode == range.getEndNode() &&
            // This weeds out cases where startNode is a text node.
            startNode.lastChild &&
            /** @type {!Element} */ (startNode.lastChild).tagName ==
                goog.dom.TagName.BR &&
            // If this check is true, then endOffset is implied to be
            // startOffset + 1, because the selection is not collapsed and
            // it starts and ends within the same element.
            startOffset == startNode.childNodes.length - 1) {
          shouldDelete = false;
        }
      }
      if (shouldDelete) {
        goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
      }
    }
  
    return goog.editor.range.getDeepEndPoint(range, true);
  };
  
  
  /**
   * Deletes the contents of the selection from the DOM.
   * @param {goog.dom.AbstractRange} range The range to remove contents from.
   * @return {goog.dom.AbstractRange} The resulting range. Used for testing.
   * @private
   */
  goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
    if (range && !range.isCollapsed()) {
      var reselect = true;
      var baseNode = range.getContainerElement();
      var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
      var rangeOffset = range.getStartOffset();
  
      // Whether the selection crosses no container boundaries.
      var isInOneContainer =
          goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
  
      // Whether the selection ends in a container it doesn't fully select.
      var isPartialEnd = !isInOneContainer &&
          goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
  
      // Remove The range contents, and ensure the correct content stays selected.
      range.removeContents();
      var node = nodeOffset.findTargetNode(baseNode);
      if (node) {
        range = goog.dom.Range.createCaret(node, rangeOffset);
      } else {
        // This occurs when the node that would have been referenced has now been
        // deleted and there are no other nodes in the baseNode. Thus need to
        // set the caret to the end of the base node.
        range = goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
        reselect = false;
      }
      range.select();
  
      // If we just deleted everything from the container, add an nbsp
      // to the container, and leave the cursor inside of it
      if (isInOneContainer) {
        var container = goog.editor.style.getContainer(range.getStartNode());
        if (goog.editor.node.isEmpty(container, true)) {
          var html = '&nbsp;';
          if (goog.userAgent.OPERA && container.tagName == goog.dom.TagName.LI) {
            // Don't break Opera's native break-out-of-lists behavior.
            html = '<br>';
          }
          goog.editor.node.replaceInnerHtml(container, html);
          goog.editor.range.selectNodeStart(container.firstChild);
          reselect = false;
        }
      }
  
      if (isPartialEnd) {
        /*
         This code handles the following, where | is the cursor:
           <div>a|b</div><div>c|d</div>
         After removeContents, the remaining HTML is
           <div>a</div><div>d</div>
         which means the line break between the two divs remains.  This block
         moves children of the second div in to the first div to get the correct
         result:
           <div>ad</div>
  
         TODO(robbyw): Should we wrap the second div's contents in a span if they
                       have inline style?
        */
        var rangeStart = goog.editor.style.getContainer(range.getStartNode());
        var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
        if (rangeStart && redundantContainer) {
          goog.dom.append(rangeStart, redundantContainer.childNodes);
          goog.dom.removeNode(redundantContainer);
        }
      }
  
      if (reselect) {
        // The contents of the original range are gone, so restore the cursor
        // position at the start of where the range once was.
        range = goog.dom.Range.createCaret(
            nodeOffset.findTargetNode(baseNode), rangeOffset);
        range.select();
      }
    }
  
    return range;
  };
  
  
  /**
   * Checks whether the whole range is in a single block-level element.
   * @param {goog.dom.AbstractRange} range The range to check.
   * @return {boolean} Whether the whole range is in a single block-level element.
   * @private
   */
  goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
    // Find the block element containing the start of the selection.
    var startContainer = range.getStartNode();
    if (goog.editor.style.isContainer(startContainer)) {
      startContainer =
          startContainer.childNodes[range.getStartOffset()] || startContainer;
    }
    startContainer = goog.editor.style.getContainer(startContainer);
  
    // Find the block element containing the end of the selection.
    var endContainer = range.getEndNode();
    if (goog.editor.style.isContainer(endContainer)) {
      endContainer =
          endContainer.childNodes[range.getEndOffset()] || endContainer;
    }
    endContainer = goog.editor.style.getContainer(endContainer);
  
    // Compare the two.
    return startContainer == endContainer;
  };
  
  
  /**
   * Checks whether the end of the range is not at the end of a block-level
   * element.
   * @param {goog.dom.AbstractRange} range The range to check.
   * @return {boolean} Whether the end of the range is not at the end of a
   *     block-level element.
   * @private
   */
  goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
    var endContainer = range.getEndNode();
    var endOffset = range.getEndOffset();
    var node = endContainer;
    if (goog.editor.style.isContainer(node)) {
      var child = node.childNodes[endOffset];
      // Child is null when end offset is >= length, which indicates the entire
      // container is selected.  Otherwise, we also know the entire container
      // is selected if the selection ends at a new container.
      if (!child ||
          child.nodeType == goog.dom.NodeType.ELEMENT &&
              goog.editor.style.isContainer(child)) {
        return false;
      }
    }
  
    var container = goog.editor.style.getContainer(node);
    while (container != node) {
      if (goog.editor.node.getNextSibling(node)) {
        return true;
      }
      node = node.parentNode;
    }
  
    return endOffset != goog.editor.node.getLength(endContainer);
  };
  