// Copyright 2006 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 selections in input boxes and text
 * areas.
 *
 * @author arv@google.com (Erik Arvidsson)
 * @see ../demos/dom_selection.html
 */


goog.provide('goog.dom.selection');

goog.require('goog.string');
goog.require('goog.userAgent');


/**
 * Sets the place where the selection should start inside a textarea or a text
 * input
 * @param {Element} textfield A textarea or text input.
 * @param {number} pos The position to set the start of the selection at.
 */
goog.dom.selection.setStart = function(textfield, pos) {
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    textfield.selectionStart = pos;
  } else if (goog.userAgent.IE) {
    // destructuring assignment would have been sweet
    var tmp = goog.dom.selection.getRangeIe_(textfield);
    var range = tmp[0];
    var selectionRange = tmp[1];

    if (range.inRange(selectionRange)) {
      pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);

      range.collapse(true);
      range.move('character', pos);
      range.select();
    }
  }
};


/**
 * Return the place where the selection starts inside a textarea or a text
 * input
 * @param {Element} textfield A textarea or text input.
 * @return {number} The position where the selection starts or 0 if it was
 *     unable to find the position or no selection exists. Note that we can't
 *     reliably tell the difference between an element that has no selection and
 *     one where it starts at 0.
 */
goog.dom.selection.getStart = function(textfield) {
  return goog.dom.selection.getEndPoints_(textfield, true)[0];
};


/**
 * Returns the start and end points of the selection within a textarea in IE.
 * IE treats newline characters as \r\n characters, and we need to check for
 * these characters at the edge of our selection, to ensure that we return the
 * right cursor position.
 * @param {TextRange} range Complete range object, e.g., "Hello\r\n".
 * @param {TextRange} selRange Selected range object.
 * @param {boolean} getOnlyStart Value indicating if only start
 *     cursor position is to be returned. In IE, obtaining the end position
 *     involves extra work, hence we have this parameter for calls which need
 *     only start position.
 * @return {!Array<number>} An array with the start and end positions where the
 *     selection starts and ends or [0,0] if it was unable to find the
 *     positions or no selection exists. Note that we can't reliably tell the
 *     difference between an element that has no selection and one where
 *     it starts and ends at 0. If getOnlyStart was true, we return
 *     -1 as end offset.
 * @private
 */
goog.dom.selection.getEndPointsTextareaIe_ = function(
    range, selRange, getOnlyStart) {
  // Create a duplicate of the selected range object to perform our actions
  // against. Example of selectionRange = "" (assuming that the cursor is
  // just after the \r\n combination)
  var selectionRange = selRange.duplicate();

  // Text before the selection start, e.g.,"Hello" (notice how range.text
  // excludes the \r\n sequence)
  var beforeSelectionText = range.text;
  // Text before the selection start, e.g., "Hello" (this will later include
  // the \r\n sequences also)
  var untrimmedBeforeSelectionText = beforeSelectionText;
  // Text within the selection , e.g. "" assuming that the cursor is just after
  // the \r\n combination.
  var selectionText = selectionRange.text;
  // Text within the selection, e.g.,  "" (this will later include the \r\n
  // sequences also)
  var untrimmedSelectionText = selectionText;

  // Boolean indicating whether we are done dealing with the text before the
  // selection's beginning.
  var isRangeEndTrimmed = false;
  // Go over the range until it becomes a 0-lengthed range or until the range
  // text starts changing when we move the end back by one character.
  // If after moving the end back by one character, the text remains the same,
  // then we need to add a "\r\n" at the end to get the actual text.
  while (!isRangeEndTrimmed) {
    if (range.compareEndPoints('StartToEnd', range) == 0) {
      isRangeEndTrimmed = true;
    } else {
      range.moveEnd('character', -1);
      if (range.text == beforeSelectionText) {
        // If the start position of the cursor was after a \r\n string,
        // we would skip over it in one go with the moveEnd call, but
        // range.text will still show "Hello" (because of the IE range.text
        // bug) - this implies that we should add a \r\n to our
        // untrimmedBeforeSelectionText string.
        untrimmedBeforeSelectionText += '\r\n';
      } else {
        isRangeEndTrimmed = true;
      }
    }
  }

  if (getOnlyStart) {
    // We return -1 as end, since the caller is only interested in the start
    // value.
    return [untrimmedBeforeSelectionText.length, -1];
  }
  // Boolean indicating whether we are done dealing with the text inside the
  // selection.
  var isSelectionRangeEndTrimmed = false;
  // Go over the selected range until it becomes a 0-lengthed range or until
  // the range text starts changing when we move the end back by one character.
  // If after moving the end back by one character, the text remains the same,
  // then we need to add a "\r\n" at the end to get the actual text.
  while (!isSelectionRangeEndTrimmed) {
    if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
      isSelectionRangeEndTrimmed = true;
    } else {
      selectionRange.moveEnd('character', -1);
      if (selectionRange.text == selectionText) {
        // If the selection was not empty, and the end point of the selection
        // was just after a \r\n, we would have skipped it in one go with the
        // moveEnd call, and this implies that we should add a \r\n to the
        // untrimmedSelectionText string.
        untrimmedSelectionText += '\r\n';
      } else {
        isSelectionRangeEndTrimmed = true;
      }
    }
  }
  return [
    untrimmedBeforeSelectionText.length,
    untrimmedBeforeSelectionText.length + untrimmedSelectionText.length];
};


