// 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.
 *
 * @author robbyw@google.com (Robby Walker)
 */

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);
};
