textrange.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. // Copyright 2007 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 text ranges in HTML documents.
  16. *
  17. * @author robbyw@google.com (Robby Walker)
  18. */
  19. goog.provide('goog.dom.TextRange');
  20. goog.require('goog.array');
  21. goog.require('goog.dom');
  22. goog.require('goog.dom.AbstractRange');
  23. goog.require('goog.dom.RangeType');
  24. goog.require('goog.dom.SavedRange');
  25. goog.require('goog.dom.TagName');
  26. goog.require('goog.dom.TextRangeIterator');
  27. goog.require('goog.dom.browserrange');
  28. goog.require('goog.string');
  29. goog.require('goog.userAgent');
  30. /**
  31. * Create a new text selection with no properties. Do not use this constructor:
  32. * use one of the goog.dom.Range.createFrom* methods instead.
  33. * @constructor
  34. * @extends {goog.dom.AbstractRange}
  35. * @final
  36. */
  37. goog.dom.TextRange = function() {
  38. /**
  39. * The browser specific range wrapper. This can be null if one of the other
  40. * representations of the range is specified.
  41. * @private {goog.dom.browserrange.AbstractRange?}
  42. */
  43. this.browserRangeWrapper_ = null;
  44. /**
  45. * The start node of the range. This can be null if one of the other
  46. * representations of the range is specified.
  47. * @private {Node}
  48. */
  49. this.startNode_ = null;
  50. /**
  51. * The start offset of the range. This can be null if one of the other
  52. * representations of the range is specified.
  53. * @private {?number}
  54. */
  55. this.startOffset_ = null;
  56. /**
  57. * The end node of the range. This can be null if one of the other
  58. * representations of the range is specified.
  59. * @private {Node}
  60. */
  61. this.endNode_ = null;
  62. /**
  63. * The end offset of the range. This can be null if one of the other
  64. * representations of the range is specified.
  65. * @private {?number}
  66. */
  67. this.endOffset_ = null;
  68. /**
  69. * Whether the focus node is before the anchor node.
  70. * @private {boolean}
  71. */
  72. this.isReversed_ = false;
  73. };
  74. goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
  75. /**
  76. * Create a new range wrapper from the given browser range object. Do not use
  77. * this method directly - please use goog.dom.Range.createFrom* instead.
  78. * @param {Range|TextRange} range The browser range object.
  79. * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
  80. * node.
  81. * @return {!goog.dom.TextRange} A range wrapper object.
  82. */
  83. goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
  84. return goog.dom.TextRange.createFromBrowserRangeWrapper_(
  85. goog.dom.browserrange.createRange(range), opt_isReversed);
  86. };
  87. /**
  88. * Create a new range wrapper from the given browser range wrapper.
  89. * @param {goog.dom.browserrange.AbstractRange} browserRange The browser range
  90. * wrapper.
  91. * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
  92. * node.
  93. * @return {!goog.dom.TextRange} A range wrapper object.
  94. * @private
  95. */
  96. goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(
  97. browserRange, opt_isReversed) {
  98. var range = new goog.dom.TextRange();
  99. // Initialize the range as a browser range wrapper type range.
  100. range.browserRangeWrapper_ = browserRange;
  101. range.isReversed_ = !!opt_isReversed;
  102. return range;
  103. };
  104. /**
  105. * Create a new range wrapper that selects the given node's text. Do not use
  106. * this method directly - please use goog.dom.Range.createFrom* instead.
  107. * @param {Node} node The node to select.
  108. * @param {boolean=} opt_isReversed Whether the focus node is before the anchor
  109. * node.
  110. * @return {!goog.dom.TextRange} A range wrapper object.
  111. */
  112. goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
  113. return goog.dom.TextRange.createFromBrowserRangeWrapper_(
  114. goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed);
  115. };
  116. /**
  117. * Create a new range wrapper that selects the area between the given nodes,
  118. * accounting for the given offsets. Do not use this method directly - please
  119. * use goog.dom.Range.createFrom* instead.
  120. * @param {Node} anchorNode The node to start with.
  121. * @param {number} anchorOffset The offset within the node to start.
  122. * @param {Node} focusNode The node to end with.
  123. * @param {number} focusOffset The offset within the node to end.
  124. * @return {!goog.dom.TextRange} A range wrapper object.
  125. */
  126. goog.dom.TextRange.createFromNodes = function(
  127. anchorNode, anchorOffset, focusNode, focusOffset) {
  128. var range = new goog.dom.TextRange();
  129. range.isReversed_ = /** @suppress {missingRequire} */ (
  130. goog.dom.Range.isReversed(
  131. anchorNode, anchorOffset, focusNode, focusOffset));
  132. // Avoid selecting terminal elements directly
  133. if (goog.dom.isElement(anchorNode) && !goog.dom.canHaveChildren(anchorNode)) {
  134. var parent = anchorNode.parentNode;
  135. anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
  136. anchorNode = parent;
  137. }
  138. if (goog.dom.isElement(focusNode) && !goog.dom.canHaveChildren(focusNode)) {
  139. var parent = focusNode.parentNode;
  140. focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
  141. focusNode = parent;
  142. }
  143. // Initialize the range as a W3C style range.
  144. if (range.isReversed_) {
  145. range.startNode_ = focusNode;
  146. range.startOffset_ = focusOffset;
  147. range.endNode_ = anchorNode;
  148. range.endOffset_ = anchorOffset;
  149. } else {
  150. range.startNode_ = anchorNode;
  151. range.startOffset_ = anchorOffset;
  152. range.endNode_ = focusNode;
  153. range.endOffset_ = focusOffset;
  154. }
  155. return range;
  156. };
  157. // Method implementations
  158. /**
  159. * @return {!goog.dom.TextRange} A clone of this range.
  160. * @override
  161. */
  162. goog.dom.TextRange.prototype.clone = function() {
  163. var range = new goog.dom.TextRange();
  164. range.browserRangeWrapper_ =
  165. this.browserRangeWrapper_ && this.browserRangeWrapper_.clone();
  166. range.startNode_ = this.startNode_;
  167. range.startOffset_ = this.startOffset_;
  168. range.endNode_ = this.endNode_;
  169. range.endOffset_ = this.endOffset_;
  170. range.isReversed_ = this.isReversed_;
  171. return range;
  172. };
  173. /** @override */
  174. goog.dom.TextRange.prototype.getType = function() {
  175. return goog.dom.RangeType.TEXT;
  176. };
  177. /** @override */
  178. goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
  179. return this.getBrowserRangeWrapper_().getBrowserRange();
  180. };
  181. /** @override */
  182. goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
  183. // Test if it's a control range by seeing if a control range only method
  184. // exists.
  185. if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
  186. return false;
  187. }
  188. this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange);
  189. this.clearCachedValues_();
  190. return true;
  191. };
  192. /**
  193. * Clear all cached values.
  194. * @private
  195. */
  196. goog.dom.TextRange.prototype.clearCachedValues_ = function() {
  197. this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;
  198. };
  199. /** @override */
  200. goog.dom.TextRange.prototype.getTextRangeCount = function() {
  201. return 1;
  202. };
  203. /** @override */
  204. goog.dom.TextRange.prototype.getTextRange = function(i) {
  205. return this;
  206. };
  207. /**
  208. * @return {!goog.dom.browserrange.AbstractRange} The range wrapper object.
  209. * @private
  210. */
  211. goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
  212. return this.browserRangeWrapper_ ||
  213. (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(
  214. this.getStartNode(), this.getStartOffset(), this.getEndNode(),
  215. this.getEndOffset()));
  216. };
  217. /** @override */
  218. goog.dom.TextRange.prototype.getContainer = function() {
  219. return this.getBrowserRangeWrapper_().getContainer();
  220. };
  221. /** @override */
  222. goog.dom.TextRange.prototype.getStartNode = function() {
  223. return this.startNode_ ||
  224. (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());
  225. };
  226. /** @override */
  227. goog.dom.TextRange.prototype.getStartOffset = function() {
  228. return this.startOffset_ != null ?
  229. this.startOffset_ :
  230. (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());
  231. };
  232. /** @override */
  233. goog.dom.TextRange.prototype.getStartPosition = function() {
  234. return this.getBrowserRangeWrapper_().getStartPosition();
  235. };
  236. /** @override */
  237. goog.dom.TextRange.prototype.getEndNode = function() {
  238. return this.endNode_ ||
  239. (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());
  240. };
  241. /** @override */
  242. goog.dom.TextRange.prototype.getEndOffset = function() {
  243. return this.endOffset_ != null ?
  244. this.endOffset_ :
  245. (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());
  246. };
  247. /** @override */
  248. goog.dom.TextRange.prototype.getEndPosition = function() {
  249. return this.getBrowserRangeWrapper_().getEndPosition();
  250. };
  251. /**
  252. * Moves a TextRange to the provided nodes and offsets.
  253. * @param {Node} startNode The node to start with.
  254. * @param {number} startOffset The offset within the node to start.
  255. * @param {Node} endNode The node to end with.
  256. * @param {number} endOffset The offset within the node to end.
  257. * @param {boolean} isReversed Whether the range is reversed.
  258. */
  259. goog.dom.TextRange.prototype.moveToNodes = function(
  260. startNode, startOffset, endNode, endOffset, isReversed) {
  261. this.startNode_ = startNode;
  262. this.startOffset_ = startOffset;
  263. this.endNode_ = endNode;
  264. this.endOffset_ = endOffset;
  265. this.isReversed_ = isReversed;
  266. this.browserRangeWrapper_ = null;
  267. };
  268. /** @override */
  269. goog.dom.TextRange.prototype.isReversed = function() {
  270. return this.isReversed_;
  271. };
  272. /** @override */
  273. goog.dom.TextRange.prototype.containsRange = function(
  274. otherRange, opt_allowPartial) {
  275. var otherRangeType = otherRange.getType();
  276. if (otherRangeType == goog.dom.RangeType.TEXT) {
  277. return this.getBrowserRangeWrapper_().containsRange(
  278. otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
  279. } else if (otherRangeType == goog.dom.RangeType.CONTROL) {
  280. var elements = otherRange.getElements();
  281. var fn = opt_allowPartial ? goog.array.some : goog.array.every;
  282. return fn(
  283. elements,
  284. /**
  285. * @this {goog.dom.TextRange}
  286. * @param {!Element} el
  287. * @return {boolean}
  288. */
  289. function(el) {
  290. return this.containsNode(el, opt_allowPartial);
  291. },
  292. this);
  293. }
  294. return false;
  295. };
  296. /** @override */
  297. goog.dom.TextRange.prototype.containsNode = function(node, opt_allowPartial) {
  298. return this.containsRange(
  299. goog.dom.TextRange.createFromNodeContents(node), opt_allowPartial);
  300. };
  301. /**
  302. * Tests if the given node is in a document.
  303. * @param {Node} node The node to check.
  304. * @return {boolean} Whether the given node is in the given document.
  305. */
  306. goog.dom.TextRange.isAttachedNode = function(node) {
  307. if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
  308. var returnValue = false;
  309. try {
  310. returnValue = node.parentNode;
  311. } catch (e) {
  312. // IE sometimes throws Invalid Argument errors when a node is detached.
  313. // Note: trying to return a value from the above try block can cause IE
  314. // to crash. It is necessary to use the local returnValue
  315. }
  316. return !!returnValue;
  317. } else {
  318. return goog.dom.contains(node.ownerDocument.body, node);
  319. }
  320. };
  321. /** @override */
  322. goog.dom.TextRange.prototype.isRangeInDocument = function() {
  323. // Ensure any cached nodes are in the document. IE also allows ranges to
  324. // become detached, so we check if the range is still in the document as
  325. // well for IE.
  326. return (!this.startNode_ ||
  327. goog.dom.TextRange.isAttachedNode(this.startNode_)) &&
  328. (!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) &&
  329. (!(goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||
  330. this.getBrowserRangeWrapper_().isRangeInDocument());
  331. };
  332. /** @override */
  333. goog.dom.TextRange.prototype.isCollapsed = function() {
  334. return this.getBrowserRangeWrapper_().isCollapsed();
  335. };
  336. /** @override */
  337. goog.dom.TextRange.prototype.getText = function() {
  338. return this.getBrowserRangeWrapper_().getText();
  339. };
  340. /** @override */
  341. goog.dom.TextRange.prototype.getHtmlFragment = function() {
  342. // TODO(robbyw): Generalize the code in browserrange so it is static and
  343. // just takes an iterator. This would mean we don't always have to create a
  344. // browser range.
  345. return this.getBrowserRangeWrapper_().getHtmlFragment();
  346. };
  347. /** @override */
  348. goog.dom.TextRange.prototype.getValidHtml = function() {
  349. return this.getBrowserRangeWrapper_().getValidHtml();
  350. };
  351. /** @override */
  352. goog.dom.TextRange.prototype.getPastableHtml = function() {
  353. // TODO(robbyw): Get any attributes the table or tr has.
  354. var html = this.getValidHtml();
  355. if (html.match(/^\s*<td\b/i)) {
  356. // Match html starting with a TD.
  357. html = '<table><tbody><tr>' + html + '</tr></tbody></table>';
  358. } else if (html.match(/^\s*<tr\b/i)) {
  359. // Match html starting with a TR.
  360. html = '<table><tbody>' + html + '</tbody></table>';
  361. } else if (html.match(/^\s*<tbody\b/i)) {
  362. // Match html starting with a TBODY.
  363. html = '<table>' + html + '</table>';
  364. } else if (html.match(/^\s*<li\b/i)) {
  365. // Match html starting with an LI.
  366. var container = /** @type {!Element} */ (this.getContainer());
  367. var tagType = goog.dom.TagName.UL;
  368. while (container) {
  369. if (container.tagName == goog.dom.TagName.OL) {
  370. tagType = goog.dom.TagName.OL;
  371. break;
  372. } else if (container.tagName == goog.dom.TagName.UL) {
  373. break;
  374. }
  375. container = container.parentNode;
  376. }
  377. html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');
  378. }
  379. return html;
  380. };
  381. /**
  382. * Returns a TextRangeIterator over the contents of the range. Regardless of
  383. * the direction of the range, the iterator will move in document order.
  384. * @param {boolean=} opt_keys Unused for this iterator.
  385. * @return {!goog.dom.TextRangeIterator} An iterator over tags in the range.
  386. * @override
  387. */
  388. goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
  389. return new goog.dom.TextRangeIterator(
  390. this.getStartNode(), this.getStartOffset(), this.getEndNode(),
  391. this.getEndOffset());
  392. };
  393. // RANGE ACTIONS
  394. /** @override */
  395. goog.dom.TextRange.prototype.select = function() {
  396. this.getBrowserRangeWrapper_().select(this.isReversed_);
  397. };
  398. /** @override */
  399. goog.dom.TextRange.prototype.removeContents = function() {
  400. this.getBrowserRangeWrapper_().removeContents();
  401. this.clearCachedValues_();
  402. };
  403. /**
  404. * Surrounds the text range with the specified element (on Mozilla) or with a
  405. * clone of the specified element (on IE). Returns a reference to the
  406. * surrounding element if the operation was successful; returns null if the
  407. * operation failed.
  408. * @param {Element} element The element with which the selection is to be
  409. * surrounded.
  410. * @return {Element} The surrounding element (same as the argument on Mozilla,
  411. * but not on IE), or null if unsuccessful.
  412. */
  413. goog.dom.TextRange.prototype.surroundContents = function(element) {
  414. var output = this.getBrowserRangeWrapper_().surroundContents(element);
  415. this.clearCachedValues_();
  416. return output;
  417. };
  418. /** @override */
  419. goog.dom.TextRange.prototype.insertNode = function(node, before) {
  420. var output = this.getBrowserRangeWrapper_().insertNode(node, before);
  421. this.clearCachedValues_();
  422. return output;
  423. };
  424. /** @override */
  425. goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
  426. this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
  427. this.clearCachedValues_();
  428. };
  429. // SAVE/RESTORE
  430. /** @override */
  431. goog.dom.TextRange.prototype.saveUsingDom = function() {
  432. return new goog.dom.DomSavedTextRange_(this);
  433. };
  434. // RANGE MODIFICATION
  435. /** @override */
  436. goog.dom.TextRange.prototype.collapse = function(toAnchor) {
  437. var toStart = this.isReversed() ? !toAnchor : toAnchor;
  438. if (this.browserRangeWrapper_) {
  439. this.browserRangeWrapper_.collapse(toStart);
  440. }
  441. if (toStart) {
  442. this.endNode_ = this.startNode_;
  443. this.endOffset_ = this.startOffset_;
  444. } else {
  445. this.startNode_ = this.endNode_;
  446. this.startOffset_ = this.endOffset_;
  447. }
  448. // Collapsed ranges can't be reversed
  449. this.isReversed_ = false;
  450. };
  451. // SAVED RANGE OBJECTS
  452. /**
  453. * A SavedRange implementation using DOM endpoints.
  454. * @param {goog.dom.AbstractRange} range The range to save.
  455. * @constructor
  456. * @extends {goog.dom.SavedRange}
  457. * @private
  458. */
  459. goog.dom.DomSavedTextRange_ = function(range) {
  460. goog.dom.DomSavedTextRange_.base(this, 'constructor');
  461. /**
  462. * The anchor node.
  463. * @type {Node}
  464. * @private
  465. */
  466. this.anchorNode_ = range.getAnchorNode();
  467. /**
  468. * The anchor node offset.
  469. * @type {number}
  470. * @private
  471. */
  472. this.anchorOffset_ = range.getAnchorOffset();
  473. /**
  474. * The focus node.
  475. * @type {Node}
  476. * @private
  477. */
  478. this.focusNode_ = range.getFocusNode();
  479. /**
  480. * The focus node offset.
  481. * @type {number}
  482. * @private
  483. */
  484. this.focusOffset_ = range.getFocusOffset();
  485. };
  486. goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
  487. /**
  488. * @return {!goog.dom.AbstractRange} The restored range.
  489. * @override
  490. */
  491. goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
  492. return /** @suppress {missingRequire} */ (
  493. goog.dom.Range.createFromNodes(
  494. this.anchorNode_, this.anchorOffset_, this.focusNode_,
  495. this.focusOffset_));
  496. };
  497. /** @override */
  498. goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
  499. goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
  500. this.anchorNode_ = null;
  501. this.focusNode_ = null;
  502. };