Path: blob/trunk/third_party/closure/goog/dom/textrange.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Utilities for working with text ranges in HTML documents.16*17* @author [email protected] (Robby Walker)18*/192021goog.provide('goog.dom.TextRange');2223goog.require('goog.array');24goog.require('goog.dom');25goog.require('goog.dom.AbstractRange');26goog.require('goog.dom.RangeType');27goog.require('goog.dom.SavedRange');28goog.require('goog.dom.TagName');29goog.require('goog.dom.TextRangeIterator');30goog.require('goog.dom.browserrange');31goog.require('goog.string');32goog.require('goog.userAgent');33343536/**37* Create a new text selection with no properties. Do not use this constructor:38* use one of the goog.dom.Range.createFrom* methods instead.39* @constructor40* @extends {goog.dom.AbstractRange}41* @final42*/43goog.dom.TextRange = function() {44/**45* The browser specific range wrapper. This can be null if one of the other46* representations of the range is specified.47* @private {goog.dom.browserrange.AbstractRange?}48*/49this.browserRangeWrapper_ = null;5051/**52* The start node of the range. This can be null if one of the other53* representations of the range is specified.54* @private {Node}55*/56this.startNode_ = null;5758/**59* The start offset of the range. This can be null if one of the other60* representations of the range is specified.61* @private {?number}62*/63this.startOffset_ = null;6465/**66* The end node of the range. This can be null if one of the other67* representations of the range is specified.68* @private {Node}69*/70this.endNode_ = null;7172/**73* The end offset of the range. This can be null if one of the other74* representations of the range is specified.75* @private {?number}76*/77this.endOffset_ = null;7879/**80* Whether the focus node is before the anchor node.81* @private {boolean}82*/83this.isReversed_ = false;84};85goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);868788/**89* Create a new range wrapper from the given browser range object. Do not use90* this method directly - please use goog.dom.Range.createFrom* instead.91* @param {Range|TextRange} range The browser range object.92* @param {boolean=} opt_isReversed Whether the focus node is before the anchor93* node.94* @return {!goog.dom.TextRange} A range wrapper object.95*/96goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {97return goog.dom.TextRange.createFromBrowserRangeWrapper_(98goog.dom.browserrange.createRange(range), opt_isReversed);99};100101102/**103* Create a new range wrapper from the given browser range wrapper.104* @param {goog.dom.browserrange.AbstractRange} browserRange The browser range105* wrapper.106* @param {boolean=} opt_isReversed Whether the focus node is before the anchor107* node.108* @return {!goog.dom.TextRange} A range wrapper object.109* @private110*/111goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(112browserRange, opt_isReversed) {113var range = new goog.dom.TextRange();114115// Initialize the range as a browser range wrapper type range.116range.browserRangeWrapper_ = browserRange;117range.isReversed_ = !!opt_isReversed;118119return range;120};121122123/**124* Create a new range wrapper that selects the given node's text. Do not use125* this method directly - please use goog.dom.Range.createFrom* instead.126* @param {Node} node The node to select.127* @param {boolean=} opt_isReversed Whether the focus node is before the anchor128* node.129* @return {!goog.dom.TextRange} A range wrapper object.130*/131goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {132return goog.dom.TextRange.createFromBrowserRangeWrapper_(133goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed);134};135136137/**138* Create a new range wrapper that selects the area between the given nodes,139* accounting for the given offsets. Do not use this method directly - please140* use goog.dom.Range.createFrom* instead.141* @param {Node} anchorNode The node to start with.142* @param {number} anchorOffset The offset within the node to start.143* @param {Node} focusNode The node to end with.144* @param {number} focusOffset The offset within the node to end.145* @return {!goog.dom.TextRange} A range wrapper object.146*/147goog.dom.TextRange.createFromNodes = function(148anchorNode, anchorOffset, focusNode, focusOffset) {149var range = new goog.dom.TextRange();150range.isReversed_ = /** @suppress {missingRequire} */ (151goog.dom.Range.isReversed(152anchorNode, anchorOffset, focusNode, focusOffset));153154// Avoid selecting terminal elements directly155if (goog.dom.isElement(anchorNode) && !goog.dom.canHaveChildren(anchorNode)) {156var parent = anchorNode.parentNode;157anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);158anchorNode = parent;159}160161if (goog.dom.isElement(focusNode) && !goog.dom.canHaveChildren(focusNode)) {162var parent = focusNode.parentNode;163focusOffset = goog.array.indexOf(parent.childNodes, focusNode);164focusNode = parent;165}166167// Initialize the range as a W3C style range.168if (range.isReversed_) {169range.startNode_ = focusNode;170range.startOffset_ = focusOffset;171range.endNode_ = anchorNode;172range.endOffset_ = anchorOffset;173} else {174range.startNode_ = anchorNode;175range.startOffset_ = anchorOffset;176range.endNode_ = focusNode;177range.endOffset_ = focusOffset;178}179180return range;181};182183184// Method implementations185186187/**188* @return {!goog.dom.TextRange} A clone of this range.189* @override190*/191goog.dom.TextRange.prototype.clone = function() {192var range = new goog.dom.TextRange();193range.browserRangeWrapper_ =194this.browserRangeWrapper_ && this.browserRangeWrapper_.clone();195range.startNode_ = this.startNode_;196range.startOffset_ = this.startOffset_;197range.endNode_ = this.endNode_;198range.endOffset_ = this.endOffset_;199range.isReversed_ = this.isReversed_;200201return range;202};203204205/** @override */206goog.dom.TextRange.prototype.getType = function() {207return goog.dom.RangeType.TEXT;208};209210211/** @override */212goog.dom.TextRange.prototype.getBrowserRangeObject = function() {213return this.getBrowserRangeWrapper_().getBrowserRange();214};215216217/** @override */218goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {219// Test if it's a control range by seeing if a control range only method220// exists.221if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {222return false;223}224this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange);225this.clearCachedValues_();226return true;227};228229230/**231* Clear all cached values.232* @private233*/234goog.dom.TextRange.prototype.clearCachedValues_ = function() {235this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;236};237238239/** @override */240goog.dom.TextRange.prototype.getTextRangeCount = function() {241return 1;242};243244245/** @override */246goog.dom.TextRange.prototype.getTextRange = function(i) {247return this;248};249250251/**252* @return {!goog.dom.browserrange.AbstractRange} The range wrapper object.253* @private254*/255goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {256return this.browserRangeWrapper_ ||257(this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(258this.getStartNode(), this.getStartOffset(), this.getEndNode(),259this.getEndOffset()));260};261262263/** @override */264goog.dom.TextRange.prototype.getContainer = function() {265return this.getBrowserRangeWrapper_().getContainer();266};267268269/** @override */270goog.dom.TextRange.prototype.getStartNode = function() {271return this.startNode_ ||272(this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());273};274275276/** @override */277goog.dom.TextRange.prototype.getStartOffset = function() {278return this.startOffset_ != null ?279this.startOffset_ :280(this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());281};282283284/** @override */285goog.dom.TextRange.prototype.getStartPosition = function() {286return this.getBrowserRangeWrapper_().getStartPosition();287};288289290/** @override */291goog.dom.TextRange.prototype.getEndNode = function() {292return this.endNode_ ||293(this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());294};295296297/** @override */298goog.dom.TextRange.prototype.getEndOffset = function() {299return this.endOffset_ != null ?300this.endOffset_ :301(this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());302};303304305/** @override */306goog.dom.TextRange.prototype.getEndPosition = function() {307return this.getBrowserRangeWrapper_().getEndPosition();308};309310311/**312* Moves a TextRange to the provided nodes and offsets.313* @param {Node} startNode The node to start with.314* @param {number} startOffset The offset within the node to start.315* @param {Node} endNode The node to end with.316* @param {number} endOffset The offset within the node to end.317* @param {boolean} isReversed Whether the range is reversed.318*/319goog.dom.TextRange.prototype.moveToNodes = function(320startNode, startOffset, endNode, endOffset, isReversed) {321this.startNode_ = startNode;322this.startOffset_ = startOffset;323this.endNode_ = endNode;324this.endOffset_ = endOffset;325this.isReversed_ = isReversed;326this.browserRangeWrapper_ = null;327};328329330/** @override */331goog.dom.TextRange.prototype.isReversed = function() {332return this.isReversed_;333};334335336/** @override */337goog.dom.TextRange.prototype.containsRange = function(338otherRange, opt_allowPartial) {339var otherRangeType = otherRange.getType();340if (otherRangeType == goog.dom.RangeType.TEXT) {341return this.getBrowserRangeWrapper_().containsRange(342otherRange.getBrowserRangeWrapper_(), opt_allowPartial);343} else if (otherRangeType == goog.dom.RangeType.CONTROL) {344var elements = otherRange.getElements();345var fn = opt_allowPartial ? goog.array.some : goog.array.every;346return fn(347elements,348/**349* @this {goog.dom.TextRange}350* @param {!Element} el351* @return {boolean}352*/353function(el) {354return this.containsNode(el, opt_allowPartial);355},356this);357}358return false;359};360361362/** @override */363goog.dom.TextRange.prototype.containsNode = function(node, opt_allowPartial) {364return this.containsRange(365goog.dom.TextRange.createFromNodeContents(node), opt_allowPartial);366};367368369370/**371* Tests if the given node is in a document.372* @param {Node} node The node to check.373* @return {boolean} Whether the given node is in the given document.374*/375goog.dom.TextRange.isAttachedNode = function(node) {376if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {377var returnValue = false;378379try {380returnValue = node.parentNode;381} catch (e) {382// IE sometimes throws Invalid Argument errors when a node is detached.383// Note: trying to return a value from the above try block can cause IE384// to crash. It is necessary to use the local returnValue385}386return !!returnValue;387} else {388return goog.dom.contains(node.ownerDocument.body, node);389}390};391392393/** @override */394goog.dom.TextRange.prototype.isRangeInDocument = function() {395// Ensure any cached nodes are in the document. IE also allows ranges to396// become detached, so we check if the range is still in the document as397// well for IE.398return (!this.startNode_ ||399goog.dom.TextRange.isAttachedNode(this.startNode_)) &&400(!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) &&401(!(goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||402this.getBrowserRangeWrapper_().isRangeInDocument());403};404405406/** @override */407goog.dom.TextRange.prototype.isCollapsed = function() {408return this.getBrowserRangeWrapper_().isCollapsed();409};410411412/** @override */413goog.dom.TextRange.prototype.getText = function() {414return this.getBrowserRangeWrapper_().getText();415};416417418/** @override */419goog.dom.TextRange.prototype.getHtmlFragment = function() {420// TODO(robbyw): Generalize the code in browserrange so it is static and421// just takes an iterator. This would mean we don't always have to create a422// browser range.423return this.getBrowserRangeWrapper_().getHtmlFragment();424};425426427/** @override */428goog.dom.TextRange.prototype.getValidHtml = function() {429return this.getBrowserRangeWrapper_().getValidHtml();430};431432433/** @override */434goog.dom.TextRange.prototype.getPastableHtml = function() {435// TODO(robbyw): Get any attributes the table or tr has.436437var html = this.getValidHtml();438439if (html.match(/^\s*<td\b/i)) {440// Match html starting with a TD.441html = '<table><tbody><tr>' + html + '</tr></tbody></table>';442} else if (html.match(/^\s*<tr\b/i)) {443// Match html starting with a TR.444html = '<table><tbody>' + html + '</tbody></table>';445} else if (html.match(/^\s*<tbody\b/i)) {446// Match html starting with a TBODY.447html = '<table>' + html + '</table>';448} else if (html.match(/^\s*<li\b/i)) {449// Match html starting with an LI.450var container = /** @type {!Element} */ (this.getContainer());451var tagType = goog.dom.TagName.UL;452while (container) {453if (container.tagName == goog.dom.TagName.OL) {454tagType = goog.dom.TagName.OL;455break;456} else if (container.tagName == goog.dom.TagName.UL) {457break;458}459container = container.parentNode;460}461html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');462}463464return html;465};466467468/**469* Returns a TextRangeIterator over the contents of the range. Regardless of470* the direction of the range, the iterator will move in document order.471* @param {boolean=} opt_keys Unused for this iterator.472* @return {!goog.dom.TextRangeIterator} An iterator over tags in the range.473* @override474*/475goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {476return new goog.dom.TextRangeIterator(477this.getStartNode(), this.getStartOffset(), this.getEndNode(),478this.getEndOffset());479};480481482// RANGE ACTIONS483484485/** @override */486goog.dom.TextRange.prototype.select = function() {487this.getBrowserRangeWrapper_().select(this.isReversed_);488};489490491/** @override */492goog.dom.TextRange.prototype.removeContents = function() {493this.getBrowserRangeWrapper_().removeContents();494this.clearCachedValues_();495};496497498/**499* Surrounds the text range with the specified element (on Mozilla) or with a500* clone of the specified element (on IE). Returns a reference to the501* surrounding element if the operation was successful; returns null if the502* operation failed.503* @param {Element} element The element with which the selection is to be504* surrounded.505* @return {Element} The surrounding element (same as the argument on Mozilla,506* but not on IE), or null if unsuccessful.507*/508goog.dom.TextRange.prototype.surroundContents = function(element) {509var output = this.getBrowserRangeWrapper_().surroundContents(element);510this.clearCachedValues_();511return output;512};513514515/** @override */516goog.dom.TextRange.prototype.insertNode = function(node, before) {517var output = this.getBrowserRangeWrapper_().insertNode(node, before);518this.clearCachedValues_();519return output;520};521522523/** @override */524goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {525this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);526this.clearCachedValues_();527};528529530// SAVE/RESTORE531532533/** @override */534goog.dom.TextRange.prototype.saveUsingDom = function() {535return new goog.dom.DomSavedTextRange_(this);536};537538539// RANGE MODIFICATION540541542/** @override */543goog.dom.TextRange.prototype.collapse = function(toAnchor) {544var toStart = this.isReversed() ? !toAnchor : toAnchor;545546if (this.browserRangeWrapper_) {547this.browserRangeWrapper_.collapse(toStart);548}549550if (toStart) {551this.endNode_ = this.startNode_;552this.endOffset_ = this.startOffset_;553} else {554this.startNode_ = this.endNode_;555this.startOffset_ = this.endOffset_;556}557558// Collapsed ranges can't be reversed559this.isReversed_ = false;560};561562563// SAVED RANGE OBJECTS564565566567/**568* A SavedRange implementation using DOM endpoints.569* @param {goog.dom.AbstractRange} range The range to save.570* @constructor571* @extends {goog.dom.SavedRange}572* @private573*/574goog.dom.DomSavedTextRange_ = function(range) {575goog.dom.DomSavedTextRange_.base(this, 'constructor');576577/**578* The anchor node.579* @type {Node}580* @private581*/582this.anchorNode_ = range.getAnchorNode();583584/**585* The anchor node offset.586* @type {number}587* @private588*/589this.anchorOffset_ = range.getAnchorOffset();590591/**592* The focus node.593* @type {Node}594* @private595*/596this.focusNode_ = range.getFocusNode();597598/**599* The focus node offset.600* @type {number}601* @private602*/603this.focusOffset_ = range.getFocusOffset();604};605goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);606607608/**609* @return {!goog.dom.AbstractRange} The restored range.610* @override611*/612goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {613return /** @suppress {missingRequire} */ (614goog.dom.Range.createFromNodes(615this.anchorNode_, this.anchorOffset_, this.focusNode_,616this.focusOffset_));617};618619620/** @override */621goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {622goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);623624this.anchorNode_ = null;625this.focusNode_ = null;626};627628629