/**
 * Returns the start and end points of the selection inside a textarea or a
 * text input.
 * @param {Element} textfield A textarea or text input.
 * @return {!Array<number>} An array with the start and end positions where the
 *     selection starts and ends or [0,0] if it was unable to find the
 *     positions or no selection exists. Note that we can't reliably tell the
 *     difference between an element that has no selection and one where
 *     it starts and ends at 0.
 */
goog.dom.selection.getEndPoints = function(textfield) {
  return goog.dom.selection.getEndPoints_(textfield, false);
};


/**
 * Returns the start and end points of the selection inside a textarea or a
 * text input.
 * @param {Element} textfield A textarea or text input.
 * @param {boolean} getOnlyStart Value indicating if only start
 *     cursor position is to be returned. In IE, obtaining the end position
 *     involves extra work, hence we have this parameter. In FF, there is not
 *     much extra effort involved.
 * @return {!Array<number>} An array with the start and end positions where the
 *     selection starts and ends or [0,0] if it was unable to find the
 *     positions or no selection exists. Note that we can't reliably tell the
 *     difference between an element that has no selection and one where
 *     it starts and ends at 0. If getOnlyStart was true, we return
 *     -1 as end offset.
 * @private
 */
goog.dom.selection.getEndPoints_ = function(textfield, getOnlyStart) {
  var startPos = 0;
  var endPos = 0;
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    startPos = textfield.selectionStart;
    endPos = getOnlyStart ? -1 : textfield.selectionEnd;
  } else if (goog.userAgent.IE) {
    var tmp = goog.dom.selection.getRangeIe_(textfield);
    var range = tmp[0];
    var selectionRange = tmp[1];

    if (range.inRange(selectionRange)) {
      range.setEndPoint('EndToStart', selectionRange);
      if (textfield.type == 'textarea') {
        return goog.dom.selection.getEndPointsTextareaIe_(
            range, selectionRange, getOnlyStart);
      }
      startPos = range.text.length;
      if (!getOnlyStart) {
        endPos = range.text.length + selectionRange.text.length;
      } else {
        endPos = -1;  // caller did not ask for end position
      }
    }
  }
  return [startPos, endPos];
};


/**
 * Sets the place where the selection should end inside a text area or a text
 * input
 * @param {Element} textfield A textarea or text input.
 * @param {number} pos The position to end the selection at.
 */
goog.dom.selection.setEnd = function(textfield, pos) {
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    textfield.selectionEnd = pos;
  } else if (goog.userAgent.IE) {
    var tmp = goog.dom.selection.getRangeIe_(textfield);
    var range = tmp[0];
    var selectionRange = tmp[1];

    if (range.inRange(selectionRange)) {
      // Both the current position and the start cursor position need
      // to be canonicalized to take care of possible \r\n miscounts.
      pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
      var startCursorPos = goog.dom.selection.canonicalizePositionIe_(
          textfield, goog.dom.selection.getStart(textfield));

      selectionRange.collapse(true);
      selectionRange.moveEnd('character', pos - startCursorPos);
      selectionRange.select();
    }
  }
};


/**
 * Returns the place where the selection ends inside a textarea or a text input
 * @param {Element} textfield A textarea or text input.
 * @return {number} The position where the selection ends or 0 if it was
 *     unable to find the position or no selection exists.
 */
goog.dom.selection.getEnd = function(textfield) {
  return goog.dom.selection.getEndPoints_(textfield, false)[1];
};


/**
 * Sets the cursor position within a textfield.
 * @param {Element} textfield A textarea or text input.
 * @param {number} pos The position within the text field.
 */
goog.dom.selection.setCursorPosition = function(textfield, pos) {
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    // Mozilla directly supports this
    textfield.selectionStart = pos;
    textfield.selectionEnd = pos;

  } else if (goog.userAgent.IE) {
    pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);

    // IE has textranges. A textfield's textrange encompasses the
    // entire textfield's text by default
    var sel = textfield.createTextRange();

    sel.collapse(true);
    sel.move('character', pos);
    sel.select();
  }
};


/**
 * Sets the selected text inside a textarea or a text input
 * @param {Element} textfield A textarea or text input.
 * @param {string} text The text to change the selection to.
 */
goog.dom.selection.setText = function(textfield, text) {
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    var value = textfield.value;
    var oldSelectionStart = textfield.selectionStart;
    var before = value.substr(0, oldSelectionStart);
    var after = value.substr(textfield.selectionEnd);
    textfield.value = before + text + after;
    textfield.selectionStart = oldSelectionStart;
    textfield.selectionEnd = oldSelectionStart + text.length;
  } else if (goog.userAgent.IE) {
    var tmp = goog.dom.selection.getRangeIe_(textfield);
    var range = tmp[0];
    var selectionRange = tmp[1];

    if (!range.inRange(selectionRange)) {
      return;
    }
    // When we set the selection text the selection range is collapsed to the
    // end. We therefore duplicate the current selection so we know where it
    // started. Once we've set the selection text we move the start of the
    // selection range to the old start
    var range2 = selectionRange.duplicate();
    selectionRange.text = text;
    selectionRange.setEndPoint('StartToStart', range2);
    selectionRange.select();
  } else {
    throw Error('Cannot set the selection end');
  }
};


