| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview Utilities for working with IE control ranges. |
| * |
| * @author robbyw@google.com (Robby Walker) |
| */ |
| |
| |
| goog.provide('goog.dom.ControlRange'); |
| goog.provide('goog.dom.ControlRangeIterator'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.AbstractMultiRange'); |
| goog.require('goog.dom.AbstractRange'); |
| goog.require('goog.dom.RangeIterator'); |
| goog.require('goog.dom.RangeType'); |
| goog.require('goog.dom.SavedRange'); |
| goog.require('goog.dom.TagWalkType'); |
| goog.require('goog.dom.TextRange'); |
| goog.require('goog.iter.StopIteration'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * Create a new control selection with no properties. Do not use this |
| * constructor: use one of the goog.dom.Range.createFrom* methods instead. |
| * @constructor |
| * @extends {goog.dom.AbstractMultiRange} |
| * @final |
| */ |
| goog.dom.ControlRange = function() { |
| }; |
| goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange); |
| |
| |
| /** |
| * Create a new range wrapper from the given browser range object. Do not use |
| * this method directly - please use goog.dom.Range.createFrom* instead. |
| * @param {Object} controlRange The browser range object. |
| * @return {!goog.dom.ControlRange} A range wrapper object. |
| */ |
| goog.dom.ControlRange.createFromBrowserRange = function(controlRange) { |
| var range = new goog.dom.ControlRange(); |
| range.range_ = controlRange; |
| return range; |
| }; |
| |
| |
| /** |
| * Create a new range wrapper that selects the given element. Do not use |
| * this method directly - please use goog.dom.Range.createFrom* instead. |
| * @param {...Element} var_args The element(s) to select. |
| * @return {!goog.dom.ControlRange} A range wrapper object. |
| */ |
| goog.dom.ControlRange.createFromElements = function(var_args) { |
| var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange(); |
| for (var i = 0, len = arguments.length; i < len; i++) { |
| range.addElement(arguments[i]); |
| } |
| return goog.dom.ControlRange.createFromBrowserRange(range); |
| }; |
| |
| |
| /** |
| * The IE control range obejct. |
| * @type {Object} |
| * @private |
| */ |
| goog.dom.ControlRange.prototype.range_ = null; |
| |
| |
| /** |
| * Cached list of elements. |
| * @type {Array<Element>?} |
| * @private |
| */ |
| goog.dom.ControlRange.prototype.elements_ = null; |
| |
| |
| /** |
| * Cached sorted list of elements. |
| * @type {Array<Element>?} |
| * @private |
| */ |
| goog.dom.ControlRange.prototype.sortedElements_ = null; |
| |
| |
| // Method implementations |
| |
| |
| /** |
| * Clear cached values. |
| * @private |
| */ |
| goog.dom.ControlRange.prototype.clearCachedValues_ = function() { |
| this.elements_ = null; |
| this.sortedElements_ = null; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.clone = function() { |
| return goog.dom.ControlRange.createFromElements.apply(this, |
| this.getElements()); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getType = function() { |
| return goog.dom.RangeType.CONTROL; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getBrowserRangeObject = function() { |
| return this.range_ || document.body.createControlRange(); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) { |
| if (!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) { |
| return false; |
| } |
| this.range_ = nativeRange; |
| return true; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getTextRangeCount = function() { |
| return this.range_ ? this.range_.length : 0; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getTextRange = function(i) { |
| return goog.dom.TextRange.createFromNodeContents(this.range_.item(i)); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getContainer = function() { |
| return goog.dom.findCommonAncestor.apply(null, this.getElements()); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getStartNode = function() { |
| return this.getSortedElements()[0]; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getStartOffset = function() { |
| return 0; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getEndNode = function() { |
| var sorted = this.getSortedElements(); |
| var startsLast = /** @type {Node} */ (goog.array.peek(sorted)); |
| return /** @type {Node} */ (goog.array.find(sorted, function(el) { |
| return goog.dom.contains(el, startsLast); |
| })); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getEndOffset = function() { |
| return this.getEndNode().childNodes.length; |
| }; |
| |
| |
| // TODO(robbyw): Figure out how to unify getElements with TextRange API. |
| /** |
| * @return {!Array<Element>} Array of elements in the control range. |
| */ |
| goog.dom.ControlRange.prototype.getElements = function() { |
| if (!this.elements_) { |
| this.elements_ = []; |
| if (this.range_) { |
| for (var i = 0; i < this.range_.length; i++) { |
| this.elements_.push(this.range_.item(i)); |
| } |
| } |
| } |
| |
| return this.elements_; |
| }; |
| |
| |
| /** |
| * @return {!Array<Element>} Array of elements comprising the control range, |
| * sorted by document order. |
| */ |
| goog.dom.ControlRange.prototype.getSortedElements = function() { |
| if (!this.sortedElements_) { |
| this.sortedElements_ = this.getElements().concat(); |
| this.sortedElements_.sort(function(a, b) { |
| return a.sourceIndex - b.sourceIndex; |
| }); |
| } |
| |
| return this.sortedElements_; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.isRangeInDocument = function() { |
| var returnValue = false; |
| |
| try { |
| returnValue = goog.array.every(this.getElements(), function(element) { |
| // On IE, this throws an exception when the range is detached. |
| return goog.userAgent.IE ? |
| !!element.parentNode : |
| goog.dom.contains(element.ownerDocument.body, element); |
| }); |
| } catch (e) { |
| // IE sometimes throws Invalid Argument errors for detached elements. |
| // Note: trying to return a value from the above try block can cause IE |
| // to crash. It is necessary to use the local returnValue. |
| } |
| |
| return returnValue; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.isCollapsed = function() { |
| return !this.range_ || !this.range_.length; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getText = function() { |
| // TODO(robbyw): What about for table selections? Should those have text? |
| return ''; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getHtmlFragment = function() { |
| return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml). |
| join(''); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getValidHtml = function() { |
| return this.getHtmlFragment(); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.getPastableHtml = |
| goog.dom.ControlRange.prototype.getValidHtml; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) { |
| return new goog.dom.ControlRangeIterator(this); |
| }; |
| |
| |
| // RANGE ACTIONS |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.select = function() { |
| if (this.range_) { |
| this.range_.select(); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.removeContents = function() { |
| // TODO(robbyw): Test implementing with execCommand('Delete') |
| if (this.range_) { |
| var nodes = []; |
| for (var i = 0, len = this.range_.length; i < len; i++) { |
| nodes.push(this.range_.item(i)); |
| } |
| goog.array.forEach(nodes, goog.dom.removeNode); |
| |
| this.collapse(false); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) { |
| // Control selections have to have the node inserted before removing the |
| // selection contents because a collapsed control range doesn't have start or |
| // end nodes. |
| var result = this.insertNode(node, true); |
| |
| if (!this.isCollapsed()) { |
| this.removeContents(); |
| } |
| |
| return result; |
| }; |
| |
| |
| // SAVE/RESTORE |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.saveUsingDom = function() { |
| return new goog.dom.DomSavedControlRange_(this); |
| }; |
| |
| |
| // RANGE MODIFICATION |
| |
| |
| /** @override */ |
| goog.dom.ControlRange.prototype.collapse = function(toAnchor) { |
| // TODO(robbyw): Should this return a text range? If so, API needs to change. |
| this.range_ = null; |
| this.clearCachedValues_(); |
| }; |
| |
| |
| // SAVED RANGE OBJECTS |
| |
| |
| |
| /** |
| * A SavedRange implementation using DOM endpoints. |
| * @param {goog.dom.ControlRange} range The range to save. |
| * @constructor |
| * @extends {goog.dom.SavedRange} |
| * @private |
| */ |
| goog.dom.DomSavedControlRange_ = function(range) { |
| /** |
| * The element list. |
| * @type {Array<Element>} |
| * @private |
| */ |
| this.elements_ = range.getElements(); |
| }; |
| goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange); |
| |
| |
| /** @override */ |
| goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() { |
| var doc = this.elements_.length ? |
| goog.dom.getOwnerDocument(this.elements_[0]) : document; |
| var controlRange = doc.body.createControlRange(); |
| for (var i = 0, len = this.elements_.length; i < len; i++) { |
| controlRange.addElement(this.elements_[i]); |
| } |
| return goog.dom.ControlRange.createFromBrowserRange(controlRange); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() { |
| goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this); |
| delete this.elements_; |
| }; |
| |
| |
| // RANGE ITERATION |
| |
| |
| |
| /** |
| * Subclass of goog.dom.TagIterator that iterates over a DOM range. It |
| * adds functions to determine the portion of each text node that is selected. |
| * |
| * @param {goog.dom.ControlRange?} range The range to traverse. |
| * @constructor |
| * @extends {goog.dom.RangeIterator} |
| * @final |
| */ |
| goog.dom.ControlRangeIterator = function(range) { |
| if (range) { |
| this.elements_ = range.getSortedElements(); |
| this.startNode_ = this.elements_.shift(); |
| this.endNode_ = /** @type {Node} */ (goog.array.peek(this.elements_)) || |
| this.startNode_; |
| } |
| |
| goog.dom.RangeIterator.call(this, this.startNode_, false); |
| }; |
| goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator); |
| |
| |
| /** |
| * The first node in the selection. |
| * @type {Node} |
| * @private |
| */ |
| goog.dom.ControlRangeIterator.prototype.startNode_ = null; |
| |
| |
| /** |
| * The last node in the selection. |
| * @type {Node} |
| * @private |
| */ |
| goog.dom.ControlRangeIterator.prototype.endNode_ = null; |
| |
| |
| /** |
| * The list of elements left to traverse. |
| * @type {Array<Element>?} |
| * @private |
| */ |
| goog.dom.ControlRangeIterator.prototype.elements_ = null; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() { |
| return 0; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() { |
| return 0; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.getStartNode = function() { |
| return this.startNode_; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.getEndNode = function() { |
| return this.endNode_; |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.isLast = function() { |
| return !this.depth && !this.elements_.length; |
| }; |
| |
| |
| /** |
| * Move to the next position in the selection. |
| * Throws {@code goog.iter.StopIteration} when it passes the end of the range. |
| * @return {Node} The node at the next position. |
| * @override |
| */ |
| goog.dom.ControlRangeIterator.prototype.next = function() { |
| // Iterate over each element in the range, and all of its children. |
| if (this.isLast()) { |
| throw goog.iter.StopIteration; |
| } else if (!this.depth) { |
| var el = this.elements_.shift(); |
| this.setPosition(el, |
| goog.dom.TagWalkType.START_TAG, |
| goog.dom.TagWalkType.START_TAG); |
| return el; |
| } |
| |
| // Call the super function. |
| return goog.dom.ControlRangeIterator.superClass_.next.call(this); |
| }; |
| |
| |
| /** @override */ |
| goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) { |
| this.elements_ = other.elements_; |
| this.startNode_ = other.startNode_; |
| this.endNode_ = other.endNode_; |
| |
| goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other); |
| }; |
| |
| |
| /** |
| * @return {!goog.dom.ControlRangeIterator} An identical iterator. |
| * @override |
| */ |
| goog.dom.ControlRangeIterator.prototype.clone = function() { |
| var copy = new goog.dom.ControlRangeIterator(null); |
| copy.copyFrom(this); |
| return copy; |
| }; |