ierange.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  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 Definition of the IE browser specific range wrapper.
  16. * @suppress {missingRequire} Cannot depend on goog.dom.browserrange because it
  17. * creates a circular dependency.
  18. *
  19. * DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
  20. *
  21. * @author robbyw@google.com (Robby Walker)
  22. */
  23. goog.provide('goog.dom.browserrange.IeRange');
  24. goog.require('goog.array');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.NodeType');
  27. goog.require('goog.dom.RangeEndpoint');
  28. goog.require('goog.dom.TagName');
  29. goog.require('goog.dom.browserrange.AbstractRange');
  30. goog.require('goog.log');
  31. goog.require('goog.string');
  32. /**
  33. * The constructor for IE specific browser ranges.
  34. * @param {TextRange} range The range object.
  35. * @param {Document} doc The document the range exists in.
  36. * @constructor
  37. * @extends {goog.dom.browserrange.AbstractRange}
  38. * @final
  39. */
  40. goog.dom.browserrange.IeRange = function(range, doc) {
  41. /**
  42. * Lazy cache of the node containing the entire selection.
  43. * @private {Node}
  44. */
  45. this.parentNode_ = null;
  46. /**
  47. * Lazy cache of the node containing the start of the selection.
  48. * @private {Node}
  49. */
  50. this.startNode_ = null;
  51. /**
  52. * Lazy cache of the node containing the end of the selection.
  53. * @private {Node}
  54. */
  55. this.endNode_ = null;
  56. /**
  57. * Lazy cache of the offset in startNode_ where this range starts.
  58. * @private {number}
  59. */
  60. this.startOffset_ = -1;
  61. /**
  62. * Lazy cache of the offset in endNode_ where this range ends.
  63. * @private {number}
  64. */
  65. this.endOffset_ = -1;
  66. /**
  67. * The browser range object this class wraps.
  68. * @private {TextRange}
  69. */
  70. this.range_ = range;
  71. /**
  72. * The document the range exists in.
  73. * @private {Document}
  74. */
  75. this.doc_ = doc;
  76. };
  77. goog.inherits(
  78. goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange);
  79. /**
  80. * Logging object.
  81. * @type {goog.log.Logger}
  82. * @private
  83. */
  84. goog.dom.browserrange.IeRange.logger_ =
  85. goog.log.getLogger('goog.dom.browserrange.IeRange');
  86. /**
  87. * Returns a browser range spanning the given node's contents.
  88. * @param {Node} node The node to select.
  89. * @return {!TextRange} A browser range spanning the node's contents.
  90. * @private
  91. */
  92. goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {
  93. var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();
  94. if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  95. // Elements are easy.
  96. nodeRange.moveToElementText(node);
  97. // Note(user) : If there are no child nodes of the element, the
  98. // range.htmlText includes the element's outerHTML. The range created above
  99. // is not collapsed, and should be collapsed explicitly.
  100. // Example : node = <div></div>
  101. // But if the node is sth like <br>, it shouldn't be collapsed.
  102. if (goog.dom.browserrange.canContainRangeEndpoint(node) &&
  103. !node.childNodes.length) {
  104. nodeRange.collapse(false);
  105. }
  106. } else {
  107. // Text nodes are hard.
  108. // Compute the offset from the nearest element related position.
  109. var offset = 0;
  110. var sibling = node;
  111. while (sibling = sibling.previousSibling) {
  112. var nodeType = sibling.nodeType;
  113. if (nodeType == goog.dom.NodeType.TEXT) {
  114. offset += sibling.length;
  115. } else if (nodeType == goog.dom.NodeType.ELEMENT) {
  116. // Move to the space after this element.
  117. nodeRange.moveToElementText(sibling);
  118. break;
  119. }
  120. }
  121. if (!sibling) {
  122. nodeRange.moveToElementText(node.parentNode);
  123. }
  124. nodeRange.collapse(!sibling);
  125. if (offset) {
  126. nodeRange.move('character', offset);
  127. }
  128. nodeRange.moveEnd('character', node.length);
  129. }
  130. return nodeRange;
  131. };
  132. /**
  133. * Returns a browser range spanning the given nodes.
  134. * @param {Node} startNode The node to start with.
  135. * @param {number} startOffset The offset within the start node.
  136. * @param {Node} endNode The node to end with.
  137. * @param {number} endOffset The offset within the end node.
  138. * @return {!TextRange} A browser range spanning the node's contents.
  139. * @private
  140. */
  141. goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(
  142. startNode, startOffset, endNode, endOffset) {
  143. // Create a range starting at the correct start position.
  144. var child, collapse = false;
  145. if (startNode.nodeType == goog.dom.NodeType.ELEMENT) {
  146. if (startOffset > startNode.childNodes.length) {
  147. goog.log.error(
  148. goog.dom.browserrange.IeRange.logger_,
  149. 'Cannot have startOffset > startNode child count');
  150. }
  151. child = startNode.childNodes[startOffset];
  152. collapse = !child;
  153. startNode = child || startNode.lastChild || startNode;
  154. startOffset = 0;
  155. }
  156. var leftRange =
  157. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode);
  158. // This happens only when startNode is a text node.
  159. if (startOffset) {
  160. leftRange.move('character', startOffset);
  161. }
  162. // The range movements in IE are still an approximation to the standard W3C
  163. // behavior, and IE has its trickery when it comes to htmlText and text
  164. // properties of the range. So we short-circuit computation whenever we can.
  165. if (startNode == endNode && startOffset == endOffset) {
  166. leftRange.collapse(true);
  167. return leftRange;
  168. }
  169. // This can happen only when the startNode is an element, and there is no node
  170. // at the given offset. We start at the last point inside the startNode in
  171. // that case.
  172. if (collapse) {
  173. leftRange.collapse(false);
  174. }
  175. // Create a range that ends at the right position.
  176. collapse = false;
  177. if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {
  178. if (endOffset > endNode.childNodes.length) {
  179. goog.log.error(
  180. goog.dom.browserrange.IeRange.logger_,
  181. 'Cannot have endOffset > endNode child count');
  182. }
  183. child = endNode.childNodes[endOffset];
  184. endNode = child || endNode.lastChild || endNode;
  185. endOffset = 0;
  186. collapse = !child;
  187. }
  188. var rightRange =
  189. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode);
  190. rightRange.collapse(!collapse);
  191. if (endOffset) {
  192. rightRange.moveEnd('character', endOffset);
  193. }
  194. // Merge and return.
  195. leftRange.setEndPoint('EndToEnd', rightRange);
  196. return leftRange;
  197. };
  198. /**
  199. * Create a range object that selects the given node's text.
  200. * @param {Node} node The node to select.
  201. * @return {!goog.dom.browserrange.IeRange} An IE range wrapper object.
  202. */
  203. goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {
  204. var range = new goog.dom.browserrange.IeRange(
  205. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node),
  206. goog.dom.getOwnerDocument(node));
  207. if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
  208. range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;
  209. range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);
  210. range.endOffset_ = range.startOffset_ + 1;
  211. } else {
  212. // Note(user) : Emulate the behavior of W3CRange - Go to deepest possible
  213. // range containers on both edges. It seems W3CRange did this to match the
  214. // IE behavior, and now it is a circle. Changing W3CRange may break clients
  215. // in all sorts of ways.
  216. var tempNode, leaf = node;
  217. while ((tempNode = leaf.firstChild) &&
  218. goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
  219. leaf = tempNode;
  220. }
  221. range.startNode_ = leaf;
  222. range.startOffset_ = 0;
  223. leaf = node;
  224. while ((tempNode = leaf.lastChild) &&
  225. goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
  226. leaf = tempNode;
  227. }
  228. range.endNode_ = leaf;
  229. range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ?
  230. leaf.childNodes.length :
  231. leaf.length;
  232. range.parentNode_ = node;
  233. }
  234. return range;
  235. };
  236. /**
  237. * Static method that returns the proper type of browser range.
  238. * @param {Node} startNode The node to start with.
  239. * @param {number} startOffset The offset within the start node.
  240. * @param {Node} endNode The node to end with.
  241. * @param {number} endOffset The offset within the end node.
  242. * @return {!goog.dom.browserrange.AbstractRange} A wrapper object.
  243. */
  244. goog.dom.browserrange.IeRange.createFromNodes = function(
  245. startNode, startOffset, endNode, endOffset) {
  246. var range = new goog.dom.browserrange.IeRange(
  247. goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(
  248. startNode, startOffset, endNode, endOffset),
  249. goog.dom.getOwnerDocument(startNode));
  250. range.startNode_ = startNode;
  251. range.startOffset_ = startOffset;
  252. range.endNode_ = endNode;
  253. range.endOffset_ = endOffset;
  254. return range;
  255. };
  256. /**
  257. * @return {!goog.dom.browserrange.IeRange} A clone of this range.
  258. * @override
  259. */
  260. goog.dom.browserrange.IeRange.prototype.clone = function() {
  261. var range =
  262. new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_);
  263. range.parentNode_ = this.parentNode_;
  264. range.startNode_ = this.startNode_;
  265. range.endNode_ = this.endNode_;
  266. return range;
  267. };
  268. /** @override */
  269. goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {
  270. return this.range_;
  271. };
  272. /**
  273. * Clears the cached values for containers.
  274. * @private
  275. */
  276. goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {
  277. this.parentNode_ = this.startNode_ = this.endNode_ = null;
  278. this.startOffset_ = this.endOffset_ = -1;
  279. };
  280. /** @override */
  281. goog.dom.browserrange.IeRange.prototype.getContainer = function() {
  282. if (!this.parentNode_) {
  283. var selectText = this.range_.text;
  284. // If the selection ends with spaces, we need to remove these to get the
  285. // parent container of only the real contents. This is to get around IE's
  286. // inconsistency where it selects the spaces after a word when you double
  287. // click, but leaves out the spaces during execCommands.
  288. var range = this.range_.duplicate();
  289. // We can't use goog.string.trimRight, as that will remove other whitespace
  290. // too.
  291. var rightTrimmedSelectText = selectText.replace(/ +$/, '');
  292. var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;
  293. if (numSpacesAtEnd) {
  294. range.moveEnd('character', -numSpacesAtEnd);
  295. }
  296. // Get the parent node. This should be the end, but alas, it is not.
  297. var parent = range.parentElement();
  298. var htmlText = range.htmlText;
  299. var htmlTextLen = goog.string.stripNewlines(htmlText).length;
  300. if (this.isCollapsed() && htmlTextLen > 0) {
  301. return (this.parentNode_ = parent);
  302. }
  303. // Deal with selection bug where IE thinks one of the selection's children
  304. // is actually the selection's parent. Relies on the assumption that the
  305. // HTML text of the parent container is longer than the length of the
  306. // selection's HTML text.
  307. // Also note IE will sometimes insert \r and \n whitespace, which should be
  308. // disregarded. Otherwise the loop may run too long and return wrong parent
  309. while (htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {
  310. parent = parent.parentNode;
  311. }
  312. // Deal with IE's selecting the outer tags when you double click
  313. // If the innerText is the same, then we just want the inner node
  314. while (parent.childNodes.length == 1 &&
  315. parent.innerText ==
  316. goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) {
  317. // A container should be an element which can have children or a text
  318. // node. Elements like IMG, BR, etc. can not be containers.
  319. if (!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {
  320. break;
  321. }
  322. parent = parent.firstChild;
  323. }
  324. // If the selection is empty, we may need to do extra work to position it
  325. // properly.
  326. if (selectText.length == 0) {
  327. parent = this.findDeepestContainer_(parent);
  328. }
  329. this.parentNode_ = parent;
  330. }
  331. return this.parentNode_;
  332. };
  333. /**
  334. * Helper method to find the deepest parent for this range, starting
  335. * the search from {@code node}, which must contain the range.
  336. * @param {Node} node The node to start the search from.
  337. * @return {Node} The deepest parent for this range.
  338. * @private
  339. */
  340. goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {
  341. var childNodes = node.childNodes;
  342. for (var i = 0, len = childNodes.length; i < len; i++) {
  343. var child = childNodes[i];
  344. if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
  345. var childRange =
  346. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);
  347. var start = goog.dom.RangeEndpoint.START;
  348. var end = goog.dom.RangeEndpoint.END;
  349. // There are two types of erratic nodes where the range over node has
  350. // different htmlText than the node's outerHTML.
  351. // Case 1 - A node with magic &nbsp; child. In this case :
  352. // nodeRange.htmlText shows &nbsp; ('<p>&nbsp;</p>), while
  353. // node.outerHTML doesn't show the magic node (<p></p>).
  354. // Case 2 - Empty span. In this case :
  355. // node.outerHTML shows '<span></span>'
  356. // node.htmlText is just empty string ''.
  357. var isChildRangeErratic = (childRange.htmlText != child.outerHTML);
  358. // Moreover the inRange comparison fails only when the
  359. var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;
  360. // In case 2 mentioned above, childRange is also collapsed. So we need to
  361. // compare start of this range with both start and end of child range.
  362. var inChildRange = isNativeInRangeErratic ?
  363. (this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 &&
  364. this.compareBrowserRangeEndpoints(childRange, start, end) <= 0) :
  365. this.range_.inRange(childRange);
  366. if (inChildRange) {
  367. return this.findDeepestContainer_(child);
  368. }
  369. }
  370. }
  371. return node;
  372. };
  373. /** @override */
  374. goog.dom.browserrange.IeRange.prototype.getStartNode = function() {
  375. if (!this.startNode_) {
  376. this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);
  377. if (this.isCollapsed()) {
  378. this.endNode_ = this.startNode_;
  379. }
  380. }
  381. return this.startNode_;
  382. };
  383. /** @override */
  384. goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {
  385. if (this.startOffset_ < 0) {
  386. this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);
  387. if (this.isCollapsed()) {
  388. this.endOffset_ = this.startOffset_;
  389. }
  390. }
  391. return this.startOffset_;
  392. };
  393. /** @override */
  394. goog.dom.browserrange.IeRange.prototype.getEndNode = function() {
  395. if (this.isCollapsed()) {
  396. return this.getStartNode();
  397. }
  398. if (!this.endNode_) {
  399. this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END);
  400. }
  401. return this.endNode_;
  402. };
  403. /** @override */
  404. goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {
  405. if (this.isCollapsed()) {
  406. return this.getStartOffset();
  407. }
  408. if (this.endOffset_ < 0) {
  409. this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);
  410. if (this.isCollapsed()) {
  411. this.startOffset_ = this.endOffset_;
  412. }
  413. }
  414. return this.endOffset_;
  415. };
  416. /** @override */
  417. goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(
  418. range, thisEndpoint, otherEndpoint) {
  419. return this.range_.compareEndPoints(
  420. (thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +
  421. (otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
  422. range);
  423. };
  424. /**
  425. * Recurses to find the correct node for the given endpoint.
  426. * @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the node for.
  427. * @param {Node=} opt_node Optional node to start the search from.
  428. * @return {Node} The deepest node containing the endpoint.
  429. * @private
  430. */
  431. goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(
  432. endpoint, opt_node) {
  433. /** @type {Node} */
  434. var node = opt_node || this.getContainer();
  435. // If we're at a leaf in the DOM, we're done.
  436. if (!node || !node.firstChild) {
  437. return node;
  438. }
  439. var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
  440. var isStartEndpoint = endpoint == start;
  441. // Find the first/last child that overlaps the selection.
  442. // NOTE(user) : One of the children can be the magic &nbsp; node. This
  443. // node will have only nodeType property as valid and accessible. All other
  444. // dom related properties like ownerDocument, parentNode, nextSibling etc
  445. // cause error when accessed. Therefore use the for-loop on childNodes to
  446. // iterate.
  447. for (var j = 0, length = node.childNodes.length; j < length; j++) {
  448. var i = isStartEndpoint ? j : length - j - 1;
  449. var child = node.childNodes[i];
  450. var childRange;
  451. try {
  452. childRange = goog.dom.browserrange.createRangeFromNodeContents(child);
  453. } catch (e) {
  454. // If the child is the magic &nbsp; node, then the above will throw
  455. // error. The magic node exists only when editing using keyboard, so can
  456. // not add any unit test.
  457. continue;
  458. }
  459. var ieRange = childRange.getBrowserRange();
  460. // Case 1 : Finding end points when this range is collapsed.
  461. // Note that in case of collapsed range, getEnd{Node,Offset} call
  462. // getStart{Node,Offset}.
  463. if (this.isCollapsed()) {
  464. // Handle situations where caret is not in a text node. In such cases,
  465. // the adjacent child won't be a valid range endpoint container.
  466. if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
  467. // The following handles a scenario like <div><BR>[caret]<BR></div>,
  468. // where point should be (div, 1).
  469. if (this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {
  470. this.startOffset_ = this.endOffset_ = i;
  471. return node;
  472. }
  473. } else if (childRange.containsRange(this)) {
  474. // For collapsed range, we should invert the containsRange check with
  475. // childRange.
  476. return this.getEndpointNode_(endpoint, child);
  477. }
  478. // Case 2 - The first child encountered to have overlap this range is
  479. // contained entirely in this range.
  480. } else if (this.containsRange(childRange)) {
  481. // If it is an element which can not be a range endpoint container, the
  482. // current child offset can be used to deduce the endpoint offset.
  483. if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
  484. // Container can't be any deeper, so current node is the container.
  485. if (isStartEndpoint) {
  486. this.startOffset_ = i;
  487. } else {
  488. this.endOffset_ = i + 1;
  489. }
  490. return node;
  491. }
  492. // If child can contain range endpoints, recurse inside this child.
  493. return this.getEndpointNode_(endpoint, child);
  494. // Case 3 - Partial non-adjacency overlap.
  495. } else if (
  496. this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 &&
  497. this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {
  498. // If this child overlaps the selection partially, recurse down to find
  499. // the first/last child the next level down that overlaps the selection
  500. // completely. We do not consider edge-adjacency (== 0) as overlap.
  501. return this.getEndpointNode_(endpoint, child);
  502. }
  503. }
  504. // None of the children of this node overlapped the selection, that means
  505. // the selection starts/ends in this node directly.
  506. return node;
  507. };
  508. /**
  509. * Compares one endpoint of this range with the endpoint of a node.
  510. * For internal methods, we should prefer this method to containsNode.
  511. * containsNode has a lot of false negatives when we're dealing with
  512. * {@code <br>} tags.
  513. *
  514. * @param {Node} node The node to compare against.
  515. * @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range
  516. * to compare with.
  517. * @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the node
  518. * to compare with.
  519. * @return {number} 0 if the endpoints are equal, negative if this range
  520. * endpoint comes before the other node endpoint, and positive otherwise.
  521. * @private
  522. */
  523. goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(
  524. node, thisEndpoint, otherEndpoint) {
  525. /** @suppress {missingRequire} Circular dep with browserrange */
  526. return this.range_.compareEndPoints(
  527. (thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +
  528. (otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
  529. goog.dom.browserrange.createRangeFromNodeContents(node)
  530. .getBrowserRange());
  531. };
  532. /**
  533. * Returns the offset into the start/end container.
  534. * @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the offset for.
  535. * @param {Node=} opt_container The container to get the offset relative to.
  536. * Defaults to the value returned by getStartNode/getEndNode.
  537. * @return {number} The offset.
  538. * @private
  539. */
  540. goog.dom.browserrange.IeRange.prototype.getOffset_ = function(
  541. endpoint, opt_container) {
  542. var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;
  543. var container = opt_container ||
  544. (isStartEndpoint ? this.getStartNode() : this.getEndNode());
  545. if (container.nodeType == goog.dom.NodeType.ELEMENT) {
  546. // Find the first/last child that overlaps the selection
  547. var children = container.childNodes;
  548. var len = children.length;
  549. var edge = isStartEndpoint ? 0 : len - 1;
  550. var sign = isStartEndpoint ? 1 : -1;
  551. // We find the index in the child array of the endpoint of the selection.
  552. for (var i = edge; i >= 0 && i < len; i += sign) {
  553. var child = children[i];
  554. // Ignore the child nodes, which could be end point containers.
  555. /** @suppress {missingRequire} Circular dep with browserrange */
  556. if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
  557. continue;
  558. }
  559. // Stop looping when we reach the edge of the selection.
  560. var endPointCompare =
  561. this.compareNodeEndpoints_(child, endpoint, endpoint);
  562. if (endPointCompare == 0) {
  563. return isStartEndpoint ? i : i + 1;
  564. }
  565. }
  566. // When starting from the end in an empty container, we erroneously return
  567. // -1: fix this to return 0.
  568. return i == -1 ? 0 : i;
  569. } else {
  570. // Get a temporary range object.
  571. var range = this.range_.duplicate();
  572. // Create a range that selects the entire container.
  573. var nodeRange =
  574. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container);
  575. // Now, intersect our range with the container range - this should give us
  576. // the part of our selection that is in the container.
  577. range.setEndPoint(isStartEndpoint ? 'EndToEnd' : 'StartToStart', nodeRange);
  578. var rangeLength = range.text.length;
  579. return isStartEndpoint ? container.length - rangeLength : rangeLength;
  580. }
  581. };
  582. /**
  583. * Returns the text of the given node. Uses IE specific properties.
  584. * @param {Node} node The node to retrieve the text of.
  585. * @return {string} The node's text.
  586. * @private
  587. */
  588. goog.dom.browserrange.IeRange.getNodeText_ = function(node) {
  589. return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue :
  590. node.innerText;
  591. };
  592. /**
  593. * Tests whether this range is valid (i.e. whether its endpoints are still in
  594. * the document). A range becomes invalid when, after this object was created,
  595. * either one or both of its endpoints are removed from the document. Use of
  596. * an invalid range can lead to runtime errors, particularly in IE.
  597. * @return {boolean} Whether the range is valid.
  598. */
  599. goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {
  600. var range = this.doc_.body.createTextRange();
  601. range.moveToElementText(this.doc_.body);
  602. return this.containsRange(
  603. new goog.dom.browserrange.IeRange(range, this.doc_), true);
  604. };
  605. /** @override */
  606. goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {
  607. // Note(user) : The earlier implementation used (range.text == ''), but this
  608. // fails when (range.htmlText == '<br>')
  609. // Alternative: this.range_.htmlText == '';
  610. return this.range_.compareEndPoints('StartToEnd', this.range_) == 0;
  611. };
  612. /** @override */
  613. goog.dom.browserrange.IeRange.prototype.getText = function() {
  614. return this.range_.text;
  615. };
  616. /** @override */
  617. goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {
  618. return this.range_.htmlText;
  619. };
  620. // SELECTION MODIFICATION
  621. /** @override */
  622. goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {
  623. // IE doesn't support programmatic reversed selections.
  624. this.range_.select();
  625. };
  626. /** @override */
  627. goog.dom.browserrange.IeRange.prototype.removeContents = function() {
  628. // NOTE: Sometimes htmlText is non-empty, but the range is actually empty.
  629. // TODO(gboyer): The htmlText check is probably unnecessary, but I left it in
  630. // for paranoia.
  631. if (!this.isCollapsed() && this.range_.htmlText) {
  632. // Store some before-removal state.
  633. var startNode = this.getStartNode();
  634. var endNode = this.getEndNode();
  635. var oldText = this.range_.text;
  636. // IE sometimes deletes nodes unrelated to the selection. This trick fixes
  637. // that problem most of the time. Even though it looks like a no-op, it is
  638. // somehow changing IE's internal state such that empty unrelated nodes are
  639. // no longer deleted.
  640. var clone = this.range_.duplicate();
  641. clone.moveStart('character', 1);
  642. clone.moveStart('character', -1);
  643. // However, sometimes moving the start back and forth ends up changing the
  644. // range.
  645. // TODO(gboyer): This condition used to happen for empty ranges, but (1)
  646. // never worked, and (2) the isCollapsed call should protect against empty
  647. // ranges better than before. However, this is left for paranoia.
  648. if (clone.text == oldText) {
  649. this.range_ = clone;
  650. }
  651. // Use the browser's native deletion code.
  652. this.range_.text = '';
  653. this.clearCachedValues_();
  654. // Unfortunately, when deleting a portion of a single text node, IE creates
  655. // an extra text node unlike other browsers which just change the text in
  656. // the node. We normalize for that behavior here, making IE behave like all
  657. // the other browsers.
  658. var newStartNode = this.getStartNode();
  659. var newStartOffset = this.getStartOffset();
  660. try {
  661. var sibling = startNode.nextSibling;
  662. if (startNode == endNode && startNode.parentNode &&
  663. startNode.nodeType == goog.dom.NodeType.TEXT && sibling &&
  664. sibling.nodeType == goog.dom.NodeType.TEXT) {
  665. startNode.nodeValue += sibling.nodeValue;
  666. goog.dom.removeNode(sibling);
  667. // Make sure to reselect the appropriate position.
  668. this.range_ =
  669. goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode);
  670. this.range_.move('character', newStartOffset);
  671. this.clearCachedValues_();
  672. }
  673. } catch (e) {
  674. // IE throws errors on orphaned nodes.
  675. }
  676. }
  677. };
  678. /**
  679. * @param {TextRange} range The range to get a dom helper for.
  680. * @return {!goog.dom.DomHelper} A dom helper for the document the range
  681. * resides in.
  682. * @private
  683. */
  684. goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {
  685. return goog.dom.getDomHelper(range.parentElement());
  686. };
  687. /**
  688. * Pastes the given element into the given range, returning the resulting
  689. * element.
  690. * @param {TextRange} range The range to paste into.
  691. * @param {Element} element The node to insert a copy of.
  692. * @param {goog.dom.DomHelper=} opt_domHelper DOM helper object for the document
  693. * the range resides in.
  694. * @return {Element} The resulting copy of element.
  695. * @private
  696. */
  697. goog.dom.browserrange.IeRange.pasteElement_ = function(
  698. range, element, opt_domHelper) {
  699. opt_domHelper =
  700. opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range);
  701. // Make sure the node has a unique id.
  702. var id;
  703. var originalId = id = element.id;
  704. if (!id) {
  705. id = element.id = goog.string.createUniqueString();
  706. }
  707. // Insert (a clone of) the node.
  708. range.pasteHTML(element.outerHTML);
  709. // Pasting the outerHTML of the modified element into the document creates
  710. // a clone of the element argument. We want to return a reference to the
  711. // clone, not the original. However we need to remove the temporary ID
  712. // first.
  713. element = opt_domHelper.getElement(id);
  714. // If element is null here, we failed.
  715. if (element) {
  716. if (!originalId) {
  717. element.removeAttribute('id');
  718. }
  719. }
  720. return element;
  721. };
  722. /** @override */
  723. goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {
  724. // Make sure the element is detached from the document.
  725. goog.dom.removeNode(element);
  726. // IE more or less guarantees that range.htmlText is well-formed & valid.
  727. element.innerHTML = this.range_.htmlText;
  728. element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);
  729. // If element is null here, we failed.
  730. if (element) {
  731. this.range_.moveToElementText(element);
  732. }
  733. this.clearCachedValues_();
  734. return element;
  735. };
  736. /**
  737. * Internal handler for inserting a node.
  738. * @param {TextRange} clone A clone of this range's browser range object.
  739. * @param {Node} node The node to insert.
  740. * @param {boolean} before Whether to insert the node before or after the range.
  741. * @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use.
  742. * @return {Node} The resulting copy of node.
  743. * @private
  744. */
  745. goog.dom.browserrange.IeRange.insertNode_ = function(
  746. clone, node, before, opt_domHelper) {
  747. // Get a DOM helper.
  748. opt_domHelper =
  749. opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone);
  750. // If it's not an element, wrap it in one.
  751. var isNonElement;
  752. if (node.nodeType != goog.dom.NodeType.ELEMENT) {
  753. isNonElement = true;
  754. node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node);
  755. }
  756. clone.collapse(before);
  757. node = goog.dom.browserrange.IeRange.pasteElement_(
  758. clone,
  759. /** @type {!Element} */ (node), opt_domHelper);
  760. // If we didn't want an element, unwrap the element and return the node.
  761. if (isNonElement) {
  762. // pasteElement_() may have returned a copy of the wrapper div, and the
  763. // node it wraps could also be a new copy. So we must extract that new
  764. // node from the new wrapper.
  765. var newNonElement = node.firstChild;
  766. opt_domHelper.flattenElement(node);
  767. node = newNonElement;
  768. }
  769. return node;
  770. };
  771. /** @override */
  772. goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {
  773. var output = goog.dom.browserrange.IeRange.insertNode_(
  774. this.range_.duplicate(), node, before);
  775. this.clearCachedValues_();
  776. return output;
  777. };
  778. /** @override */
  779. goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(
  780. startNode, endNode) {
  781. var clone1 = this.range_.duplicate();
  782. var clone2 = this.range_.duplicate();
  783. goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);
  784. goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);
  785. this.clearCachedValues_();
  786. };
  787. /** @override */
  788. goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {
  789. this.range_.collapse(toStart);
  790. if (toStart) {
  791. this.endNode_ = this.startNode_;
  792. this.endOffset_ = this.startOffset_;
  793. } else {
  794. this.startNode_ = this.endNode_;
  795. this.startOffset_ = this.endOffset_;
  796. }
  797. };