text: // Copyright 2007 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 Utilities for working with text ranges in HTML documents.
   *
   * @author robbyw@google.com (Robby Walker)
   * @author ojan@google.com (Ojan Vafai)
   * @author jparent@google.com (Julie Parent)
   */
  
  
  goog.provide('goog.dom.TextRange');
  
  goog.require('goog.array');
  goog.require('goog.dom');
  goog.require('goog.dom.AbstractRange');
  goog.require('goog.dom.RangeType');
  goog.require('goog.dom.SavedRange');
  goog.require('goog.dom.TagName');
  goog.require('goog.dom.TextRangeIterator');
  goog.require('goog.dom.browserrange');
  goog.require('goog.string');
  goog.require('goog.userAgent');
  
  
  
  /**
   * Create a new text selection with no properties.  Do not use this constructor:
   * use one of the goog.dom.Range.createFrom* methods instead.
   * @constructor
   * @extends {goog.dom.AbstractRange}
   */
  goog.dom.TextRange = function() {
  };
  goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
  
  
  /**
   * Create a new range wrapper from the given browser range object.  Do not use
   * this method directly - please use goog.dom.Range.createFrom* instead.
   * @param {Range|TextRange} range The browser range object.
   * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
   *     node.
   * @return {goog.dom.TextRange} A range wrapper object.
   */
  goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
    return goog.dom.TextRange.createFromBrowserRangeWrapper_(
        goog.dom.browserrange.createRange(range), opt_isReversed);
  };
  
  
  /**
   * Create a new range wrapper from the given browser range wrapper.
   * @param {goog.dom.browserrange.AbstractRange} browserRange The browser range
   *     wrapper.
   * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
   *     node.
   * @return {goog.dom.TextRange} A range wrapper object.
   * @private
   */
  goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange,
      opt_isReversed) {
    var range = new goog.dom.TextRange();
  
    // Initialize the range as a browser range wrapper type range.
    range.browserRangeWrapper_ = browserRange;
    range.isReversed_ = !!opt_isReversed;
  
    return range;
  };
  
  
  /**
   * Create a new range wrapper that selects the given node's text.  Do not use
   * this method directly - please use goog.dom.Range.createFrom* instead.
   * @param {Node} node The node to select.
   * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
   *     node.
   * @return {goog.dom.TextRange} A range wrapper object.
   */
  goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
    return goog.dom.TextRange.createFromBrowserRangeWrapper_(
        goog.dom.browserrange.createRangeFromNodeContents(node),
        opt_isReversed);
  };
  
  
  /**
   * Create a new range wrapper that selects the area between the given nodes,
   * accounting for the given offsets.  Do not use this method directly - please
   * use goog.dom.Range.createFrom* instead.
   * @param {Node} anchorNode The node to start with.
   * @param {number} anchorOffset The offset within the node to start.
   * @param {Node} focusNode The node to end with.
   * @param {number} focusOffset The offset within the node to end.
   * @return {goog.dom.TextRange} A range wrapper object.
   */
  goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset,
      focusNode, focusOffset) {
    var range = new goog.dom.TextRange();
    range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset,
        focusNode, focusOffset);
  
    // Avoid selecting BRs directly
    if (anchorNode.tagName == 'BR') {
      var parent = anchorNode.parentNode;
      anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
      anchorNode = parent;
    }
  
    if (focusNode.tagName == 'BR') {
      var parent = focusNode.parentNode;
      focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
      focusNode = parent;
    }
  
    // Initialize the range as a W3C style range.
    if (range.isReversed_) {
      range.startNode_ = focusNode;
      range.startOffset_ = focusOffset;
      range.endNode_ = anchorNode;
      range.endOffset_ = anchorOffset;
    } else {
      range.startNode_ = anchorNode;
      range.startOffset_ = anchorOffset;
      range.endNode_ = focusNode;
      range.endOffset_ = focusOffset;
    }
  
    return range;
  };
  
  
  // Representation 1: a browser range wrapper.
  
  
  /**
   * The browser specific range wrapper.  This can be null if one of the other
   * representations of the range is specified.
   * @type {goog.dom.browserrange.AbstractRange?}
   * @private
   */
  goog.dom.TextRange.prototype.browserRangeWrapper_ = null;
  
  
  // Representation 2: two endpoints specified as nodes + offsets
  
  
  /**
   * The start node of the range.  This can be null if one of the other
   * representations of the range is specified.
   * @type {Node}
   * @private
   */
  goog.dom.TextRange.prototype.startNode_ = null;
  
  
  /**
   * The start offset of the range.  This can be null if one of the other
   * representations of the range is specified.
   * @type {?number}
   * @private
   */
  goog.dom.TextRange.prototype.startOffset_ = null;
  
  
  /**
   * The end node of the range.  This can be null if one of the other
   * representations of the range is specified.
   * @type {Node}
   * @private
   */
  goog.dom.TextRange.prototype.endNode_ = null;
  
  
  /**
   * The end offset of the range.  This can be null if one of the other
   * representations of the range is specified.
   * @type {?number}
   * @private
   */
  goog.dom.TextRange.prototype.endOffset_ = null;
  
  
  /**
   * Whether the focus node is before the anchor node.
   * @type {boolean}
   * @private
   */
  goog.dom.TextRange.prototype.isReversed_ = false;
  
  
  // Method implementations
  
  
  /**
   * @return {goog.dom.TextRange} A clone of this range.
   */
  goog.dom.TextRange.prototype.clone = function() {
    var range = new goog.dom.TextRange();
    range.browserRangeWrapper_ = this.browserRangeWrapper_;
    range.startNode_ = this.startNode_;
    range.startOffset_ = this.startOffset_;
    range.endNode_ = this.endNode_;
    range.endOffset_ = this.endOffset_;
    range.isReversed_ = this.isReversed_;
  
    return range;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getType = function() {
    return goog.dom.RangeType.TEXT;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
    return this.getBrowserRangeWrapper_().getBrowserRange();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
    // Test if it's a control range by seeing if a control range only method
    // exists.
    if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
      return false;
    }
    this.browserRangeWrapper_ = goog.dom.browserrange.createRange(
        nativeRange);
    this.clearCachedValues_();
    return true;
  };
  
  
  /**
   * Clear all cached values.
   * @private
   */
  goog.dom.TextRange.prototype.clearCachedValues_ = function() {
    this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getTextRangeCount = function() {
    return 1;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getTextRange = function(i) {
    return this;
  };
  
  
  /**
   * @return {goog.dom.browserrange.AbstractRange} The range wrapper object.
   * @private
   */
  goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
    return this.browserRangeWrapper_ ||
        (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(
            this.getStartNode(), this.getStartOffset(),
            this.getEndNode(), this.getEndOffset()));
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getContainer = function() {
    return this.getBrowserRangeWrapper_().getContainer();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getStartNode = function() {
    return this.startNode_ ||
        (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getStartOffset = function() {
    return this.startOffset_ != null ? this.startOffset_ :
        (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getEndNode = function() {
    return this.endNode_ ||
        (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getEndOffset = function() {
    return this.endOffset_ != null ? this.endOffset_ :
        (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());
  };
  
  
  /**
   * Moves a TextRange to the provided nodes and offsets.
   * @param {Node} startNode The node to start with.
   * @param {number} startOffset The offset within the node to start.
   * @param {Node} endNode The node to end with.
   * @param {number} endOffset The offset within the node to end.
   * @param {boolean} isReversed Whether the range is reversed.
   */
  goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset,
                                                      endNode, endOffset,
                                                      isReversed) {
    this.startNode_ = startNode;
    this.startOffset_ = startOffset;
    this.endNode_ = endNode;
    this.endOffset_ = endOffset;
    this.isReversed_ = isReversed;
    this.browserRangeWrapper_ = null;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.isReversed = function() {
    return this.isReversed_;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.containsRange = function(otherRange,
                                                        opt_allowPartial) {
    var otherRangeType = otherRange.getType();
    if (otherRangeType == goog.dom.RangeType.TEXT) {
      return this.getBrowserRangeWrapper_().containsRange(
          otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
    } else if (otherRangeType == goog.dom.RangeType.CONTROL) {
      var elements = otherRange.getElements();
      var fn = opt_allowPartial ? goog.array.some : goog.array.every;
      return fn(elements, function(el) {
        return this.containsNode(el, opt_allowPartial);
      }, this);
    }
    return false;
  };
  
  
  /**
   * Tests if the given node is in a document.
   * @param {Node} node The node to check.
   * @return {boolean} Whether the given node is in the given document.
   */
  goog.dom.TextRange.isAttachedNode = function(node) {
    if (goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) {
      var returnValue = false;
      /** @preserveTry */
      try {
        returnValue = node.parentNode;
      } catch (e) {
        // IE sometimes throws Invalid Argument errors when a node is detached.
        // Note: trying to return a value from the above try block can cause IE
        // to crash.  It is necessary to use the local returnValue
      }
      return !!returnValue;
    } else {
      return goog.dom.contains(node.ownerDocument.body, node);
    }
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.isRangeInDocument = function() {
    // Ensure any cached nodes are in the document.  IE also allows ranges to
    // become detached, so we check if the range is still in the document as
    // well for IE.
    return (!this.startNode_ ||
            goog.dom.TextRange.isAttachedNode(this.startNode_)) &&
           (!this.endNode_ ||
            goog.dom.TextRange.isAttachedNode(this.endNode_)) &&
           (!(goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) ||
            this.getBrowserRangeWrapper_().isRangeInDocument());
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.isCollapsed = function() {
    return this.getBrowserRangeWrapper_().isCollapsed();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getText = function() {
    return this.getBrowserRangeWrapper_().getText();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getHtmlFragment = function() {
    // TODO(robbyw): Generalize the code in browserrange so it is static and
    // just takes an iterator.  This would mean we don't always have to create a
    // browser range.
    return this.getBrowserRangeWrapper_().getHtmlFragment();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getValidHtml = function() {
    return this.getBrowserRangeWrapper_().getValidHtml();
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.getPastableHtml = function() {
    // TODO(robbyw): Get any attributes the table or tr has.
  
    var html = this.getValidHtml();
  
    if (html.match(/^\s*<td\b/i)) {
      // Match html starting with a TD.
      html = '<table><tbody><tr>' + html + '</tr></tbody></table>';
    } else if (html.match(/^\s*<tr\b/i)) {
      // Match html starting with a TR.
      html = '<table><tbody>' + html + '</tbody></table>';
    } else if (html.match(/^\s*<tbody\b/i)) {
      // Match html starting with a TBODY.
      html = '<table>' + html + '</table>';
    } else if (html.match(/^\s*<li\b/i)) {
      // Match html starting with an LI.
      var container = this.getContainer();
      var tagType = goog.dom.TagName.UL;
      while (container) {
        if (container.tagName == goog.dom.TagName.OL) {
          tagType = goog.dom.TagName.OL;
          break;
        } else if (container.tagName == goog.dom.TagName.UL) {
          break;
        }
        container = container.parentNode;
      }
      html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');
    }
  
    return html;
  };
  
  
  /**
   * Returns a TextRangeIterator over the contents of the range.  Regardless of
   * the direction of the range, the iterator will move in document order.
   * @param {boolean=} opt_keys Unused for this iterator.
   * @return {goog.dom.TextRangeIterator} An iterator over tags in the range.
   */
  goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
    return new goog.dom.TextRangeIterator(this.getStartNode(),
        this.getStartOffset(), this.getEndNode(), this.getEndOffset());
  };
  
  
  // RANGE ACTIONS
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.select = function() {
    this.getBrowserRangeWrapper_().select(this.isReversed_);
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.removeContents = function() {
    this.getBrowserRangeWrapper_().removeContents();
    this.clearCachedValues_();
  };
  
  
  /**
   * Surrounds the text range with the specified element (on Mozilla) or with a
   * clone of the specified element (on IE).  Returns a reference to the
   * surrounding element if the operation was successful; returns null if the
   * operation failed.
   * @param {Element} element The element with which the selection is to be
   *    surrounded.
   * @return {Element} The surrounding element (same as the argument on Mozilla,
   *    but not on IE), or null if unsuccessful.
   */
  goog.dom.TextRange.prototype.surroundContents = function(element) {
    var output = this.getBrowserRangeWrapper_().surroundContents(element);
    this.clearCachedValues_();
    return output;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.insertNode = function(node, before) {
    var output = this.getBrowserRangeWrapper_().insertNode(node, before);
    this.clearCachedValues_();
    return output;
  };
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
    this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
    this.clearCachedValues_();
  };
  
  
  // SAVE/RESTORE
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.saveUsingDom = function() {
    return new goog.dom.DomSavedTextRange_(this);
  };
  
  
  // RANGE MODIFICATION
  
  
  /** @inheritDoc */
  goog.dom.TextRange.prototype.collapse = function(toAnchor) {
    var toStart = this.isReversed() ? !toAnchor : toAnchor;
  
    if (this.browserRangeWrapper_) {
      this.browserRangeWrapper_.collapse(toStart);
    }
  
    if (toStart) {
      this.endNode_ = this.startNode_;
      this.endOffset_ = this.startOffset_;
    } else {
      this.startNode_ = this.endNode_;
      this.startOffset_ = this.endOffset_;
    }
  
    // Collapsed ranges can't be reversed
    this.isReversed_ = false;
  };
  
  
  // SAVED RANGE OBJECTS
  
  
  
  /**
   * A SavedRange implementation using DOM endpoints.
   * @param {goog.dom.AbstractRange} range The range to save.
   * @constructor
   * @extends {goog.dom.SavedRange}
   * @private
   */
  goog.dom.DomSavedTextRange_ = function(range) {
    /**
     * The anchor node.
     * @type {Node}
     * @private
     */
    this.anchorNode_ = range.getAnchorNode();
  
    /**
     * The anchor node offset.
     * @type {number}
     * @private
     */
    this.anchorOffset_ = range.getAnchorOffset();
  
    /**
     * The focus node.
     * @type {Node}
     * @private
     */
    this.focusNode_ = range.getFocusNode();
  
    /**
     * The focus node offset.
     * @type {number}
     * @private
     */
    this.focusOffset_ = range.getFocusOffset();
  };
  goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
  
  
  /**
   * @return {goog.dom.AbstractRange} The restored range.
   */
  goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
    return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_,
        this.focusNode_, this.focusOffset_);
  };
  
  
  /** @inheritDoc */
  goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
    goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
  
    this.anchorNode_ = null;
    this.focusNode_ = null;
  };
  