/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Definition of the browser range interface.
 *
 * DO NOT USE THIS FILE DIRECTLY.  Use goog.dom.Range instead.
 */


goog.provide('goog.dom.browserrange.AbstractRange');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.TagName');
goog.require('goog.dom.TextRangeIterator');
goog.require('goog.iter');
goog.require('goog.math.Coordinate');
goog.require('goog.string');
goog.require('goog.string.StringBuffer');
goog.require('goog.userAgent');
goog.requireType('goog.dom.RangeIterator');



/**
 * The constructor for abstract ranges.  Don't call this from subclasses.
 * @constructor
 */
goog.dom.browserrange.AbstractRange = function() {};


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


/**
 * Returns the browser native implementation of the range.  Please refrain from
 * using this function - if you find you need the range please add wrappers for
 * the functionality you need rather than just using the native range.
 * @return {Range|TextRange} The browser native range object.
 */
goog.dom.browserrange.AbstractRange.prototype.getBrowserRange =
    goog.abstractMethod;


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


/**
 * Returns the node the range starts in.
 * @return {Node} The element or text node the range starts in.
 */
goog.dom.browserrange.AbstractRange.prototype.getStartNode =
    goog.abstractMethod;


/**
 * Returns the offset into the node the range starts in.
 * @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.browserrange.AbstractRange.prototype.getStartOffset =
    goog.abstractMethod;


/**
 * @return {goog.math.Coordinate} The coordinate of the selection start node
 *     and offset.
 */
goog.dom.browserrange.AbstractRange.prototype.getStartPosition = function() {
  'use strict';
  return this.getPosition_(true);
};


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


/**
 * Returns the offset into the node the range ends in.
 * @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.browserrange.AbstractRange.prototype.getEndOffset =
    goog.abstractMethod;


/**
 * @return {goog.math.Coordinate} The coordinate of the selection end node
 *     and offset.
 */
goog.dom.browserrange.AbstractRange.prototype.getEndPosition = function() {
  'use strict';
  return this.getPosition_(false);
};


/**
 * @param {boolean} start Whether to get the position of the start or end.
 * @return {goog.math.Coordinate} The coordinate of the selection point.
 * @private
 * @suppress {missingProperties} circular definitions
 */
goog.dom.browserrange.AbstractRange.prototype.getPosition_ = function(start) {
  'use strict';
  goog.asserts.assert(
      this.range_.getClientRects,
      'Getting selection coordinates is not supported.');

  var rects = this.range_.getClientRects();
  if (rects.length) {
    var r = start ? rects[0] : goog.array.peek(rects);
    return new goog.math.Coordinate(
        start ? r.left : r.right, start ? r.top : r.bottom);
  }
  return null;
};


/**
 * Compares one endpoint of this range with the endpoint of another browser
 * native range object.
 * @param {Range|TextRange} range The browser native range to compare against.
 * @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range
 *     to compare with.
 * @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the other
 *     range to compare with.
 * @return {number} 0 if the endpoints are equal, negative if this range
 *     endpoint comes before the other range endpoint, and positive otherwise.
 */
goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints =
    goog.abstractMethod;


/**
 * Tests if this range contains the given range.
 * @param {goog.dom.browserrange.AbstractRange} abstractRange The range to test.
 * @param {boolean=} opt_allowPartial If not set or false, the range must be
 *     entirely contained in the selection for this function to return true.
 * @return {boolean} Whether this range contains the given range.
 */
goog.dom.browserrange.AbstractRange.prototype.containsRange = function(
    abstractRange, opt_allowPartial) {
  'use strict';
  // IE sometimes misreports the boundaries for collapsed ranges. So if the
  // other range is collapsed, make sure the whole range is contained. This is
  // logically equivalent, and works around IE's bug.
  var checkPartial = opt_allowPartial && !abstractRange.isCollapsed();

  var range = abstractRange.getBrowserRange();
  var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;

  try {
    if (checkPartial) {
      // There are two ways to not overlap.  Being before, and being after.
      // Before is represented by this.end before range.start: comparison < 0.
      // After is represented by this.start after range.end: comparison > 0.
      // The below is the negation of not overlapping.
      return this.compareBrowserRangeEndpoints(range, end, start) >= 0 &&
          this.compareBrowserRangeEndpoints(range, start, end) <= 0;

    } else {
      // Return true if this range bounds the parameter range from both sides.
      return this.compareBrowserRangeEndpoints(range, end, end) >= 0 &&
          this.compareBrowserRangeEndpoints(range, start, start) <= 0;
    }
  } catch (e) {
    if (!goog.userAgent.IE) {
      throw e;
    }
    // IE sometimes throws exceptions when one range is invalid, i.e. points
    // to a node that has been removed from the document.  Return false in this
    // case.
    return false;
  }
};


