// 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 Interface definitions for working with ranges
 * in HTML documents.
 *
 * @author robbyw@google.com (Robby Walker)
 */


goog.provide('goog.dom.AbstractRange');
goog.provide('goog.dom.RangeIterator');
goog.provide('goog.dom.RangeType');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.SavedCaretRange');
goog.require('goog.dom.TagIterator');
goog.require('goog.userAgent');


/**
 * Types of ranges.
 * @enum {string}
 */
goog.dom.RangeType = {
  TEXT: 'text',
  CONTROL: 'control',
  MULTI: 'mutli'
};



/**
 * Creates a new selection with no properties.  Do not use this constructor -
 * use one of the goog.dom.Range.from* methods instead.
 * @constructor
 */
goog.dom.AbstractRange = function() {};


/**
 * Gets the browser native selection object from the given window.
 * @param {Window} win The window to get the selection object from.
 * @return {Object} The browser native selection object, or null if it could
 *     not be retrieved.
 */
goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) {
  if (win.getSelection) {
    // W3C
    return win.getSelection();
  } else {
    // IE
    var doc = win.document;
    var sel = doc.selection;
    if (sel) {
      // IE has a bug where it sometimes returns a selection from the wrong
      // document. Catching these cases now helps us avoid problems later.
      try {
        var range = sel.createRange();
        // Only TextRanges have a parentElement method.
        if (range.parentElement) {
          if (range.parentElement().document != doc) {
            return null;
          }
        } else if (
            !range.length ||
            /** @type {ControlRange} */ (range).item(0).document != doc) {
          // For ControlRanges, check that the range has items, and that
          // the first item in the range is in the correct document.
          return null;
        }
      } catch (e) {
        // If the selection is in the wrong document, and the wrong document is
        // in a different domain, IE will throw an exception.
        return null;
      }
      // TODO(user|robbyw) Sometimes IE 6 returns a selection instance
      // when there is no selection.  This object has a 'type' property equals
      // to 'None' and a typeDetail property bound to undefined. Ideally this
      // function should not return this instance.
      return sel;
    }
    return null;
  }
};


/**
 * Tests if the given Object is a controlRange.
 * @param {Object} range The range object to test.
 * @return {boolean} Whether the given Object is a controlRange.
 */
goog.dom.AbstractRange.isNativeControlRange = function(range) {
  // For now, tests for presence of a control range function.
  return !!range && !!range.addElement;
};


/**
 * @return {!goog.dom.AbstractRange} A clone of this range.
 */
goog.dom.AbstractRange.prototype.clone = goog.abstractMethod;


/**
 * @return {goog.dom.RangeType} The type of range represented by this object.
 */
goog.dom.AbstractRange.prototype.getType = goog.abstractMethod;


/**
 * @return {Range|TextRange} The native browser range object.
 */
goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod;


/**
 * Sets the native browser range object, overwriting any state this range was
 * storing.
 * @param {Range|TextRange} nativeRange The native browser range object.
 * @return {boolean} Whether the given range was accepted.  If not, the caller
 *     will need to call goog.dom.Range.createFromBrowserRange to create a new
 *     range object.
 */
goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) {
  return false;
};


/**
 * @return {number} The number of text ranges in this range.
 */
goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod;


/**
 * Get the i-th text range in this range.  The behavior is undefined if
 * i >= getTextRangeCount or i < 0.
 * @param {number} i The range number to retrieve.
 * @return {goog.dom.TextRange} The i-th text range.
 */
goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod;


/**
 * Gets an array of all text ranges this range is comprised of.  For non-multi
 * ranges, returns a single element array containing this.
 * @return {!Array<goog.dom.TextRange>} Array of text ranges.
 */
goog.dom.AbstractRange.prototype.getTextRanges = function() {
  var output = [];
  for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {
    output.push(this.getTextRange(i));
  }
  return output;
};


/**
 * @return {Node} The deepest node that contains the entire range.
 */
goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod;


/**
 * Returns the deepest element in the tree that contains the entire range.
 * @return {Element} The deepest element that contains the entire range.
 */
goog.dom.AbstractRange.prototype.getContainerElement = function() {
  var node = this.getContainer();
  return /** @type {Element} */ (
      node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode);
};


/**
 * @return {Node} The element or text node the range starts in.  For text
 *     ranges, the range comprises all text between the start and end position.
 *     For other types of range, start and end give bounds of the range but
 *     do not imply all nodes in those bounds are selected.
 */
goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod;


