controlrange.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. // Copyright 2008 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 IE control ranges.
  16. *
  17. * @author robbyw@google.com (Robby Walker)
  18. */
  19. goog.provide('goog.dom.ControlRange');
  20. goog.provide('goog.dom.ControlRangeIterator');
  21. goog.require('goog.array');
  22. goog.require('goog.dom');
  23. goog.require('goog.dom.AbstractMultiRange');
  24. goog.require('goog.dom.AbstractRange');
  25. goog.require('goog.dom.RangeIterator');
  26. goog.require('goog.dom.RangeType');
  27. goog.require('goog.dom.SavedRange');
  28. goog.require('goog.dom.TagWalkType');
  29. goog.require('goog.dom.TextRange');
  30. goog.require('goog.iter.StopIteration');
  31. goog.require('goog.userAgent');
  32. /**
  33. * Create a new control selection with no properties. Do not use this
  34. * constructor: use one of the goog.dom.Range.createFrom* methods instead.
  35. * @constructor
  36. * @extends {goog.dom.AbstractMultiRange}
  37. * @final
  38. */
  39. goog.dom.ControlRange = function() {
  40. /**
  41. * The IE control range obejct.
  42. * @private {Object}
  43. */
  44. this.range_ = null;
  45. /**
  46. * Cached list of elements.
  47. * @private {Array<Element>}
  48. */
  49. this.elements_ = null;
  50. /**
  51. * Cached sorted list of elements.
  52. * @private {Array<Element>}
  53. */
  54. this.sortedElements_ = null;
  55. };
  56. goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange);
  57. /**
  58. * Create a new range wrapper from the given browser range object. Do not use
  59. * this method directly - please use goog.dom.Range.createFrom* instead.
  60. * @param {Object} controlRange The browser range object.
  61. * @return {!goog.dom.ControlRange} A range wrapper object.
  62. */
  63. goog.dom.ControlRange.createFromBrowserRange = function(controlRange) {
  64. var range = new goog.dom.ControlRange();
  65. range.range_ = controlRange;
  66. return range;
  67. };
  68. /**
  69. * Create a new range wrapper that selects the given element. Do not use
  70. * this method directly - please use goog.dom.Range.createFrom* instead.
  71. * @param {...Element} var_args The element(s) to select.
  72. * @return {!goog.dom.ControlRange} A range wrapper object.
  73. */
  74. goog.dom.ControlRange.createFromElements = function(var_args) {
  75. var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange();
  76. for (var i = 0, len = arguments.length; i < len; i++) {
  77. range.addElement(arguments[i]);
  78. }
  79. return goog.dom.ControlRange.createFromBrowserRange(range);
  80. };
  81. // Method implementations
  82. /**
  83. * Clear cached values.
  84. * @private
  85. */
  86. goog.dom.ControlRange.prototype.clearCachedValues_ = function() {
  87. this.elements_ = null;
  88. this.sortedElements_ = null;
  89. };
  90. /** @override */
  91. goog.dom.ControlRange.prototype.clone = function() {
  92. return goog.dom.ControlRange.createFromElements.apply(
  93. this, this.getElements());
  94. };
  95. /** @override */
  96. goog.dom.ControlRange.prototype.getType = function() {
  97. return goog.dom.RangeType.CONTROL;
  98. };
  99. /** @override */
  100. goog.dom.ControlRange.prototype.getBrowserRangeObject = function() {
  101. return this.range_ || document.body.createControlRange();
  102. };
  103. /** @override */
  104. goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) {
  105. if (!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
  106. return false;
  107. }
  108. this.range_ = nativeRange;
  109. return true;
  110. };
  111. /** @override */
  112. goog.dom.ControlRange.prototype.getTextRangeCount = function() {
  113. return this.range_ ? this.range_.length : 0;
  114. };
  115. /** @override */
  116. goog.dom.ControlRange.prototype.getTextRange = function(i) {
  117. return goog.dom.TextRange.createFromNodeContents(this.range_.item(i));
  118. };
  119. /** @override */
  120. goog.dom.ControlRange.prototype.getContainer = function() {
  121. return goog.dom.findCommonAncestor.apply(null, this.getElements());
  122. };
  123. /** @override */
  124. goog.dom.ControlRange.prototype.getStartNode = function() {
  125. return this.getSortedElements()[0];
  126. };
  127. /** @override */
  128. goog.dom.ControlRange.prototype.getStartOffset = function() {
  129. return 0;
  130. };
  131. /** @override */
  132. goog.dom.ControlRange.prototype.getEndNode = function() {
  133. var sorted = this.getSortedElements();
  134. var startsLast = /** @type {Node} */ (goog.array.peek(sorted));
  135. return /** @type {Node} */ (goog.array.find(sorted, function(el) {
  136. return goog.dom.contains(el, startsLast);
  137. }));
  138. };
  139. /** @override */
  140. goog.dom.ControlRange.prototype.getEndOffset = function() {
  141. return this.getEndNode().childNodes.length;
  142. };
  143. // TODO(robbyw): Figure out how to unify getElements with TextRange API.
  144. /**
  145. * @return {!Array<Element>} Array of elements in the control range.
  146. */
  147. goog.dom.ControlRange.prototype.getElements = function() {
  148. if (!this.elements_) {
  149. this.elements_ = [];
  150. if (this.range_) {
  151. for (var i = 0; i < this.range_.length; i++) {
  152. this.elements_.push(this.range_.item(i));
  153. }
  154. }
  155. }
  156. return this.elements_;
  157. };
  158. /**
  159. * @return {!Array<Element>} Array of elements comprising the control range,
  160. * sorted by document order.
  161. */
  162. goog.dom.ControlRange.prototype.getSortedElements = function() {
  163. if (!this.sortedElements_) {
  164. this.sortedElements_ = this.getElements().concat();
  165. this.sortedElements_.sort(function(a, b) {
  166. return a.sourceIndex - b.sourceIndex;
  167. });
  168. }
  169. return this.sortedElements_;
  170. };
  171. /** @override */
  172. goog.dom.ControlRange.prototype.isRangeInDocument = function() {
  173. var returnValue = false;
  174. try {
  175. returnValue = goog.array.every(this.getElements(), function(element) {
  176. // On IE, this throws an exception when the range is detached.
  177. return goog.userAgent.IE ?
  178. !!element.parentNode :
  179. goog.dom.contains(element.ownerDocument.body, element);
  180. });
  181. } catch (e) {
  182. // IE sometimes throws Invalid Argument errors for detached elements.
  183. // Note: trying to return a value from the above try block can cause IE
  184. // to crash. It is necessary to use the local returnValue.
  185. }
  186. return returnValue;
  187. };
  188. /** @override */
  189. goog.dom.ControlRange.prototype.isCollapsed = function() {
  190. return !this.range_ || !this.range_.length;
  191. };
  192. /** @override */
  193. goog.dom.ControlRange.prototype.getText = function() {
  194. // TODO(robbyw): What about for table selections? Should those have text?
  195. return '';
  196. };
  197. /** @override */
  198. goog.dom.ControlRange.prototype.getHtmlFragment = function() {
  199. return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml)
  200. .join('');
  201. };
  202. /** @override */
  203. goog.dom.ControlRange.prototype.getValidHtml = function() {
  204. return this.getHtmlFragment();
  205. };
  206. /** @override */
  207. goog.dom.ControlRange.prototype.getPastableHtml =
  208. goog.dom.ControlRange.prototype.getValidHtml;
  209. /** @override */
  210. goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) {
  211. return new goog.dom.ControlRangeIterator(this);
  212. };
  213. // RANGE ACTIONS
  214. /** @override */
  215. goog.dom.ControlRange.prototype.select = function() {
  216. if (this.range_) {
  217. this.range_.select();
  218. }
  219. };
  220. /** @override */
  221. goog.dom.ControlRange.prototype.removeContents = function() {
  222. // TODO(robbyw): Test implementing with execCommand('Delete')
  223. if (this.range_) {
  224. var nodes = [];
  225. for (var i = 0, len = this.range_.length; i < len; i++) {
  226. nodes.push(this.range_.item(i));
  227. }
  228. goog.array.forEach(nodes, goog.dom.removeNode);
  229. this.collapse(false);
  230. }
  231. };
  232. /** @override */
  233. goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) {
  234. // Control selections have to have the node inserted before removing the
  235. // selection contents because a collapsed control range doesn't have start or
  236. // end nodes.
  237. var result = this.insertNode(node, true);
  238. if (!this.isCollapsed()) {
  239. this.removeContents();
  240. }
  241. return result;
  242. };
  243. // SAVE/RESTORE
  244. /** @override */
  245. goog.dom.ControlRange.prototype.saveUsingDom = function() {
  246. return new goog.dom.DomSavedControlRange_(this);
  247. };
  248. // RANGE MODIFICATION
  249. /** @override */
  250. goog.dom.ControlRange.prototype.collapse = function(toAnchor) {
  251. // TODO(robbyw): Should this return a text range? If so, API needs to change.
  252. this.range_ = null;
  253. this.clearCachedValues_();
  254. };
  255. // SAVED RANGE OBJECTS
  256. /**
  257. * A SavedRange implementation using DOM endpoints.
  258. * @param {goog.dom.ControlRange} range The range to save.
  259. * @constructor
  260. * @extends {goog.dom.SavedRange}
  261. * @private
  262. */
  263. goog.dom.DomSavedControlRange_ = function(range) {
  264. /**
  265. * The element list.
  266. * @type {Array<Element>}
  267. * @private
  268. */
  269. this.elements_ = range.getElements();
  270. };
  271. goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange);
  272. /** @override */
  273. goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() {
  274. var doc = this.elements_.length ?
  275. goog.dom.getOwnerDocument(this.elements_[0]) :
  276. document;
  277. var controlRange = doc.body.createControlRange();
  278. for (var i = 0, len = this.elements_.length; i < len; i++) {
  279. controlRange.addElement(this.elements_[i]);
  280. }
  281. return goog.dom.ControlRange.createFromBrowserRange(controlRange);
  282. };
  283. /** @override */
  284. goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() {
  285. goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this);
  286. delete this.elements_;
  287. };
  288. // RANGE ITERATION
  289. /**
  290. * Subclass of goog.dom.TagIterator that iterates over a DOM range. It
  291. * adds functions to determine the portion of each text node that is selected.
  292. *
  293. * @param {goog.dom.ControlRange?} range The range to traverse.
  294. * @constructor
  295. * @extends {goog.dom.RangeIterator}
  296. * @final
  297. */
  298. goog.dom.ControlRangeIterator = function(range) {
  299. /**
  300. * The first node in the selection.
  301. * @private {Node}
  302. */
  303. this.startNode_ = null;
  304. /**
  305. * The last node in the selection.
  306. * @private {Node}
  307. */
  308. this.endNode_ = null;
  309. /**
  310. * The list of elements left to traverse.
  311. * @private {Array<Element>?}
  312. */
  313. this.elements_ = null;
  314. if (range) {
  315. this.elements_ = range.getSortedElements();
  316. this.startNode_ = this.elements_.shift();
  317. this.endNode_ = /** @type {Node} */ (goog.array.peek(this.elements_)) ||
  318. this.startNode_;
  319. }
  320. goog.dom.ControlRangeIterator.base(
  321. this, 'constructor', this.startNode_, false);
  322. };
  323. goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator);
  324. /** @override */
  325. goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() {
  326. return 0;
  327. };
  328. /** @override */
  329. goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() {
  330. return 0;
  331. };
  332. /** @override */
  333. goog.dom.ControlRangeIterator.prototype.getStartNode = function() {
  334. return this.startNode_;
  335. };
  336. /** @override */
  337. goog.dom.ControlRangeIterator.prototype.getEndNode = function() {
  338. return this.endNode_;
  339. };
  340. /** @override */
  341. goog.dom.ControlRangeIterator.prototype.isLast = function() {
  342. return !this.depth && !this.elements_.length;
  343. };
  344. /**
  345. * Move to the next position in the selection.
  346. * Throws {@code goog.iter.StopIteration} when it passes the end of the range.
  347. * @return {Node} The node at the next position.
  348. * @override
  349. */
  350. goog.dom.ControlRangeIterator.prototype.next = function() {
  351. // Iterate over each element in the range, and all of its children.
  352. if (this.isLast()) {
  353. throw goog.iter.StopIteration;
  354. } else if (!this.depth) {
  355. var el = this.elements_.shift();
  356. this.setPosition(
  357. el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG);
  358. return el;
  359. }
  360. // Call the super function.
  361. return goog.dom.ControlRangeIterator.superClass_.next.call(this);
  362. };
  363. /** @override */
  364. goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) {
  365. this.elements_ = other.elements_;
  366. this.startNode_ = other.startNode_;
  367. this.endNode_ = other.endNode_;
  368. goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other);
  369. };
  370. /**
  371. * @return {!goog.dom.ControlRangeIterator} An identical iterator.
  372. * @override
  373. */
  374. goog.dom.ControlRangeIterator.prototype.clone = function() {
  375. var copy = new goog.dom.ControlRangeIterator(null);
  376. copy.copyFrom(this);
  377. return copy;
  378. };