/**
 * Tests if this range contains the given node.
 * @param {Node} node The node to test.
 * @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.
 * @suppress {missingRequire,missingProperties} Cannot depend on
 *     goog.dom.browserrange because it creates a circular dependency.
 */
goog.dom.browserrange.AbstractRange.prototype.containsNode = function(
    node, opt_allowPartial) {
  'use strict';
  /** @suppress {missingRequire} Circular dep with browserrange */
  return this.containsRange(
      goog.dom.browserrange.createRangeFromNodeContents(node),
      opt_allowPartial);
};


/**
 * Tests if the selection is collapsed - i.e. is just a caret.
 * @return {boolean} Whether the range is collapsed.
 */
goog.dom.browserrange.AbstractRange.prototype.isCollapsed = goog.abstractMethod;


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


/**
 * Returns the HTML fragment this range selects.  This is slow on all browsers.
 * @return {string} HTML fragment of the range, does not include context
 *     containing elements.
 * @suppress {missingProperties}
 */
goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() {
  'use strict';
  var output = new goog.string.StringBuffer();
  goog.iter.forEach(this, function(node, ignore, it) {
    'use strict';
    if (node.nodeType == goog.dom.NodeType.TEXT) {
      output.append(
          goog.string.htmlEscape(
              node.nodeValue.substring(
                  it.getStartTextOffset(), it.getEndTextOffset())));
    } else if (node.nodeType == goog.dom.NodeType.ELEMENT) {
      if (it.isEndTag()) {
        if (goog.dom.canHaveChildren(node)) {
          output.append('</' + node.tagName + '>');
        }
      } else {
        var shallow = node.cloneNode(false);
        var html = goog.dom.getOuterHtml(shallow);
        if (goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) {
          // For an LI, IE just returns "<li>" with no closing tag
          output.append(html);
        } else {
          var index = html.lastIndexOf('<');
          // if index is -1, then this appends nothing.
          // if index is 0, then the entire HTML content should be added.
          // if the index is > 0, then only the portion of the html before the
          // last open tag is appended.
          if (index !== -1) {
            output.append(index > 0 ? html.slice(0, index) : html);
          }
        }
      }
    }
  }, this);

  return output.toString();
};


/**
 * 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.browserrange.AbstractRange.prototype.getValidHtml =
    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.browserrange.AbstractRange.prototype.__iterator__ = function(
    opt_keys) {
  'use strict';
  return new goog.dom.TextRangeIterator(
      this.getStartNode(), this.getStartOffset(), this.getEndNode(),
      this.getEndOffset());
};


// SELECTION MODIFICATION


/**
 * Set this range as the selection in its window.
 * @param {boolean=} opt_reverse Whether to select the range in reverse,
 *     if possible.
 */
goog.dom.browserrange.AbstractRange.prototype.select = goog.abstractMethod;


/**
 * Removes the contents of the range from the document.  As a side effect, the
 * selection will be collapsed.  The behavior of content removal is normalized
 * across browsers.  For instance, IE sometimes creates extra text nodes that
 * a W3C browser does not.  That behavior is corrected for.
 */
goog.dom.browserrange.AbstractRange.prototype.removeContents =
    goog.abstractMethod;


/**
 * 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.browserrange.AbstractRange.prototype.surroundContents =
    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.browserrange.AbstractRange.prototype.insertNode = goog.abstractMethod;


/**
 * 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.browserrange.AbstractRange.prototype.surroundWithNodes =
    goog.abstractMethod;


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