/**
 * @return {number} The offset into the node the range starts in.  For text
 *     nodes, this is an offset into the node value.  For elements, this is
 *     an offset into the childNodes array.
 */
goog.dom.AbstractRange.prototype.getStartOffset = goog.abstractMethod;


/**
 * @return {goog.math.Coordinate} The coordinate of the selection start node
 *     and offset.
 */
goog.dom.AbstractRange.prototype.getStartPosition = goog.abstractMethod;


/**
 * @return {Node} The element or text node the range ends in.
 */
goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod;


/**
 * @return {number} The offset into the node the range ends in.  For text
 *     nodes, this is an offset into the node value.  For elements, this is
 *     an offset into the childNodes array.
 */
goog.dom.AbstractRange.prototype.getEndOffset = goog.abstractMethod;


/**
 * @return {goog.math.Coordinate} The coordinate of the selection end
 *     node and offset.
 */
goog.dom.AbstractRange.prototype.getEndPosition = goog.abstractMethod;


/**
 * @return {Node} The element or text node the range is anchored at.
 */
goog.dom.AbstractRange.prototype.getAnchorNode = function() {
  return this.isReversed() ? this.getEndNode() : this.getStartNode();
};


/**
 * @return {number} The offset into the node the range is anchored at.  For
 *     text nodes, this is an offset into the node value.  For elements, this
 *     is an offset into the childNodes array.
 */
goog.dom.AbstractRange.prototype.getAnchorOffset = function() {
  return this.isReversed() ? this.getEndOffset() : this.getStartOffset();
};


/**
 * @return {Node} The element or text node the range is focused at - i.e. where
 *     the cursor is.
 */
goog.dom.AbstractRange.prototype.getFocusNode = function() {
  return this.isReversed() ? this.getStartNode() : this.getEndNode();
};


/**
 * @return {number} The offset into the node the range is focused at - i.e.
 *     where the cursor is.  For text nodes, this is an offset into the node
 *     value.  For elements, this is an offset into the childNodes array.
 */
goog.dom.AbstractRange.prototype.getFocusOffset = function() {
  return this.isReversed() ? this.getStartOffset() : this.getEndOffset();
};


/**
 * @return {boolean} Whether the selection is reversed.
 */
goog.dom.AbstractRange.prototype.isReversed = function() {
  return false;
};


/**
 * @return {!Document} The document this selection is a part of.
 */
goog.dom.AbstractRange.prototype.getDocument = function() {
  // Using start node in IE was crashing the browser in some cases so use
  // getContainer for that browser. It's also faster for IE, but still slower
  // than start node for other browsers so we continue to use getStartNode when
  // it is not problematic. See bug 1687309.
  return goog.dom.getOwnerDocument(
      goog.userAgent.IE ? this.getContainer() : this.getStartNode());
};


/**
 * @return {!Window} The window this selection is a part of.
 */
goog.dom.AbstractRange.prototype.getWindow = function() {
  return goog.dom.getWindow(this.getDocument());
};


/**
 * Tests if this range contains the given range.
 * @param {goog.dom.AbstractRange} range The range to test.
 * @param {boolean=} opt_allowPartial If true, the range can be partially
 *     contained in the selection, otherwise the range must be entirely
 *     contained.
 * @return {boolean} Whether this range contains the given range.
 */
goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod;


/**
 * Tests if this range contains the given node.
 * @param {Node} node The node to test for.
 * @param {boolean=} opt_allowPartial If not set or false, the node must be
 *     entirely contained in the selection for this function to return true.
 * @return {boolean} Whether this range contains the given node.
 */
goog.dom.AbstractRange.prototype.containsNode = goog.abstractMethod;



/**
 * Tests whether this range is valid (i.e. whether its endpoints are still in
 * the document).  A range becomes invalid when, after this object was created,
 * either one or both of its endpoints are removed from the document.  Use of
 * an invalid range can lead to runtime errors, particularly in IE.
 * @return {boolean} Whether the range is valid.
 */
goog.dom.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod;


/**
 * @return {boolean} Whether the range is collapsed.
 */
goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod;


/**
 * @return {string} The text content of the range.
 */
goog.dom.AbstractRange.prototype.getText = goog.abstractMethod;


/**
 * Returns the HTML fragment this range selects.  This is slow on all browsers.
 * The HTML fragment may not be valid HTML, for instance if the user selects
 * from a to b inclusively in the following html:
 *
 * &lt;div&gt;a&lt;/div&gt;b
 *
 * This method will return
 *
 * a&lt;/div&gt;b
 *
 * If you need valid HTML, use {@link #getValidHtml} instead.
 *
 * @return {string} HTML fragment of the range, does not include context
 *     containing elements.
 */
goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod;


/**
 * Returns valid HTML for this range.  This is fast on IE, and semi-fast on
 * other browsers.
 * @return {string} Valid HTML of the range, including context containing
 *     elements.
 */
goog.dom.AbstractRange.prototype.getValidHtml = goog.abstractMethod;


/**
 * Returns pastable HTML for this range.  This guarantees that any child items
 * that must have specific ancestors will have them, for instance all TDs will
 * be contained in a TR in a TBODY in a TABLE and all LIs will be contained in
 * a UL or OL as appropriate.  This is semi-fast on all browsers.
 * @return {string} Pastable HTML of the range, including context containing
 *     elements.
 */
goog.dom.AbstractRange.prototype.getPastableHtml = goog.abstractMethod;


/**
 * Returns a RangeIterator 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.RangeIterator} An iterator over tags in the range.
 */
goog.dom.AbstractRange.prototype.__iterator__ = goog.abstractMethod;


// RANGE ACTIONS


/**
 * Sets this range as the selection in its window.
 */
goog.dom.AbstractRange.prototype.select = goog.abstractMethod;


/**
 * Removes the contents of the range from the document.
 */
goog.dom.AbstractRange.prototype.removeContents = goog.abstractMethod;


/**
 * Inserts a node before (or after) the range.  The range may be disrupted
 * beyond recovery because of the way this splits nodes.
 * @param {Node} node The node to insert.
 * @param {boolean} before True to insert before, false to insert after.
 * @return {Node} The node added to the document.  This may be different
 *     than the node parameter because on IE we have to clone it.
 */
goog.dom.AbstractRange.prototype.insertNode = goog.abstractMethod;


/**
 * Replaces the range contents with (possibly a copy of) the given node.  The
 * range may be disrupted beyond recovery because of the way this splits nodes.
 * @param {Node} node The node to insert.
 * @return {Node} The node added to the document.  This may be different
 *     than the node parameter because on IE we have to clone it.
 */
goog.dom.AbstractRange.prototype.replaceContentsWithNode = function(node) {
  if (!this.isCollapsed()) {
    this.removeContents();
  }

  return this.insertNode(node, true);
};


/**
 * Surrounds this range with the two given nodes.  The range may be disrupted
 * beyond recovery because of the way this splits nodes.
 * @param {Element} startNode The node to insert at the start.
 * @param {Element} endNode The node to insert at the end.
 */
goog.dom.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;


// SAVE/RESTORE


/**
 * Saves the range so that if the start and end nodes are left alone, it can
 * be restored.
 * @return {!goog.dom.SavedRange} A range representation that can be restored
 *     as long as the endpoint nodes of the selection are not modified.
 */
goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod;


/**
 * Saves the range using HTML carets. As long as the carets remained in the
 * HTML, the range can be restored...even when the HTML is copied across
 * documents.
 * @return {goog.dom.SavedCaretRange?} A range representation that can be
 *     restored as long as carets are not removed. Returns null if carets
 *     could not be created.
 */
goog.dom.AbstractRange.prototype.saveUsingCarets = function() {
  return (this.getStartNode() && this.getEndNode()) ?
      new goog.dom.SavedCaretRange(this) :
      null;
};


// RANGE MODIFICATION


/**
 * Collapses the range to one of its boundary points.
 * @param {boolean} toAnchor Whether to collapse to the anchor of the range.
 */
goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod;

// RANGE ITERATION



/**
 * Subclass of goog.dom.TagIterator that iterates over a DOM range.  It
 * adds functions to determine the portion of each text node that is selected.
 * @param {Node} node The node to start traversal at.  When null, creates an
 *     empty iterator.
 * @param {boolean=} opt_reverse Whether to traverse nodes in reverse.
 * @constructor
 * @extends {goog.dom.TagIterator}
 */
goog.dom.RangeIterator = function(node, opt_reverse) {
  goog.dom.TagIterator.call(this, node, opt_reverse, true);
};
goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator);


/**
 * @return {number} The offset into the current node, or -1 if the current node
 *     is not a text node.
 */
goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod;


/**
 * @return {number} The end offset into the current node, or -1 if the current
 *     node is not a text node.
 */
goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod;


/**
 * @return {Node} node The iterator's start node.
 */
goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod;


/**
 * @return {Node} The iterator's end node.
 */
goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod;


/**
 * @return {boolean} Whether a call to next will fail.
 */
goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod;