/**
 * Returns the selected text inside a textarea or a text input
 * @param {Element} textfield A textarea or text input.
 * @return {string} The selected text.
 */
goog.dom.selection.getText = function(textfield) {
  if (goog.dom.selection.useSelectionProperties_(textfield)) {
    var s = textfield.value;
    return s.substring(textfield.selectionStart, textfield.selectionEnd);
  }

  if (goog.userAgent.IE) {
    var tmp = goog.dom.selection.getRangeIe_(textfield);
    var range = tmp[0];
    var selectionRange = tmp[1];

    if (!range.inRange(selectionRange)) {
      return '';
    } else if (textfield.type == 'textarea') {
      return goog.dom.selection.getSelectionRangeText_(selectionRange);
    }
    return selectionRange.text;
  }

  throw Error('Cannot get the selection text');
};


/**
 * Returns the selected text within a textarea in IE.
 * IE treats newline characters as \r\n characters, and we need to check for
 * these characters at the edge of our selection, to ensure that we return the
 * right string.
 * @param {TextRange} selRange Selected range object.
 * @return {string} Selected text in the textarea.
 * @private
 */
goog.dom.selection.getSelectionRangeText_ = function(selRange) {
  // Create a duplicate of the selected range object to perform our actions
  // against. Suppose the text in the textarea is "Hello\r\nWorld" and the
  // selection encompasses the "o\r\n" bit, initial selectionRange will be "o"
  // (assuming that the cursor is just after the \r\n combination)
  var selectionRange = selRange.duplicate();

  // Text within the selection , e.g. "o" assuming that the cursor is just after
  // the \r\n combination.
  var selectionText = selectionRange.text;
  // Text within the selection, e.g.,  "o" (this will later include the \r\n
  // sequences also)
  var untrimmedSelectionText = selectionText;

  // Boolean indicating whether we are done dealing with the text inside the
  // selection.
  var isSelectionRangeEndTrimmed = false;
  // Go over the selected range until it becomes a 0-lengthed range or until
  // the range text starts changing when we move the end back by one character.
  // If after moving the end back by one character, the text remains the same,
  // then we need to add a "\r\n" at the end to get the actual text.
  while (!isSelectionRangeEndTrimmed) {
    if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
      isSelectionRangeEndTrimmed = true;
    } else {
      selectionRange.moveEnd('character', -1);
      if (selectionRange.text == selectionText) {
        // If the selection was not empty, and the end point of the selection
        // was just after a \r\n, we would have skipped it in one go with the
        // moveEnd call, and this implies that we should add a \r\n to the
        // untrimmedSelectionText string.
        untrimmedSelectionText += '\r\n';
      } else {
        isSelectionRangeEndTrimmed = true;
      }
    }
  }
  return untrimmedSelectionText;
};


/**
 * Helper function for returning the range for an object as well as the
 * selection range
 * @private
 * @param {Element} el The element to get the range for.
 * @return {!Array<TextRange>} Range of object and selection range in two
 *     element array.
 */
goog.dom.selection.getRangeIe_ = function(el) {
  var doc = el.ownerDocument || el.document;

  var selectionRange = doc.selection.createRange();
  // el.createTextRange() doesn't work on textareas
  var range;

  if (el.type == 'textarea') {
    range = doc.body.createTextRange();
    range.moveToElementText(el);
  } else {
    range = el.createTextRange();
  }

  return [range, selectionRange];
};


/**
 * Helper function for canonicalizing a position inside a textfield in IE.
 * Deals with the issue that \r\n counts as 2 characters, but
 * move('character', n) passes over both characters in one move.
 * @private
 * @param {Element} textfield The text element.
 * @param {number} pos The position desired in that element.
 * @return {number} The canonicalized position that will work properly with
 *     move('character', pos).
 */
goog.dom.selection.canonicalizePositionIe_ = function(textfield, pos) {
  if (textfield.type == 'textarea') {
    // We do this only for textarea because it is the only one which can
    // have a \r\n (input cannot have this).
    var value = textfield.value.substring(0, pos);
    pos = goog.string.canonicalizeNewlines(value).length;
  }
  return pos;
};


/**
 * Helper function to determine whether it's okay to use
 * selectionStart/selectionEnd.
 *
 * @param {Element} el The element to check for.
 * @return {boolean} Whether it's okay to use the selectionStart and
 *     selectionEnd properties on {@code el}.
 * @private
 */
goog.dom.selection.useSelectionProperties_ = function(el) {
  try {
    return typeof el.selectionStart == 'number';
  } catch (e) {
    // Firefox throws an exception if you try to access selectionStart
    // on an element with display: none.
    return false;
  }
};
