w3crange.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 W3C spec following range wrapper.
  16. *
  17. * DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
  18. *
  19. * @author robbyw@google.com (Robby Walker)
  20. */
  21. goog.provide('goog.dom.browserrange.W3cRange');
  22. goog.require('goog.array');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.NodeType');
  25. goog.require('goog.dom.RangeEndpoint');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.dom.browserrange.AbstractRange');
  28. goog.require('goog.string');
  29. goog.require('goog.userAgent');
  30. /**
  31. * The constructor for W3C specific browser ranges.
  32. * @param {Range} range The range object.
  33. * @constructor
  34. * @extends {goog.dom.browserrange.AbstractRange}
  35. */
  36. goog.dom.browserrange.W3cRange = function(range) {
  37. this.range_ = range;
  38. };
  39. goog.inherits(
  40. goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange);
  41. /**
  42. * Returns a browser range spanning the given node's contents.
  43. * @param {Node} node The node to select.
  44. * @return {!Range} A browser range spanning the node's contents.
  45. * @protected
  46. */
  47. goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {
  48. var nodeRange = goog.dom.getOwnerDocument(node).createRange();
  49. if (node.nodeType == goog.dom.NodeType.TEXT) {
  50. nodeRange.setStart(node, 0);
  51. nodeRange.setEnd(node, node.length);
  52. } else {
  53. /** @suppress {missingRequire} */
  54. if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
  55. var rangeParent = node.parentNode;
  56. var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);
  57. nodeRange.setStart(rangeParent, rangeStartOffset);
  58. nodeRange.setEnd(rangeParent, rangeStartOffset + 1);
  59. } else {
  60. var tempNode, leaf = node;
  61. while ((tempNode = leaf.firstChild) &&
  62. /** @suppress {missingRequire} */
  63. goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
  64. leaf = tempNode;
  65. }
  66. nodeRange.setStart(leaf, 0);
  67. leaf = node;
  68. /** @suppress {missingRequire} Circular dep with browserrange */
  69. while ((tempNode = leaf.lastChild) &&
  70. goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
  71. leaf = tempNode;
  72. }
  73. nodeRange.setEnd(
  74. leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ?
  75. leaf.childNodes.length :
  76. leaf.length);
  77. }
  78. }
  79. return nodeRange;
  80. };
  81. /**
  82. * Returns a browser range spanning the given nodes.
  83. * @param {Node} startNode The node to start with - should not be a BR.
  84. * @param {number} startOffset The offset within the start node.
  85. * @param {Node} endNode The node to end with - should not be a BR.
  86. * @param {number} endOffset The offset within the end node.
  87. * @return {!Range} A browser range spanning the node's contents.
  88. * @protected
  89. */
  90. goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(
  91. startNode, startOffset, endNode, endOffset) {
  92. // Create and return the range.
  93. var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();
  94. nodeRange.setStart(startNode, startOffset);
  95. nodeRange.setEnd(endNode, endOffset);
  96. return nodeRange;
  97. };
  98. /**
  99. * Creates a range object that selects the given node's text.
  100. * @param {Node} node The node to select.
  101. * @return {!goog.dom.browserrange.W3cRange} A Gecko range wrapper object.
  102. */
  103. goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {
  104. return new goog.dom.browserrange.W3cRange(
  105. goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node));
  106. };
  107. /**
  108. * Creates a range object that selects between the given nodes.
  109. * @param {Node} startNode The node to start with.
  110. * @param {number} startOffset The offset within the start node.
  111. * @param {Node} endNode The node to end with.
  112. * @param {number} endOffset The offset within the end node.
  113. * @return {!goog.dom.browserrange.W3cRange} A wrapper object.
  114. */
  115. goog.dom.browserrange.W3cRange.createFromNodes = function(
  116. startNode, startOffset, endNode, endOffset) {
  117. return new goog.dom.browserrange.W3cRange(
  118. goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(
  119. startNode, startOffset, endNode, endOffset));
  120. };
  121. /**
  122. * @return {!goog.dom.browserrange.W3cRange} A clone of this range.
  123. * @override
  124. */
  125. goog.dom.browserrange.W3cRange.prototype.clone = function() {
  126. return new this.constructor(this.range_.cloneRange());
  127. };
  128. /** @override */
  129. goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {
  130. return this.range_;
  131. };
  132. /** @override */
  133. goog.dom.browserrange.W3cRange.prototype.getContainer = function() {
  134. return this.range_.commonAncestorContainer;
  135. };
  136. /** @override */
  137. goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {
  138. return this.range_.startContainer;
  139. };
  140. /** @override */
  141. goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {
  142. return this.range_.startOffset;
  143. };
  144. /** @override */
  145. goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {
  146. return this.range_.endContainer;
  147. };
  148. /** @override */
  149. goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {
  150. return this.range_.endOffset;
  151. };
  152. /** @override */
  153. goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints =
  154. function(range, thisEndpoint, otherEndpoint) {
  155. return this.range_.compareBoundaryPoints(
  156. otherEndpoint == goog.dom.RangeEndpoint.START ?
  157. (thisEndpoint == goog.dom.RangeEndpoint.START ?
  158. goog.global['Range'].START_TO_START :
  159. goog.global['Range'].START_TO_END) :
  160. (thisEndpoint == goog.dom.RangeEndpoint.START ?
  161. goog.global['Range'].END_TO_START :
  162. goog.global['Range'].END_TO_END),
  163. /** @type {Range} */ (range));
  164. };
  165. /** @override */
  166. goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {
  167. return this.range_.collapsed;
  168. };
  169. /** @override */
  170. goog.dom.browserrange.W3cRange.prototype.getText = function() {
  171. return this.range_.toString();
  172. };
  173. /** @override */
  174. goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {
  175. var div = goog.dom.getDomHelper(this.range_.startContainer)
  176. .createDom(goog.dom.TagName.DIV);
  177. div.appendChild(this.range_.cloneContents());
  178. var result = div.innerHTML;
  179. if (goog.string.startsWith(result, '<') ||
  180. !this.isCollapsed() && !goog.string.contains(result, '<')) {
  181. // We attempt to mimic IE, which returns no containing element when a
  182. // only text nodes are selected, does return the containing element when
  183. // the selection is empty, and does return the element when multiple nodes
  184. // are selected.
  185. return result;
  186. }
  187. var container = this.getContainer();
  188. container = container.nodeType == goog.dom.NodeType.ELEMENT ?
  189. container :
  190. container.parentNode;
  191. var html = goog.dom.getOuterHtml(
  192. /** @type {!Element} */ (container.cloneNode(false)));
  193. return html.replace('>', '>' + result);
  194. };
  195. // SELECTION MODIFICATION
  196. /** @override */
  197. goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {
  198. var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
  199. this.selectInternal(win.getSelection(), reverse);
  200. };
  201. /**
  202. * Select this range.
  203. * @param {Selection} selection Browser selection object.
  204. * @param {*} reverse Whether to select this range in reverse.
  205. * @protected
  206. */
  207. goog.dom.browserrange.W3cRange.prototype.selectInternal = function(
  208. selection, reverse) {
  209. // Browser-specific tricks are needed to create reversed selections
  210. // programatically. For this generic W3C codepath, ignore the reverse
  211. // parameter.
  212. selection.removeAllRanges();
  213. selection.addRange(this.range_);
  214. };
  215. /** @override */
  216. goog.dom.browserrange.W3cRange.prototype.removeContents = function() {
  217. var range = this.range_;
  218. range.extractContents();
  219. if (range.startContainer.hasChildNodes()) {
  220. // Remove any now empty nodes surrounding the extracted contents.
  221. var rangeStartContainer =
  222. range.startContainer.childNodes[range.startOffset];
  223. if (rangeStartContainer) {
  224. var rangePrevious = rangeStartContainer.previousSibling;
  225. if (goog.dom.getRawTextContent(rangeStartContainer) == '') {
  226. goog.dom.removeNode(rangeStartContainer);
  227. }
  228. if (rangePrevious && goog.dom.getRawTextContent(rangePrevious) == '') {
  229. goog.dom.removeNode(rangePrevious);
  230. }
  231. }
  232. }
  233. if (goog.userAgent.EDGE_OR_IE) {
  234. // Unfortunately, when deleting a portion of a single text node, IE creates
  235. // an extra text node instead of modifying the nodeValue of the start node.
  236. // We normalize for that behavior here, similar to code in
  237. // goog.dom.browserrange.IeRange#removeContents
  238. // See https://connect.microsoft.com/IE/feedback/details/746591
  239. var startNode = this.getStartNode();
  240. var startOffset = this.getStartOffset();
  241. var endNode = this.getEndNode();
  242. var endOffset = this.getEndOffset();
  243. var sibling = startNode.nextSibling;
  244. if (startNode == endNode && startNode.parentNode &&
  245. startNode.nodeType == goog.dom.NodeType.TEXT && sibling &&
  246. sibling.nodeType == goog.dom.NodeType.TEXT) {
  247. startNode.nodeValue += sibling.nodeValue;
  248. goog.dom.removeNode(sibling);
  249. // Modifying the node value clears the range offsets. Reselect the
  250. // position in the modified start node.
  251. range.setStart(startNode, startOffset);
  252. range.setEnd(endNode, endOffset);
  253. }
  254. }
  255. };
  256. /** @override */
  257. goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {
  258. this.range_.surroundContents(element);
  259. return element;
  260. };
  261. /** @override */
  262. goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {
  263. var range = this.range_.cloneRange();
  264. range.collapse(before);
  265. range.insertNode(node);
  266. range.detach();
  267. return node;
  268. };
  269. /** @override */
  270. goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(
  271. startNode, endNode) {
  272. var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
  273. /** @suppress {missingRequire} */
  274. var selectionRange = goog.dom.Range.createFromWindow(win);
  275. if (selectionRange) {
  276. var sNode = selectionRange.getStartNode();
  277. var eNode = selectionRange.getEndNode();
  278. var sOffset = selectionRange.getStartOffset();
  279. var eOffset = selectionRange.getEndOffset();
  280. }
  281. var clone1 = this.range_.cloneRange();
  282. var clone2 = this.range_.cloneRange();
  283. clone1.collapse(false);
  284. clone2.collapse(true);
  285. clone1.insertNode(endNode);
  286. clone2.insertNode(startNode);
  287. clone1.detach();
  288. clone2.detach();
  289. if (selectionRange) {
  290. // There are 4 ways that surroundWithNodes can wreck the saved
  291. // selection object. All of them happen when an inserted node splits
  292. // a text node, and one of the end points of the selection was in the
  293. // latter half of that text node.
  294. //
  295. // Clients of this library should use saveUsingCarets to avoid this
  296. // problem. Unfortunately, saveUsingCarets uses this method, so that's
  297. // not really an option for us. :( We just recompute the offsets.
  298. var isInsertedNode = function(n) { return n == startNode || n == endNode; };
  299. if (sNode.nodeType == goog.dom.NodeType.TEXT) {
  300. while (sOffset > sNode.length) {
  301. sOffset -= sNode.length;
  302. do {
  303. sNode = sNode.nextSibling;
  304. } while (isInsertedNode(sNode));
  305. }
  306. }
  307. if (eNode.nodeType == goog.dom.NodeType.TEXT) {
  308. while (eOffset > eNode.length) {
  309. eOffset -= eNode.length;
  310. do {
  311. eNode = eNode.nextSibling;
  312. } while (isInsertedNode(eNode));
  313. }
  314. }
  315. /** @suppress {missingRequire} */
  316. goog.dom.Range
  317. .createFromNodes(
  318. sNode, /** @type {number} */ (sOffset), eNode,
  319. /** @type {number} */ (eOffset))
  320. .select();
  321. }
  322. };
  323. /** @override */
  324. goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {
  325. this.range_.collapse(toStart);
  326. };