// 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 TrogEdit plugin to handle enter keys by inserting the
 * specified block level tag.
 *
 * @author robbyw@google.com (Robby Walker)
 */

goog.provide('goog.editor.plugins.TagOnEnterHandler');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.Command');
goog.require('goog.editor.node');
goog.require('goog.editor.plugins.EnterHandler');
goog.require('goog.editor.range');
goog.require('goog.editor.style');
goog.require('goog.events.KeyCodes');
goog.require('goog.functions');
goog.require('goog.string.Unicode');
goog.require('goog.style');
goog.require('goog.userAgent');



/**
 * Plugin to handle enter keys. This subclass normalizes all browsers to use
 * the given block tag on enter.
 * @param {goog.dom.TagName} tag The type of tag to add on enter.
 * @constructor
 * @extends {goog.editor.plugins.EnterHandler}
 */
goog.editor.plugins.TagOnEnterHandler = function(tag) {
  this.tag = tag;

  goog.editor.plugins.EnterHandler.call(this);
};
goog.inherits(goog.editor.plugins.TagOnEnterHandler,
    goog.editor.plugins.EnterHandler);


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.getTrogClassId = function() {
  return 'TagOnEnterHandler';
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.getNonCollapsingBlankHtml =
    function() {
  if (this.tag == goog.dom.TagName.P) {
    return '<p>&nbsp;</p>';
  } else if (this.tag == goog.dom.TagName.DIV) {
    return '<div><br></div>';
  }
  return '<br>';
};


/**
 * This plugin is active on uneditable fields so it can provide a value for
 * queryCommandValue calls asking for goog.editor.Command.BLOCKQUOTE.
 * @return {boolean} True.
 * @override
 */
goog.editor.plugins.TagOnEnterHandler.prototype.activeOnUneditableFields =
    goog.functions.TRUE;


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.isSupportedCommand = function(
    command) {
  return command == goog.editor.Command.DEFAULT_TAG;
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.queryCommandValue = function(
    command) {
  return command == goog.editor.Command.DEFAULT_TAG ? this.tag : null;
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.handleBackspaceInternal =
    function(e, range) {
  goog.editor.plugins.TagOnEnterHandler.superClass_.handleBackspaceInternal.
      call(this, e, range);

  if (goog.userAgent.GECKO) {
    this.markBrToNotBeRemoved_(range, true);
  }
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.processParagraphTagsInternal =
    function(e, split) {
  if ((goog.userAgent.OPERA || goog.userAgent.IE) &&
      this.tag != goog.dom.TagName.P) {
    this.ensureBlockIeOpera(this.tag);
  }
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.handleDeleteGecko = function(
    e) {
  var range = this.getFieldObject().getRange();
  var container = goog.editor.style.getContainer(
      range && range.getContainerElement());
  if (this.getFieldObject().getElement().lastChild == container &&
      goog.editor.plugins.EnterHandler.isBrElem(container)) {
    // Don't delete if it's the last node in the field and just has a BR.
    e.preventDefault();
    // TODO(user): I think we probably don't need to stopPropagation here
    e.stopPropagation();
  } else {
    // Go ahead with deletion.
    // Prevent an existing BR immediately following the selection being deleted
    // from being removed in the keyup stage (as opposed to a BR added by FF
    // after deletion, which we do remove).
    this.markBrToNotBeRemoved_(range, false);
    // Manually delete the selection if it's at a BR.
    this.deleteBrGecko(e);
  }
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.handleKeyUpInternal = function(
    e) {
  if (goog.userAgent.GECKO) {
    if (e.keyCode == goog.events.KeyCodes.DELETE) {
      this.removeBrIfNecessary_(false);
    } else if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
      this.removeBrIfNecessary_(true);
    }
  } else if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
             e.keyCode == goog.events.KeyCodes.ENTER) {
    this.ensureBlockIeOpera(this.tag, true);
  }
  // Safari uses DIVs by default.
};


/**
 * String that matches a single BR tag or NBSP surrounded by non-breaking
 * whitespace
 * @type {string}
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ =
    '[\t\n\r ]*(<br[^>]*\/?>|&nbsp;)[\t\n\r ]*';


/**
 * String that matches a single BR tag or NBSP surrounded by non-breaking
 * whitespace
 * @type {RegExp}
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_ = new RegExp('^' +
    goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ +
    '$');


/**
 * Ensures the current node is wrapped in the tag.
 * @param {Node} node The node to ensure gets wrapped.
 * @param {Element} container Element containing the selection.
 * @return {Element} Element containing the selection, after the wrapping.
  * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.ensureNodeIsWrappedW3c_ =
    function(node, container) {
  if (container == this.getFieldObject().getElement()) {
    // If the first block-level ancestor of cursor is the field,
    // don't split the tree. Find all the text from the cursor
    // to both block-level elements surrounding it (if they exist)
    // and split the text into two elements.
    // This is the IE contentEditable behavior.

    // The easy way to do this is to wrap all the text in an element
    // and then split the element as if the user had hit enter
    // in the paragraph

    // However, simply wrapping the text into an element creates problems
    // if the text was already wrapped using some other element such as an
    // anchor.  For example, wrapping the text of
    //   <a href="">Text</a>
    // would produce
    //   <a href=""><p>Text</p></a>
    // which is not what we want.  What we really want is
    //   <p><a href="">Text</a></p>
    // So we need to search for an ancestor of position.node to be wrapped.
    // We do this by iterating up the hierarchy of postiion.node until we've
    // reached the node that's just under the container.
    var isChildOfFn = function(child) {
      return container == child.parentNode; };
    var nodeToWrap = goog.dom.getAncestor(node, isChildOfFn, true);
    container = goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_(
        this.tag, {node: nodeToWrap, offset: 0}, container);
  }
  return container;
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.handleEnterWebkitInternal =
    function(e) {
  if (this.tag == goog.dom.TagName.DIV) {
    var range = this.getFieldObject().getRange();
    var container =
        goog.editor.style.getContainer(range.getContainerElement());

    var position = goog.editor.range.getDeepEndPoint(range, true);
    container = this.ensureNodeIsWrappedW3c_(position.node, container);
    goog.dom.Range.createCaret(position.node, position.offset).select();
  }
};


/** @override */
goog.editor.plugins.TagOnEnterHandler.prototype.
    handleEnterAtCursorGeckoInternal = function(e, wasCollapsed, range) {
  // We use this because there are a few cases where FF default
  // implementation doesn't follow IE's:
  //   -Inserts BRs into empty elements instead of NBSP which has nasty
  //    side effects w/ making/deleting selections
  //   -Hitting enter when your cursor is in the field itself. IE will
  //    create two elements. FF just inserts a BR.
  //   -Hitting enter inside an empty list-item doesn't create a block
  //    tag. It just splits the list and puts your cursor in the middle.
  var li = null;
  if (wasCollapsed) {
    // Only break out of lists for collapsed selections.
    li = goog.dom.getAncestorByTagNameAndClass(
        range && range.getContainerElement(), goog.dom.TagName.LI);
  }
  var isEmptyLi = (li &&
      li.innerHTML.match(
          goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_));
  var elementAfterCursor = isEmptyLi ?
      this.breakOutOfEmptyListItemGecko_(li) :
      this.handleRegularEnterGecko_();

  // Move the cursor in front of "nodeAfterCursor", and make sure it
  // is visible
  this.scrollCursorIntoViewGecko_(elementAfterCursor);

  // Fix for http://b/1991234 :
  if (goog.editor.plugins.EnterHandler.isBrElem(elementAfterCursor)) {
    // The first element in the new line is a line with just a BR and maybe some
    // whitespace.
    // Calling normalize() is needed because there might be empty text nodes
    // before BR and empty text nodes cause the cursor position bug in Firefox.
    // See http://b/5220858
    elementAfterCursor.normalize();
    var br = elementAfterCursor.getElementsByTagName(goog.dom.TagName.BR)[0];
    if (br.previousSibling &&
        br.previousSibling.nodeType == goog.dom.NodeType.TEXT) {
      // If there is some whitespace before the BR, don't put the selection on
      // the BR, put it in the text node that's there, otherwise when you type
      // it will create adjacent text nodes.
      elementAfterCursor = br.previousSibling;
    }
  }

  goog.editor.range.selectNodeStart(elementAfterCursor);

  e.preventDefault();
  // TODO(user): I think we probably don't need to stopPropagation here
  e.stopPropagation();
};


/**
 * If The cursor is in an empty LI then break out of the list like in IE
 * @param {Node} li LI to break out of.
 * @return {!Element} Element to put the cursor after.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.breakOutOfEmptyListItemGecko_ =
    function(li) {
  // Do this as follows:
  // 1. <ul>...<li>&nbsp;</li>...</ul>
  // 2. <ul id='foo1'>...<li id='foo2'>&nbsp;</li>...</ul>
  // 3. <ul id='foo1'>...</ul><p id='foo3'>&nbsp;</p><ul id='foo2'>...</ul>
  // 4. <ul>...</ul><p>&nbsp;</p><ul>...</ul>
  //
  // There are a couple caveats to the above. If the UL is contained in
  // a list, then the new node inserted is an LI, not a P.
  // For an OL, it's all the same, except the tagname of course.
  // Finally, it's possible that with the LI at the beginning or the end
  // of the list that we'll end up with an empty list. So we special case
  // those cases.

  var listNode = li.parentNode;
  var grandparent = listNode.parentNode;
  var inSubList = grandparent.tagName == goog.dom.TagName.OL ||
      grandparent.tagName == goog.dom.TagName.UL;

  // TODO(robbyw): Should we apply the list or list item styles to the new node?
  var newNode = goog.dom.getDomHelper(li).createElement(
      inSubList ? goog.dom.TagName.LI : this.tag);

  if (!li.previousSibling) {
    goog.dom.insertSiblingBefore(newNode, listNode);
  } else {
    if (li.nextSibling) {
      var listClone = listNode.cloneNode(false);
      while (li.nextSibling) {
        listClone.appendChild(li.nextSibling);
      }
      goog.dom.insertSiblingAfter(listClone, listNode);
    }
    goog.dom.insertSiblingAfter(newNode, listNode);
  }
  if (goog.editor.node.isEmpty(listNode)) {
    goog.dom.removeNode(listNode);
  }
  goog.dom.removeNode(li);
  newNode.innerHTML = '&nbsp;';

  return newNode;
};


/**
 * Wrap the text indicated by "position" in an HTML container of type
 * "nodeName".
 * @param {string} nodeName Type of container, e.g. "p" (paragraph).
 * @param {Object} position The W3C cursor position object
 *     (from getCursorPositionW3c).
 * @param {Node} container The field containing position.
 * @return {!Element} The container element that holds the contents from
 *     position.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_ = function(nodeName,
    position, container) {
  var start = position.node;
  while (start.previousSibling &&
         !goog.editor.style.isContainer(start.previousSibling)) {
    start = start.previousSibling;
  }

  var end = position.node;
  while (end.nextSibling &&
         !goog.editor.style.isContainer(end.nextSibling)) {
    end = end.nextSibling;
  }

  var para = container.ownerDocument.createElement(nodeName);
  while (start != end) {
    var newStart = start.nextSibling;
    goog.dom.appendChild(para, start);
    start = newStart;
  }
  var nextSibling = end.nextSibling;
  goog.dom.appendChild(para, end);
  container.insertBefore(para, nextSibling);

  return para;
};


/**
 * When we delete an element, FF inserts a BR. We want to strip that
 * BR after the fact, but in the case where your cursor is at a character
 * right before a BR and you delete that character, we don't want to
 * strip it. So we detect this case on keydown and mark the BR as not needing
 * removal.
 * @param {goog.dom.AbstractRange} range The closure range object.
 * @param {boolean} isBackspace Whether this is handling the backspace key.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.markBrToNotBeRemoved_ =
    function(range, isBackspace) {
  var focusNode = range.getFocusNode();
  var focusOffset = range.getFocusOffset();
  var newEndOffset = isBackspace ? focusOffset : focusOffset + 1;

  if (goog.editor.node.getLength(focusNode) == newEndOffset) {
    var sibling = focusNode.nextSibling;
    if (sibling && sibling.tagName == goog.dom.TagName.BR) {
      this.brToKeep_ = sibling;
    }
  }
};


/**
 * If we hit delete/backspace to merge elements, FF inserts a BR.
 * We want to strip that BR. In markBrToNotBeRemoved, we detect if
 * there was already a BR there before the delete/backspace so that
 * we don't accidentally remove a user-inserted BR.
 * @param {boolean} isBackSpace Whether this is handling the backspace key.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.removeBrIfNecessary_ = function(
    isBackSpace) {
  var range = this.getFieldObject().getRange();
  var focusNode = range.getFocusNode();
  var focusOffset = range.getFocusOffset();

  var sibling;
  if (isBackSpace && focusNode.data == '') {
    // nasty hack. sometimes firefox will backspace a paragraph and put
    // the cursor before the BR. when it does this, the focusNode is
    // an empty textnode.
    sibling = focusNode.nextSibling;
  } else if (isBackSpace && focusOffset == 0) {
    var node = focusNode;
    while (node && !node.previousSibling &&
           node.parentNode != this.getFieldObject().getElement()) {
      node = node.parentNode;
    }
    sibling = node.previousSibling;
  } else if (focusNode.length == focusOffset) {
    sibling = focusNode.nextSibling;
  }

  if (!sibling || sibling.tagName != goog.dom.TagName.BR ||
      this.brToKeep_ == sibling) {
    return;
  }

  goog.dom.removeNode(sibling);
  if (focusNode.nodeType == goog.dom.NodeType.TEXT) {
    // Sometimes firefox inserts extra whitespace. Do our best to deal.
    // This is buggy though.
    focusNode.data =
        goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_(
            focusNode.data);
    // When we strip whitespace, make sure that our cursor is still at
    // the end of the textnode.
    goog.dom.Range.createCaret(focusNode,
        Math.min(focusOffset, focusNode.length)).select();
  }
};


/**
 * Trim the tabs and line breaks from a string.
 * @param {string} string String to trim.
 * @return {string} Trimmed string.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_ = function(
    string) {
  return string.replace(/^[\t\n\r]|[\t\n\r]$/g, '');
};


/**
 * Called in response to a normal enter keystroke. It has the action of
 * splitting elements.
 * @return {Element} The node that the cursor should be before.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.handleRegularEnterGecko_ =
    function() {
  var range = this.getFieldObject().getRange();
  var container =
      goog.editor.style.getContainer(range.getContainerElement());
  var newNode;
  if (goog.editor.plugins.EnterHandler.isBrElem(container)) {
    if (container.tagName == goog.dom.TagName.BODY) {
      // If the field contains only a single BR, this code ensures we don't
      // try to clone the body tag.
      container = this.ensureNodeIsWrappedW3c_(
          container.getElementsByTagName(goog.dom.TagName.BR)[0],
          container);
    }

    newNode = container.cloneNode(true);
    goog.dom.insertSiblingAfter(newNode, container);
  } else {
    if (!container.firstChild) {
      container.innerHTML = '&nbsp;';
    }

    var position = goog.editor.range.getDeepEndPoint(range, true);
    container = this.ensureNodeIsWrappedW3c_(position.node, container);

    newNode = goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(
        position.node, position.offset, container);

    // If the left half and right half of the splitted node are anchors then
    // that means the user pressed enter while the caret was inside
    // an anchor tag and split it.  The left half is the first anchor
    // found while traversing the right branch of container.  The right half
    // is the first anchor found while traversing the left branch of newNode.
    var leftAnchor =
        goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_(
            container);
    var rightAnchor =
        goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_(
            newNode, true);
    if (leftAnchor && rightAnchor &&
        leftAnchor.tagName == goog.dom.TagName.A &&
        rightAnchor.tagName == goog.dom.TagName.A) {
      // If the original anchor (left anchor) is now empty, that means
      // the user pressed [Enter] at the beginning of the anchor,
      // in which case we we
      // want to replace that anchor with its child nodes
      // Otherwise, we take the second half of the splitted text and break
      // it out of the anchor.
      var anchorToRemove = goog.editor.node.isEmpty(leftAnchor, false) ?
          leftAnchor : rightAnchor;
      goog.dom.flattenElement(/** @type {!Element} */ (anchorToRemove));
    }
  }
  return /** @type {!Element} */ (newNode);
};


