selection.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Utilities for working with selections in input boxes and text
  16. * areas.
  17. *
  18. * @author arv@google.com (Erik Arvidsson)
  19. * @see ../demos/dom_selection.html
  20. */
  21. goog.provide('goog.dom.selection');
  22. goog.require('goog.dom.InputType');
  23. goog.require('goog.string');
  24. goog.require('goog.userAgent');
  25. /**
  26. * Sets the place where the selection should start inside a textarea or a text
  27. * input
  28. * @param {Element} textfield A textarea or text input.
  29. * @param {number} pos The position to set the start of the selection at.
  30. */
  31. goog.dom.selection.setStart = function(textfield, pos) {
  32. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  33. textfield.selectionStart = pos;
  34. } else if (goog.dom.selection.isLegacyIe_()) {
  35. // destructuring assignment would have been sweet
  36. var tmp = goog.dom.selection.getRangeIe_(textfield);
  37. var range = tmp[0];
  38. var selectionRange = tmp[1];
  39. if (range.inRange(selectionRange)) {
  40. pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
  41. range.collapse(true);
  42. range.move('character', pos);
  43. range.select();
  44. }
  45. }
  46. };
  47. /**
  48. * Return the place where the selection starts inside a textarea or a text
  49. * input
  50. * @param {Element} textfield A textarea or text input.
  51. * @return {number} The position where the selection starts or 0 if it was
  52. * unable to find the position or no selection exists. Note that we can't
  53. * reliably tell the difference between an element that has no selection and
  54. * one where it starts at 0.
  55. */
  56. goog.dom.selection.getStart = function(textfield) {
  57. return goog.dom.selection.getEndPoints_(textfield, true)[0];
  58. };
  59. /**
  60. * Returns the start and end points of the selection within a textarea in IE.
  61. * IE treats newline characters as \r\n characters, and we need to check for
  62. * these characters at the edge of our selection, to ensure that we return the
  63. * right cursor position.
  64. * @param {TextRange} range Complete range object, e.g., "Hello\r\n".
  65. * @param {TextRange} selRange Selected range object.
  66. * @param {boolean} getOnlyStart Value indicating if only start
  67. * cursor position is to be returned. In IE, obtaining the end position
  68. * involves extra work, hence we have this parameter for calls which need
  69. * only start position.
  70. * @return {!Array<number>} An array with the start and end positions where the
  71. * selection starts and ends or [0,0] if it was unable to find the
  72. * positions or no selection exists. Note that we can't reliably tell the
  73. * difference between an element that has no selection and one where
  74. * it starts and ends at 0. If getOnlyStart was true, we return
  75. * -1 as end offset.
  76. * @private
  77. */
  78. goog.dom.selection.getEndPointsTextareaIe_ = function(
  79. range, selRange, getOnlyStart) {
  80. // Create a duplicate of the selected range object to perform our actions
  81. // against. Example of selectionRange = "" (assuming that the cursor is
  82. // just after the \r\n combination)
  83. var selectionRange = selRange.duplicate();
  84. // Text before the selection start, e.g.,"Hello" (notice how range.text
  85. // excludes the \r\n sequence)
  86. var beforeSelectionText = range.text;
  87. // Text before the selection start, e.g., "Hello" (this will later include
  88. // the \r\n sequences also)
  89. var untrimmedBeforeSelectionText = beforeSelectionText;
  90. // Text within the selection , e.g. "" assuming that the cursor is just after
  91. // the \r\n combination.
  92. var selectionText = selectionRange.text;
  93. // Text within the selection, e.g., "" (this will later include the \r\n
  94. // sequences also)
  95. var untrimmedSelectionText = selectionText;
  96. // Boolean indicating whether we are done dealing with the text before the
  97. // selection's beginning.
  98. var isRangeEndTrimmed = false;
  99. // Go over the range until it becomes a 0-lengthed range or until the range
  100. // text starts changing when we move the end back by one character.
  101. // If after moving the end back by one character, the text remains the same,
  102. // then we need to add a "\r\n" at the end to get the actual text.
  103. while (!isRangeEndTrimmed) {
  104. if (range.compareEndPoints('StartToEnd', range) == 0) {
  105. isRangeEndTrimmed = true;
  106. } else {
  107. range.moveEnd('character', -1);
  108. if (range.text == beforeSelectionText) {
  109. // If the start position of the cursor was after a \r\n string,
  110. // we would skip over it in one go with the moveEnd call, but
  111. // range.text will still show "Hello" (because of the IE range.text
  112. // bug) - this implies that we should add a \r\n to our
  113. // untrimmedBeforeSelectionText string.
  114. untrimmedBeforeSelectionText += '\r\n';
  115. } else {
  116. isRangeEndTrimmed = true;
  117. }
  118. }
  119. }
  120. if (getOnlyStart) {
  121. // We return -1 as end, since the caller is only interested in the start
  122. // value.
  123. return [untrimmedBeforeSelectionText.length, -1];
  124. }
  125. // Boolean indicating whether we are done dealing with the text inside the
  126. // selection.
  127. var isSelectionRangeEndTrimmed = false;
  128. // Go over the selected range until it becomes a 0-lengthed range or until
  129. // the range text starts changing when we move the end back by one character.
  130. // If after moving the end back by one character, the text remains the same,
  131. // then we need to add a "\r\n" at the end to get the actual text.
  132. while (!isSelectionRangeEndTrimmed) {
  133. if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
  134. isSelectionRangeEndTrimmed = true;
  135. } else {
  136. selectionRange.moveEnd('character', -1);
  137. if (selectionRange.text == selectionText) {
  138. // If the selection was not empty, and the end point of the selection
  139. // was just after a \r\n, we would have skipped it in one go with the
  140. // moveEnd call, and this implies that we should add a \r\n to the
  141. // untrimmedSelectionText string.
  142. untrimmedSelectionText += '\r\n';
  143. } else {
  144. isSelectionRangeEndTrimmed = true;
  145. }
  146. }
  147. }
  148. return [
  149. untrimmedBeforeSelectionText.length,
  150. untrimmedBeforeSelectionText.length + untrimmedSelectionText.length
  151. ];
  152. };
  153. /**
  154. * Returns the start and end points of the selection inside a textarea or a
  155. * text input.
  156. * @param {Element} textfield A textarea or text input.
  157. * @return {!Array<number>} An array with the start and end positions where the
  158. * selection starts and ends or [0,0] if it was unable to find the
  159. * positions or no selection exists. Note that we can't reliably tell the
  160. * difference between an element that has no selection and one where
  161. * it starts and ends at 0.
  162. */
  163. goog.dom.selection.getEndPoints = function(textfield) {
  164. return goog.dom.selection.getEndPoints_(textfield, false);
  165. };
  166. /**
  167. * Returns the start and end points of the selection inside a textarea or a
  168. * text input.
  169. * @param {Element} textfield A textarea or text input.
  170. * @param {boolean} getOnlyStart Value indicating if only start
  171. * cursor position is to be returned. In IE, obtaining the end position
  172. * involves extra work, hence we have this parameter. In FF, there is not
  173. * much extra effort involved.
  174. * @return {!Array<number>} An array with the start and end positions where the
  175. * selection starts and ends or [0,0] if it was unable to find the
  176. * positions or no selection exists. Note that we can't reliably tell the
  177. * difference between an element that has no selection and one where
  178. * it starts and ends at 0. If getOnlyStart was true, we return
  179. * -1 as end offset.
  180. * @private
  181. */
  182. goog.dom.selection.getEndPoints_ = function(textfield, getOnlyStart) {
  183. textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);
  184. var startPos = 0;
  185. var endPos = 0;
  186. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  187. startPos = textfield.selectionStart;
  188. endPos = getOnlyStart ? -1 : textfield.selectionEnd;
  189. } else if (goog.dom.selection.isLegacyIe_()) {
  190. var tmp = goog.dom.selection.getRangeIe_(textfield);
  191. var range = tmp[0];
  192. var selectionRange = tmp[1];
  193. if (range.inRange(selectionRange)) {
  194. range.setEndPoint('EndToStart', selectionRange);
  195. if (textfield.type == goog.dom.InputType.TEXTAREA) {
  196. return goog.dom.selection.getEndPointsTextareaIe_(
  197. range, selectionRange, getOnlyStart);
  198. }
  199. startPos = range.text.length;
  200. if (!getOnlyStart) {
  201. endPos = range.text.length + selectionRange.text.length;
  202. } else {
  203. endPos = -1; // caller did not ask for end position
  204. }
  205. }
  206. }
  207. return [startPos, endPos];
  208. };
  209. /**
  210. * Sets the place where the selection should end inside a text area or a text
  211. * input
  212. * @param {Element} textfield A textarea or text input.
  213. * @param {number} pos The position to end the selection at.
  214. */
  215. goog.dom.selection.setEnd = function(textfield, pos) {
  216. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  217. textfield.selectionEnd = pos;
  218. } else if (goog.dom.selection.isLegacyIe_()) {
  219. var tmp = goog.dom.selection.getRangeIe_(textfield);
  220. var range = tmp[0];
  221. var selectionRange = tmp[1];
  222. if (range.inRange(selectionRange)) {
  223. // Both the current position and the start cursor position need
  224. // to be canonicalized to take care of possible \r\n miscounts.
  225. pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
  226. var startCursorPos = goog.dom.selection.canonicalizePositionIe_(
  227. textfield, goog.dom.selection.getStart(textfield));
  228. selectionRange.collapse(true);
  229. selectionRange.moveEnd('character', pos - startCursorPos);
  230. selectionRange.select();
  231. }
  232. }
  233. };
  234. /**
  235. * Returns the place where the selection ends inside a textarea or a text input
  236. * @param {Element} textfield A textarea or text input.
  237. * @return {number} The position where the selection ends or 0 if it was
  238. * unable to find the position or no selection exists.
  239. */
  240. goog.dom.selection.getEnd = function(textfield) {
  241. return goog.dom.selection.getEndPoints_(textfield, false)[1];
  242. };
  243. /**
  244. * Sets the cursor position within a textfield.
  245. * @param {Element} textfield A textarea or text input.
  246. * @param {number} pos The position within the text field.
  247. */
  248. goog.dom.selection.setCursorPosition = function(textfield, pos) {
  249. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  250. // Mozilla directly supports this
  251. textfield.selectionStart = pos;
  252. textfield.selectionEnd = pos;
  253. } else if (goog.dom.selection.isLegacyIe_()) {
  254. pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
  255. // IE has textranges. A textfield's textrange encompasses the
  256. // entire textfield's text by default
  257. var sel = textfield.createTextRange();
  258. sel.collapse(true);
  259. sel.move('character', pos);
  260. sel.select();
  261. }
  262. };
  263. /**
  264. * Sets the selected text inside a textarea or a text input
  265. * @param {Element} textfield A textarea or text input.
  266. * @param {string} text The text to change the selection to.
  267. */
  268. goog.dom.selection.setText = function(textfield, text) {
  269. textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);
  270. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  271. var value = textfield.value;
  272. var oldSelectionStart = textfield.selectionStart;
  273. var before = value.substr(0, oldSelectionStart);
  274. var after = value.substr(textfield.selectionEnd);
  275. textfield.value = before + text + after;
  276. textfield.selectionStart = oldSelectionStart;
  277. textfield.selectionEnd = oldSelectionStart + text.length;
  278. } else if (goog.dom.selection.isLegacyIe_()) {
  279. var tmp = goog.dom.selection.getRangeIe_(textfield);
  280. var range = tmp[0];
  281. var selectionRange = tmp[1];
  282. if (!range.inRange(selectionRange)) {
  283. return;
  284. }
  285. // When we set the selection text the selection range is collapsed to the
  286. // end. We therefore duplicate the current selection so we know where it
  287. // started. Once we've set the selection text we move the start of the
  288. // selection range to the old start
  289. var range2 = selectionRange.duplicate();
  290. selectionRange.text = text;
  291. selectionRange.setEndPoint('StartToStart', range2);
  292. selectionRange.select();
  293. } else {
  294. throw Error('Cannot set the selection end');
  295. }
  296. };
  297. /**
  298. * Returns the selected text inside a textarea or a text input
  299. * @param {Element} textfield A textarea or text input.
  300. * @return {string} The selected text.
  301. */
  302. goog.dom.selection.getText = function(textfield) {
  303. textfield = /** @type {!HTMLInputElement|!HTMLTextAreaElement} */ (textfield);
  304. if (goog.dom.selection.useSelectionProperties_(textfield)) {
  305. var s = textfield.value;
  306. return s.substring(textfield.selectionStart, textfield.selectionEnd);
  307. }
  308. if (goog.dom.selection.isLegacyIe_()) {
  309. var tmp = goog.dom.selection.getRangeIe_(textfield);
  310. var range = tmp[0];
  311. var selectionRange = tmp[1];
  312. if (!range.inRange(selectionRange)) {
  313. return '';
  314. } else if (textfield.type == goog.dom.InputType.TEXTAREA) {
  315. return goog.dom.selection.getSelectionRangeText_(selectionRange);
  316. }
  317. return selectionRange.text;
  318. }
  319. throw Error('Cannot get the selection text');
  320. };
  321. /**
  322. * Returns the selected text within a textarea in IE.
  323. * IE treats newline characters as \r\n characters, and we need to check for
  324. * these characters at the edge of our selection, to ensure that we return the
  325. * right string.
  326. * @param {TextRange} selRange Selected range object.
  327. * @return {string} Selected text in the textarea.
  328. * @private
  329. */
  330. goog.dom.selection.getSelectionRangeText_ = function(selRange) {
  331. // Create a duplicate of the selected range object to perform our actions
  332. // against. Suppose the text in the textarea is "Hello\r\nWorld" and the
  333. // selection encompasses the "o\r\n" bit, initial selectionRange will be "o"
  334. // (assuming that the cursor is just after the \r\n combination)
  335. var selectionRange = selRange.duplicate();
  336. // Text within the selection , e.g. "o" assuming that the cursor is just after
  337. // the \r\n combination.
  338. var selectionText = selectionRange.text;
  339. // Text within the selection, e.g., "o" (this will later include the \r\n
  340. // sequences also)
  341. var untrimmedSelectionText = selectionText;
  342. // Boolean indicating whether we are done dealing with the text inside the
  343. // selection.
  344. var isSelectionRangeEndTrimmed = false;
  345. // Go over the selected range until it becomes a 0-lengthed range or until
  346. // the range text starts changing when we move the end back by one character.
  347. // If after moving the end back by one character, the text remains the same,
  348. // then we need to add a "\r\n" at the end to get the actual text.
  349. while (!isSelectionRangeEndTrimmed) {
  350. if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
  351. isSelectionRangeEndTrimmed = true;
  352. } else {
  353. selectionRange.moveEnd('character', -1);
  354. if (selectionRange.text == selectionText) {
  355. // If the selection was not empty, and the end point of the selection
  356. // was just after a \r\n, we would have skipped it in one go with the
  357. // moveEnd call, and this implies that we should add a \r\n to the
  358. // untrimmedSelectionText string.
  359. untrimmedSelectionText += '\r\n';
  360. } else {
  361. isSelectionRangeEndTrimmed = true;
  362. }
  363. }
  364. }
  365. return untrimmedSelectionText;
  366. };
  367. /**
  368. * Helper function for returning the range for an object as well as the
  369. * selection range
  370. * @private
  371. * @param {Element} el The element to get the range for.
  372. * @return {!Array<TextRange>} Range of object and selection range in two
  373. * element array.
  374. */
  375. goog.dom.selection.getRangeIe_ = function(el) {
  376. var doc = el.ownerDocument || el.document;
  377. var selectionRange = doc.selection.createRange();
  378. // el.createTextRange() doesn't work on textareas
  379. var range;
  380. if (/** @type {?} */ (el).type == goog.dom.InputType.TEXTAREA) {
  381. range = doc.body.createTextRange();
  382. range.moveToElementText(el);
  383. } else {
  384. range = el.createTextRange();
  385. }
  386. return [range, selectionRange];
  387. };
  388. /**
  389. * Helper function for canonicalizing a position inside a textfield in IE.
  390. * Deals with the issue that \r\n counts as 2 characters, but
  391. * move('character', n) passes over both characters in one move.
  392. * @private
  393. * @param {Element} textfield The text element.
  394. * @param {number} pos The position desired in that element.
  395. * @return {number} The canonicalized position that will work properly with
  396. * move('character', pos).
  397. */
  398. goog.dom.selection.canonicalizePositionIe_ = function(textfield, pos) {
  399. textfield = /** @type {!HTMLTextAreaElement} */ (textfield);
  400. if (textfield.type == goog.dom.InputType.TEXTAREA) {
  401. // We do this only for textarea because it is the only one which can
  402. // have a \r\n (input cannot have this).
  403. var value = textfield.value.substring(0, pos);
  404. pos = goog.string.canonicalizeNewlines(value).length;
  405. }
  406. return pos;
  407. };
  408. /**
  409. * Helper function to determine whether it's okay to use
  410. * selectionStart/selectionEnd.
  411. *
  412. * @param {Element} el The element to check for.
  413. * @return {boolean} Whether it's okay to use the selectionStart and
  414. * selectionEnd properties on {@code el}.
  415. * @private
  416. */
  417. goog.dom.selection.useSelectionProperties_ = function(el) {
  418. try {
  419. return typeof el.selectionStart == 'number';
  420. } catch (e) {
  421. // Firefox throws an exception if you try to access selectionStart
  422. // on an element with display: none.
  423. return false;
  424. }
  425. };
  426. /**
  427. * Whether the client is legacy IE which does not support
  428. * selectionStart/selectionEnd properties of a text input element.
  429. *
  430. * @see https://msdn.microsoft.com/en-us/library/ff974768(v=vs.85).aspx
  431. *
  432. * @return {boolean} Whether the client is a legacy version of IE.
  433. * @private
  434. */
  435. goog.dom.selection.isLegacyIe_ = function() {
  436. return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');
  437. };