// Copyright 2007 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 Iterator between two DOM text range positions. * * @author robbyw@google.com (Robby Walker) */ goog.provide('goog.dom.TextRangeIterator'); goog.require('goog.array'); goog.require('goog.dom.NodeType'); goog.require('goog.dom.RangeIterator'); goog.require('goog.dom.TagName'); goog.require('goog.iter.StopIteration'); /** * 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 {Node} startNode The starting node position. * @param {number} startOffset The offset in to startNode. If startNode is * an element, indicates an offset in to childNodes. If startNode is a * text node, indicates an offset in to nodeValue. * @param {Node} endNode The ending node position. * @param {number} endOffset The offset in to endNode. If endNode is * an element, indicates an offset in to childNodes. If endNode is a * text node, indicates an offset in to nodeValue. * @param {boolean=} opt_reverse Whether to traverse nodes in reverse. * @constructor * @extends {goog.dom.RangeIterator} */ goog.dom.TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) { var goNext; if (startNode) { this.startNode_ = startNode; this.startOffset_ = startOffset; this.endNode_ = endNode; this.endOffset_ = endOffset; // Skip to the offset nodes - being careful to special case BRs since these // have no children but still can appear as the startContainer of a range. if (startNode.nodeType == goog.dom.NodeType.ELEMENT && startNode.tagName != goog.dom.TagName.BR) { var startChildren = startNode.childNodes; var candidate = startChildren[startOffset]; if (candidate) { this.startNode_ = candidate; this.startOffset_ = 0; } else { if (startChildren.length) { this.startNode_ = /** @type {Node} */ (goog.array.peek(startChildren)); } goNext = true; } } if (endNode.nodeType == goog.dom.NodeType.ELEMENT) { this.endNode_ = endNode.childNodes[endOffset]; if (this.endNode_) { this.endOffset_ = 0; } else { // The offset was past the last element. this.endNode_ = endNode; } } } goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse); if (goNext) { try { this.next(); } catch (e) { if (e != goog.iter.StopIteration) { throw e; } } } }; goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator); /** * The first node in the selection. * @type {Node} * @private */ goog.dom.TextRangeIterator.prototype.startNode_ = null; /** * The last node in the selection. * @type {Node} * @private */ goog.dom.TextRangeIterator.prototype.endNode_ = null; /** * The offset within the first node in the selection. * @type {number} * @private */ goog.dom.TextRangeIterator.prototype.startOffset_ = 0; /** * The offset within the last node in the selection. * @type {number} * @private */ goog.dom.TextRangeIterator.prototype.endOffset_ = 0; /** @override */ goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() { // Offsets only apply to text nodes. If our current node is the start node, // return the saved offset. Otherwise, return 0. return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.startNode_ ? this.startOffset_ : 0; }; /** @override */ goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() { // Offsets only apply to text nodes. If our current node is the end node, // return the saved offset. Otherwise, return the length of the node. return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length; }; /** @override */ goog.dom.TextRangeIterator.prototype.getStartNode = function() { return this.startNode_; }; /** * Change the start node of the iterator. * @param {Node} node The new start node. */ goog.dom.TextRangeIterator.prototype.setStartNode = function(node) { if (!this.isStarted()) { this.setPosition(node); } this.startNode_ = node; this.startOffset_ = 0; }; /** @override */ goog.dom.TextRangeIterator.prototype.getEndNode = function() { return this.endNode_; }; /** * Change the end node of the iterator. * @param {Node} node The new end node. */ goog.dom.TextRangeIterator.prototype.setEndNode = function(node) { this.endNode_ = node; this.endOffset_ = 0; }; /** @override */ goog.dom.TextRangeIterator.prototype.isLast = function() { return this.isStarted() && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag()); }; /** * 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.TextRangeIterator.prototype.next = function() { if (this.isLast()) { throw goog.iter.StopIteration; } // Call the super function. return goog.dom.TextRangeIterator.superClass_.next.call(this); }; /** @override */ goog.dom.TextRangeIterator.prototype.skipTag = function() { goog.dom.TextRangeIterator.superClass_.skipTag.apply(this); // If the node we are skipping contains the end node, we just skipped past // the end, so we stop the iteration. if (goog.dom.contains(this.node, this.endNode_)) { throw goog.iter.StopIteration; } }; /** @override */ goog.dom.TextRangeIterator.prototype.copyFrom = function(other) { this.startNode_ = other.startNode_; this.endNode_ = other.endNode_; this.startOffset_ = other.startOffset_; this.endOffset_ = other.endOffset_; this.isReversed_ = other.isReversed_; goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other); }; /** * @return {goog.dom.TextRangeIterator} An identical iterator. * @override */ goog.dom.TextRangeIterator.prototype.clone = function() { var copy = new goog.dom.TextRangeIterator(this.startNode_, this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_); copy.copyFrom(this); return copy; };