/**
 * Scroll the cursor into view, resulting from splitting the paragraph/adding
 * a br. It behaves differently than scrollIntoView
 * @param {Element} element The element immediately following the cursor. Will
 *     be used to determine how to scroll in order to make the cursor visible.
 *     CANNOT be a BR, as they do not have offsetHeight/offsetTop.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.prototype.scrollCursorIntoViewGecko_ =
    function(element) {
  if (!this.getFieldObject().isFixedHeight()) {
    return; // Only need to scroll fixed height fields.
  }

  var field = this.getFieldObject().getElement();

  // Get the y position of the element we want to scroll to
  var elementY = goog.style.getPageOffsetTop(element);

  // Determine the height of that element, since we want the bottom of the
  // element to be in view.
  var bottomOfNode = elementY + element.offsetHeight;

  var dom = this.getFieldDomHelper();
  var win = this.getFieldDomHelper().getWindow();
  var scrollY = dom.getDocumentScroll().y;
  var viewportHeight = goog.dom.getViewportSize(win).height;

  // If the botom of the element is outside the viewport, move it into view
  if (bottomOfNode > viewportHeight + scrollY) {
    // In standards mode, use the html element and not the body
    if (field.tagName == goog.dom.TagName.BODY &&
        goog.editor.node.isStandardsMode(field)) {
      field = field.parentNode;
    }
    field.scrollTop = bottomOfNode - viewportHeight;
  }
};


/**
 * Splits the DOM tree around the given node and returns the node
 * containing the second half of the tree. The first half of the tree
 * is modified, but not removed from the DOM.
 * @param {Node} positionNode Node to split at.
 * @param {number} positionOffset Offset into positionNode to split at.  If
 *     positionNode is a text node, this offset is an offset in to the text
 *     content of that node.  Otherwise, positionOffset is an offset in to
 *     the childNodes array.  All elements with child index of  positionOffset
 *     or greater will be moved to the second half.  If positionNode is an
 *     empty element, the dom will be split at that element, with positionNode
 *     ending up in the second half.  positionOffset must be 0 in this case.
 * @param {Node=} opt_root Node at which to stop splitting the dom (the root
 *     is also split).
 * @return {!Node} The node containing the second half of the tree.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.splitDom_ = function(
    positionNode, positionOffset, opt_root) {
  if (!opt_root) opt_root = positionNode.ownerDocument.body;

  // Split the node.
  var textSplit = positionNode.nodeType == goog.dom.NodeType.TEXT;
  var secondHalfOfSplitNode;
  if (textSplit) {
    if (goog.userAgent.IE &&
        positionOffset == positionNode.nodeValue.length) {
      // Since splitText fails in IE at the end of a node, we split it manually.
      secondHalfOfSplitNode = goog.dom.getDomHelper(positionNode).
          createTextNode('');
      goog.dom.insertSiblingAfter(secondHalfOfSplitNode, positionNode);
    } else {
      secondHalfOfSplitNode = positionNode.splitText(positionOffset);
    }
  } else {
    // Here we ensure positionNode is the last node in the first half of the
    // resulting tree.
    if (positionOffset) {
      // Use offset as an index in to childNodes.
      positionNode = positionNode.childNodes[positionOffset - 1];
    } else {
      // In this case, positionNode would be the last node in the first half
      // of the tree, but we actually want to move it to the second half.
      // Therefore we set secondHalfOfSplitNode to the same node.
      positionNode = secondHalfOfSplitNode = positionNode.firstChild ||
          positionNode;
    }
  }

  // Create second half of the tree.
  var secondHalf = goog.editor.node.splitDomTreeAt(
      positionNode, secondHalfOfSplitNode, opt_root);

  if (textSplit) {
    // Join secondHalfOfSplitNode and its right text siblings together and
    // then replace leading NonNbspWhiteSpace with a Nbsp.  If
    // secondHalfOfSplitNode has a right sibling that isn't a text node,
    // then we can leave secondHalfOfSplitNode empty.
    secondHalfOfSplitNode =
        goog.editor.plugins.TagOnEnterHandler.joinTextNodes_(
            secondHalfOfSplitNode, true);
    goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
        secondHalfOfSplitNode, true, !!secondHalfOfSplitNode.nextSibling);

    // Join positionNode and its left text siblings together and then replace
    // trailing NonNbspWhiteSpace with a Nbsp.
    var firstHalf = goog.editor.plugins.TagOnEnterHandler.joinTextNodes_(
        positionNode, false);
    goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
        firstHalf, false, false);
  }

  return secondHalf;
};


/**
 * Splits the DOM tree around the given node and returns the node containing
 * second half of the tree, which is appended after the old node.  The first
 * half of the tree is modified, but not removed from the DOM.
 * @param {Node} positionNode Node to split at.
 * @param {number} positionOffset Offset into positionNode to split at.  If
 *     positionNode is a text node, this offset is an offset in to the text
 *     content of that node.  Otherwise, positionOffset is an offset in to
 *     the childNodes array.  All elements with child index of  positionOffset
 *     or greater will be moved to the second half.  If positionNode is an
 *     empty element, the dom will be split at that element, with positionNode
 *     ending up in the second half.  positionOffset must be 0 in this case.
 * @param {Node} node Node to split.
 * @return {!Node} The node containing the second half of the tree.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_ = function(
    positionNode, positionOffset, node) {
  var newNode = goog.editor.plugins.TagOnEnterHandler.splitDom_(
      positionNode, positionOffset, node);
  goog.dom.insertSiblingAfter(newNode, node);
  return newNode;
};


/**
 * Joins node and its adjacent text nodes together.
 * @param {Node} node The node to start joining.
 * @param {boolean} moveForward Determines whether to join left siblings (false)
 *     or right siblings (true).
 * @return {Node} The joined text node.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.joinTextNodes_ = function(node,
    moveForward) {
  if (node && node.nodeName == '#text') {
    var nextNodeFn = moveForward ? 'nextSibling' : 'previousSibling';
    var prevNodeFn = moveForward ? 'previousSibling' : 'nextSibling';
    var nodeValues = [node.nodeValue];
    while (node[nextNodeFn] &&
           node[nextNodeFn].nodeType == goog.dom.NodeType.TEXT) {
      node = node[nextNodeFn];
      nodeValues.push(node.nodeValue);
      goog.dom.removeNode(node[prevNodeFn]);
    }
    if (!moveForward) {
      nodeValues.reverse();
    }
    node.nodeValue = nodeValues.join('');
  }
  return node;
};


/**
 * Replaces leading or trailing spaces of a text node to a single Nbsp.
 * @param {Node} textNode The text node to search and replace white spaces.
 * @param {boolean} fromStart Set to true to replace leading spaces, false to
 *     replace trailing spaces.
 * @param {boolean} isLeaveEmpty Set to true to leave the node empty if the
 *     text node was empty in the first place, otherwise put a Nbsp into the
 *     text node.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_ = function(
    textNode, fromStart, isLeaveEmpty) {
  var regExp = fromStart ? /^[ \t\r\n]+/ : /[ \t\r\n]+$/;
  textNode.nodeValue = textNode.nodeValue.replace(regExp,
                                                  goog.string.Unicode.NBSP);

  if (!isLeaveEmpty && textNode.nodeValue == '') {
    textNode.nodeValue = goog.string.Unicode.NBSP;
  }
};


/**
 * Finds the first A element in a traversal from the input node.  The input
 * node itself is not included in the search.
 * @param {Node} node The node to start searching from.
 * @param {boolean=} opt_useFirstChild Whether to traverse along the first child
 *     (true) or last child (false).
 * @return {Node} The first anchor node found in the search, or null if none
 *     was found.
 * @private
 */
goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_ = function(node,
    opt_useFirstChild) {
  while ((node = opt_useFirstChild ? node.firstChild : node.lastChild) &&
         node.tagName != goog.dom.TagName.A) {
    // Do nothing - advancement is handled in the condition.
  }
  return node;
};
