Path: blob/trunk/third_party/closure/goog/dom/selection.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Utilities for working with selections in input boxes and text16* areas.17*18* @author [email protected] (Erik Arvidsson)19* @see ../demos/dom_selection.html20*/212223goog.provide('goog.dom.selection');2425goog.require('goog.dom.InputType');26goog.require('goog.string');27goog.require('goog.userAgent');282930/**31* Sets the place where the selection should start inside a textarea or a text32* input33* @param {Element} textfield A textarea or text input.34* @param {number} pos The position to set the start of the selection at.35*/36goog.dom.selection.setStart = function(textfield, pos) {37if (goog.dom.selection.useSelectionProperties_(textfield)) {38textfield.selectionStart = pos;39} else if (goog.dom.selection.isLegacyIe_()) {40// destructuring assignment would have been sweet41var tmp = goog.dom.selection.getRangeIe_(textfield);42var range = tmp[0];43var selectionRange = tmp[1];4445if (range.inRange(selectionRange)) {46pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);4748range.collapse(true);49range.move('character', pos);50range.select();51}52}53};545556/**57* Return the place where the selection starts inside a textarea or a text58* input59* @param {Element} textfield A textarea or text input.60* @return {number} The position where the selection starts or 0 if it was61* unable to find the position or no selection exists. Note that we can't62* reliably tell the difference between an element that has no selection and63* one where it starts at 0.64*/65goog.dom.selection.getStart = function(textfield) {66return goog.dom.selection.getEndPoints_(textfield, true)[0];67};686970/**71* Returns the start and end points of the selection within a textarea in IE.72* IE treats newline characters as \r\n characters, and we need to check for73* these characters at the edge of our selection, to ensure that we return the74* right cursor position.75* @param {TextRange} range Complete range object, e.g., "Hello\r\n".76* @param {TextRange} selRange Selected range object.77* @param {boolean} getOnlyStart Value indicating if only start78* cursor position is to be returned. In IE, obtaining the end position79* involves extra work, hence we have this parameter for calls which need80* only start position.81* @return {!Array<number>} An array with the start and end positions where the82* selection starts and ends or [0,0] if it was unable to find the83* positions or no selection exists. Note that we can't reliably tell the84* difference between an element that has no selection and one where85* it starts and ends at 0. If getOnlyStart was true, we return86* -1 as end offset.87* @private88*/89goog.dom.selection.getEndPointsTextareaIe_ = function(90range, selRange, getOnlyStart) {91// Create a duplicate of the selected range object to perform our actions92// against. Example of selectionRange = "" (assuming that the cursor is93// just after the \r\n combination)94var selectionRange = selRange.duplicate();9596// Text before the selection start, e.g.,"Hello" (notice how range.text97// excludes the \r\n sequence)98var beforeSelectionText = range.text;99// Text before the selection start, e.g., "Hello" (this will later include100// the \r\n sequences also)101var untrimmedBeforeSelectionText = beforeSelectionText;102// Text within the selection , e.g. "" assuming that the cursor is just after103// the \r\n combination.104var selectionText = selectionRange.text;105// Text within the selection, e.g., "" (this will later include the \r\n106// sequences also)107var untrimmedSelectionText = selectionText;108109// Boolean indicating whether we are done dealing with the text before the110// selection's beginning.111var isRangeEndTrimmed = false;112// Go over the range until it becomes a 0-lengthed range or until the range113// text starts changing when we move the end back by one character.114// If after moving the end back by one character, the text remains the same,115// then we need to add a "\r\n" at the end to get the actual text.116while (!isRangeEndTrimmed) {117if (range.compareEndPoints('StartToEnd', range) == 0) {118isRangeEndTrimmed = true;119} else {120range.moveEnd('character', -1);121if (range.text == beforeSelectionText) {122// If the start position of the cursor was after a \r\n string,123// we would skip over it in one go with the moveEnd call, but124// range.text will still show "Hello" (because of the IE range.text125// bug) - this implies that we should add a \r\n to our126// untrimmedBeforeSelectionText string.127untrimmedBeforeSelectionText += '\r\n';128} else {129isRangeEndTrimmed = true;130}131}132}133134if (getOnlyStart) {135// We return -1 as end, since the caller is only interested in the start136// value.137return [untrimmedBeforeSelectionText.length, -1];138}139// Boolean indicating whether we are done dealing with the text inside the140// selection.141var isSelectionRangeEndTrimmed = false;142// Go over the selected range until it becomes a 0-lengthed range or until143// the range text starts changing when we move the end back by one character.144// If after moving the end back by one character, the text remains the same,145// then we need to add a "\r\n" at the end to get the actual text.146while (!isSelectionRangeEndTrimmed) {147if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {148isSelectionRangeEndTrimmed = true;149} else {150selectionRange.moveEnd('character', -1);151if (selectionRange.text == selectionText) {152// If the selection was not empty, and the end point of the selection153// was just after a \r\n, we would have skipped it in one go with the154// moveEnd call, and this implies that we should add a \r\n to the155// untrimmedSelectionText string.156untrimmedSelectionText += '\r\n';157} else {158isSelectionRangeEndTrimmed = true;159}160}161}162return [163untrimmedBeforeSelectionText.length,164untrimmedBeforeSelectionText.length + untrimmedSelectionText.length165];166};167168169/**170* Returns the start and end points of the selection inside a textarea or a171* text input.172* @param {Element} textfield A textarea or text input.173* @return {!Array<number>} An array with the start and end positions where the174* selection starts and ends or [0,0] if it was unable to find the175* positions or no selection exists. Note that we can't reliably tell the176* difference between an element that has no selection and one where177* it starts and ends at 0.178*/179goog.dom.selection.getEndPoints = function(textfield) {180return goog.dom.selection.getEndPoints_(textfield, false);181};182183184/**185* Returns the start and end points of the selection inside a textarea or a186* text input.187* @param {Element} textfield A textarea or text input.188* @param {boolean} getOnlyStart Value indicating if only start189* cursor position is to be returned. In IE, obtaining the end position190* involves extra work, hence we have this parameter. In FF, there is not191* much extra effort involved.192* @return {!Array<number>} An array with the start and end positions where the193* selection starts and ends or [0,0] if it was unable to find the194* positions or no selection exists. Note that we can't reliably tell the195* difference between an element that has no selection and one where196* it starts and ends at 0. If getOnlyStart was true, we return197* -1 as end offset.198* @private199*/200goog.dom.selection.getEndPoints_ = function(textfield, getOnlyStart) {201textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);202var startPos = 0;203var endPos = 0;204if (goog.dom.selection.useSelectionProperties_(textfield)) {205startPos = textfield.selectionStart;206endPos = getOnlyStart ? -1 : textfield.selectionEnd;207} else if (goog.dom.selection.isLegacyIe_()) {208var tmp = goog.dom.selection.getRangeIe_(textfield);209var range = tmp[0];210var selectionRange = tmp[1];211212if (range.inRange(selectionRange)) {213range.setEndPoint('EndToStart', selectionRange);214if (textfield.type == goog.dom.InputType.TEXTAREA) {215return goog.dom.selection.getEndPointsTextareaIe_(216range, selectionRange, getOnlyStart);217}218startPos = range.text.length;219if (!getOnlyStart) {220endPos = range.text.length + selectionRange.text.length;221} else {222endPos = -1; // caller did not ask for end position223}224}225}226return [startPos, endPos];227};228229230/**231* Sets the place where the selection should end inside a text area or a text232* input233* @param {Element} textfield A textarea or text input.234* @param {number} pos The position to end the selection at.235*/236goog.dom.selection.setEnd = function(textfield, pos) {237if (goog.dom.selection.useSelectionProperties_(textfield)) {238textfield.selectionEnd = pos;239} else if (goog.dom.selection.isLegacyIe_()) {240var tmp = goog.dom.selection.getRangeIe_(textfield);241var range = tmp[0];242var selectionRange = tmp[1];243244if (range.inRange(selectionRange)) {245// Both the current position and the start cursor position need246// to be canonicalized to take care of possible \r\n miscounts.247pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);248var startCursorPos = goog.dom.selection.canonicalizePositionIe_(249textfield, goog.dom.selection.getStart(textfield));250251selectionRange.collapse(true);252selectionRange.moveEnd('character', pos - startCursorPos);253selectionRange.select();254}255}256};257258259/**260* Returns the place where the selection ends inside a textarea or a text input261* @param {Element} textfield A textarea or text input.262* @return {number} The position where the selection ends or 0 if it was263* unable to find the position or no selection exists.264*/265goog.dom.selection.getEnd = function(textfield) {266return goog.dom.selection.getEndPoints_(textfield, false)[1];267};268269270/**271* Sets the cursor position within a textfield.272* @param {Element} textfield A textarea or text input.273* @param {number} pos The position within the text field.274*/275goog.dom.selection.setCursorPosition = function(textfield, pos) {276if (goog.dom.selection.useSelectionProperties_(textfield)) {277// Mozilla directly supports this278textfield.selectionStart = pos;279textfield.selectionEnd = pos;280281} else if (goog.dom.selection.isLegacyIe_()) {282pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);283284// IE has textranges. A textfield's textrange encompasses the285// entire textfield's text by default286var sel = textfield.createTextRange();287288sel.collapse(true);289sel.move('character', pos);290sel.select();291}292};293294295/**296* Sets the selected text inside a textarea or a text input297* @param {Element} textfield A textarea or text input.298* @param {string} text The text to change the selection to.299*/300goog.dom.selection.setText = function(textfield, text) {301textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);302if (goog.dom.selection.useSelectionProperties_(textfield)) {303var value = textfield.value;304var oldSelectionStart = textfield.selectionStart;305var before = value.substr(0, oldSelectionStart);306var after = value.substr(textfield.selectionEnd);307textfield.value = before + text + after;308textfield.selectionStart = oldSelectionStart;309textfield.selectionEnd = oldSelectionStart + text.length;310} else if (goog.dom.selection.isLegacyIe_()) {311var tmp = goog.dom.selection.getRangeIe_(textfield);312var range = tmp[0];313var selectionRange = tmp[1];314315if (!range.inRange(selectionRange)) {316return;317}318// When we set the selection text the selection range is collapsed to the319// end. We therefore duplicate the current selection so we know where it320// started. Once we've set the selection text we move the start of the321// selection range to the old start322var range2 = selectionRange.duplicate();323selectionRange.text = text;324selectionRange.setEndPoint('StartToStart', range2);325selectionRange.select();326} else {327throw Error('Cannot set the selection end');328}329};330331332/**333* Returns the selected text inside a textarea or a text input334* @param {Element} textfield A textarea or text input.335* @return {string} The selected text.336*/337goog.dom.selection.getText = function(textfield) {338textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);339if (goog.dom.selection.useSelectionProperties_(textfield)) {340var s = textfield.value;341return s.substring(textfield.selectionStart, textfield.selectionEnd);342}343344if (goog.dom.selection.isLegacyIe_()) {345var tmp = goog.dom.selection.getRangeIe_(textfield);346var range = tmp[0];347var selectionRange = tmp[1];348349if (!range.inRange(selectionRange)) {350return '';351} else if (textfield.type == goog.dom.InputType.TEXTAREA) {352return goog.dom.selection.getSelectionRangeText_(selectionRange);353}354return selectionRange.text;355}356357throw Error('Cannot get the selection text');358};359360361/**362* Returns the selected text within a textarea in IE.363* IE treats newline characters as \r\n characters, and we need to check for364* these characters at the edge of our selection, to ensure that we return the365* right string.366* @param {TextRange} selRange Selected range object.367* @return {string} Selected text in the textarea.368* @private369*/370goog.dom.selection.getSelectionRangeText_ = function(selRange) {371// Create a duplicate of the selected range object to perform our actions372// against. Suppose the text in the textarea is "Hello\r\nWorld" and the373// selection encompasses the "o\r\n" bit, initial selectionRange will be "o"374// (assuming that the cursor is just after the \r\n combination)375var selectionRange = selRange.duplicate();376377// Text within the selection , e.g. "o" assuming that the cursor is just after378// the \r\n combination.379var selectionText = selectionRange.text;380// Text within the selection, e.g., "o" (this will later include the \r\n381// sequences also)382var untrimmedSelectionText = selectionText;383384// Boolean indicating whether we are done dealing with the text inside the385// selection.386var isSelectionRangeEndTrimmed = false;387// Go over the selected range until it becomes a 0-lengthed range or until388// the range text starts changing when we move the end back by one character.389// If after moving the end back by one character, the text remains the same,390// then we need to add a "\r\n" at the end to get the actual text.391while (!isSelectionRangeEndTrimmed) {392if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {393isSelectionRangeEndTrimmed = true;394} else {395selectionRange.moveEnd('character', -1);396if (selectionRange.text == selectionText) {397// If the selection was not empty, and the end point of the selection398// was just after a \r\n, we would have skipped it in one go with the399// moveEnd call, and this implies that we should add a \r\n to the400// untrimmedSelectionText string.401untrimmedSelectionText += '\r\n';402} else {403isSelectionRangeEndTrimmed = true;404}405}406}407return untrimmedSelectionText;408};409410411/**412* Helper function for returning the range for an object as well as the413* selection range414* @private415* @param {Element} el The element to get the range for.416* @return {!Array<TextRange>} Range of object and selection range in two417* element array.418*/419goog.dom.selection.getRangeIe_ = function(el) {420var doc = el.ownerDocument || el.document;421422var selectionRange = doc.selection.createRange();423// el.createTextRange() doesn't work on textareas424var range;425426if (/** @type {?} */ (el).type == goog.dom.InputType.TEXTAREA) {427range = doc.body.createTextRange();428range.moveToElementText(el);429} else {430range = el.createTextRange();431}432433return [range, selectionRange];434};435436437/**438* Helper function for canonicalizing a position inside a textfield in IE.439* Deals with the issue that \r\n counts as 2 characters, but440* move('character', n) passes over both characters in one move.441* @private442* @param {Element} textfield The text element.443* @param {number} pos The position desired in that element.444* @return {number} The canonicalized position that will work properly with445* move('character', pos).446*/447goog.dom.selection.canonicalizePositionIe_ = function(textfield, pos) {448textfield = /** @type {!HTMLTextAreaElement} */ (textfield);449if (textfield.type == goog.dom.InputType.TEXTAREA) {450// We do this only for textarea because it is the only one which can451// have a \r\n (input cannot have this).452var value = textfield.value.substring(0, pos);453pos = goog.string.canonicalizeNewlines(value).length;454}455return pos;456};457458459/**460* Helper function to determine whether it's okay to use461* selectionStart/selectionEnd.462*463* @param {Element} el The element to check for.464* @return {boolean} Whether it's okay to use the selectionStart and465* selectionEnd properties on {@code el}.466* @private467*/468goog.dom.selection.useSelectionProperties_ = function(el) {469try {470return typeof el.selectionStart == 'number';471} catch (e) {472// Firefox throws an exception if you try to access selectionStart473// on an element with display: none.474return false;475}476};477478479/**480* Whether the client is legacy IE which does not support481* selectionStart/selectionEnd properties of a text input element.482*483* @see https://msdn.microsoft.com/en-us/library/ff974768(v=vs.85).aspx484*485* @return {boolean} Whether the client is a legacy version of IE.486* @private487*/488goog.dom.selection.isLegacyIe_ = function() {489return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');490};491492493