631 lines
18 KiB
JavaScript
631 lines
18 KiB
JavaScript
// 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 Utilities for working with text ranges in HTML documents.
|
|
*
|
|
* @author robbyw@google.com (Robby Walker)
|
|
* @author ojan@google.com (Ojan Vafai)
|
|
* @author jparent@google.com (Julie Parent)
|
|
*/
|
|
|
|
|
|
goog.provide('goog.dom.TextRange');
|
|
|
|
goog.require('goog.array');
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.AbstractRange');
|
|
goog.require('goog.dom.RangeType');
|
|
goog.require('goog.dom.SavedRange');
|
|
goog.require('goog.dom.TagName');
|
|
goog.require('goog.dom.TextRangeIterator');
|
|
goog.require('goog.dom.browserrange');
|
|
goog.require('goog.string');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
/**
|
|
* Create a new text selection with no properties. Do not use this constructor:
|
|
* use one of the goog.dom.Range.createFrom* methods instead.
|
|
* @constructor
|
|
* @extends {goog.dom.AbstractRange}
|
|
*/
|
|
goog.dom.TextRange = function() {
|
|
};
|
|
goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
|
|
|
|
|
|
/**
|
|
* 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 {Range|TextRange} range The browser range object.
|
|
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
|
* node.
|
|
* @return {goog.dom.TextRange} A range wrapper object.
|
|
*/
|
|
goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
|
|
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
|
|
goog.dom.browserrange.createRange(range), opt_isReversed);
|
|
};
|
|
|
|
|
|
/**
|
|
* Create a new range wrapper from the given browser range wrapper.
|
|
* @param {goog.dom.browserrange.AbstractRange} browserRange The browser range
|
|
* wrapper.
|
|
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
|
* node.
|
|
* @return {goog.dom.TextRange} A range wrapper object.
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange,
|
|
opt_isReversed) {
|
|
var range = new goog.dom.TextRange();
|
|
|
|
// Initialize the range as a browser range wrapper type range.
|
|
range.browserRangeWrapper_ = browserRange;
|
|
range.isReversed_ = !!opt_isReversed;
|
|
|
|
return range;
|
|
};
|
|
|
|
|
|
/**
|
|
* Create a new range wrapper that selects the given node's text. Do not use
|
|
* this method directly - please use goog.dom.Range.createFrom* instead.
|
|
* @param {Node} node The node to select.
|
|
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
|
* node.
|
|
* @return {goog.dom.TextRange} A range wrapper object.
|
|
*/
|
|
goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
|
|
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
|
|
goog.dom.browserrange.createRangeFromNodeContents(node),
|
|
opt_isReversed);
|
|
};
|
|
|
|
|
|
/**
|
|
* Create a new range wrapper that selects the area between the given nodes,
|
|
* accounting for the given offsets. Do not use this method directly - please
|
|
* use goog.dom.Range.createFrom* instead.
|
|
* @param {Node} anchorNode The node to start with.
|
|
* @param {number} anchorOffset The offset within the node to start.
|
|
* @param {Node} focusNode The node to end with.
|
|
* @param {number} focusOffset The offset within the node to end.
|
|
* @return {goog.dom.TextRange} A range wrapper object.
|
|
*/
|
|
goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset,
|
|
focusNode, focusOffset) {
|
|
var range = new goog.dom.TextRange();
|
|
range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset,
|
|
focusNode, focusOffset);
|
|
|
|
// Avoid selecting terminal elements directly
|
|
if (goog.dom.isElement(anchorNode) && !goog.dom.canHaveChildren(anchorNode)) {
|
|
var parent = anchorNode.parentNode;
|
|
anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
|
|
anchorNode = parent;
|
|
}
|
|
|
|
if (goog.dom.isElement(focusNode) && !goog.dom.canHaveChildren(focusNode)) {
|
|
var parent = focusNode.parentNode;
|
|
focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
|
|
focusNode = parent;
|
|
}
|
|
|
|
// Initialize the range as a W3C style range.
|
|
if (range.isReversed_) {
|
|
range.startNode_ = focusNode;
|
|
range.startOffset_ = focusOffset;
|
|
range.endNode_ = anchorNode;
|
|
range.endOffset_ = anchorOffset;
|
|
} else {
|
|
range.startNode_ = anchorNode;
|
|
range.startOffset_ = anchorOffset;
|
|
range.endNode_ = focusNode;
|
|
range.endOffset_ = focusOffset;
|
|
}
|
|
|
|
return range;
|
|
};
|
|
|
|
|
|
// Representation 1: a browser range wrapper.
|
|
|
|
|
|
/**
|
|
* The browser specific range wrapper. This can be null if one of the other
|
|
* representations of the range is specified.
|
|
* @type {goog.dom.browserrange.AbstractRange?}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.browserRangeWrapper_ = null;
|
|
|
|
|
|
// Representation 2: two endpoints specified as nodes + offsets
|
|
|
|
|
|
/**
|
|
* The start node of the range. This can be null if one of the other
|
|
* representations of the range is specified.
|
|
* @type {Node}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.startNode_ = null;
|
|
|
|
|
|
/**
|
|
* The start offset of the range. This can be null if one of the other
|
|
* representations of the range is specified.
|
|
* @type {?number}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.startOffset_ = null;
|
|
|
|
|
|
/**
|
|
* The end node of the range. This can be null if one of the other
|
|
* representations of the range is specified.
|
|
* @type {Node}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.endNode_ = null;
|
|
|
|
|
|
/**
|
|
* The end offset of the range. This can be null if one of the other
|
|
* representations of the range is specified.
|
|
* @type {?number}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.endOffset_ = null;
|
|
|
|
|
|
/**
|
|
* Whether the focus node is before the anchor node.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.isReversed_ = false;
|
|
|
|
|
|
// Method implementations
|
|
|
|
|
|
/**
|
|
* @return {goog.dom.TextRange} A clone of this range.
|
|
* @override
|
|
*/
|
|
goog.dom.TextRange.prototype.clone = function() {
|
|
var range = new goog.dom.TextRange();
|
|
range.browserRangeWrapper_ = this.browserRangeWrapper_;
|
|
range.startNode_ = this.startNode_;
|
|
range.startOffset_ = this.startOffset_;
|
|
range.endNode_ = this.endNode_;
|
|
range.endOffset_ = this.endOffset_;
|
|
range.isReversed_ = this.isReversed_;
|
|
|
|
return range;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getType = function() {
|
|
return goog.dom.RangeType.TEXT;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
|
|
return this.getBrowserRangeWrapper_().getBrowserRange();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
|
|
// Test if it's a control range by seeing if a control range only method
|
|
// exists.
|
|
if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
|
|
return false;
|
|
}
|
|
this.browserRangeWrapper_ = goog.dom.browserrange.createRange(
|
|
nativeRange);
|
|
this.clearCachedValues_();
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Clear all cached values.
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.clearCachedValues_ = function() {
|
|
this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getTextRangeCount = function() {
|
|
return 1;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getTextRange = function(i) {
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {goog.dom.browserrange.AbstractRange} The range wrapper object.
|
|
* @private
|
|
*/
|
|
goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
|
|
return this.browserRangeWrapper_ ||
|
|
(this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(
|
|
this.getStartNode(), this.getStartOffset(),
|
|
this.getEndNode(), this.getEndOffset()));
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getContainer = function() {
|
|
return this.getBrowserRangeWrapper_().getContainer();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getStartNode = function() {
|
|
return this.startNode_ ||
|
|
(this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getStartOffset = function() {
|
|
return this.startOffset_ != null ? this.startOffset_ :
|
|
(this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getStartPosition = function() {
|
|
return this.isReversed() ?
|
|
this.getBrowserRangeWrapper_().getEndPosition() :
|
|
this.getBrowserRangeWrapper_().getStartPosition();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getEndNode = function() {
|
|
return this.endNode_ ||
|
|
(this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getEndOffset = function() {
|
|
return this.endOffset_ != null ? this.endOffset_ :
|
|
(this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getEndPosition = function() {
|
|
return this.isReversed() ?
|
|
this.getBrowserRangeWrapper_().getStartPosition() :
|
|
this.getBrowserRangeWrapper_().getEndPosition();
|
|
};
|
|
|
|
|
|
/**
|
|
* Moves a TextRange to the provided nodes and offsets.
|
|
* @param {Node} startNode The node to start with.
|
|
* @param {number} startOffset The offset within the node to start.
|
|
* @param {Node} endNode The node to end with.
|
|
* @param {number} endOffset The offset within the node to end.
|
|
* @param {boolean} isReversed Whether the range is reversed.
|
|
*/
|
|
goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset,
|
|
endNode, endOffset,
|
|
isReversed) {
|
|
this.startNode_ = startNode;
|
|
this.startOffset_ = startOffset;
|
|
this.endNode_ = endNode;
|
|
this.endOffset_ = endOffset;
|
|
this.isReversed_ = isReversed;
|
|
this.browserRangeWrapper_ = null;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.isReversed = function() {
|
|
return this.isReversed_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.containsRange = function(otherRange,
|
|
opt_allowPartial) {
|
|
var otherRangeType = otherRange.getType();
|
|
if (otherRangeType == goog.dom.RangeType.TEXT) {
|
|
return this.getBrowserRangeWrapper_().containsRange(
|
|
otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
|
|
} else if (otherRangeType == goog.dom.RangeType.CONTROL) {
|
|
var elements = otherRange.getElements();
|
|
var fn = opt_allowPartial ? goog.array.some : goog.array.every;
|
|
return fn(elements, function(el) {
|
|
return this.containsNode(el, opt_allowPartial);
|
|
}, this);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Tests if the given node is in a document.
|
|
* @param {Node} node The node to check.
|
|
* @return {boolean} Whether the given node is in the given document.
|
|
*/
|
|
goog.dom.TextRange.isAttachedNode = function(node) {
|
|
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
|
|
var returnValue = false;
|
|
/** @preserveTry */
|
|
try {
|
|
returnValue = node.parentNode;
|
|
} catch (e) {
|
|
// IE sometimes throws Invalid Argument errors when a node is detached.
|
|
// 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;
|
|
} else {
|
|
return goog.dom.contains(node.ownerDocument.body, node);
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.isRangeInDocument = function() {
|
|
// Ensure any cached nodes are in the document. IE also allows ranges to
|
|
// become detached, so we check if the range is still in the document as
|
|
// well for IE.
|
|
return (!this.startNode_ ||
|
|
goog.dom.TextRange.isAttachedNode(this.startNode_)) &&
|
|
(!this.endNode_ ||
|
|
goog.dom.TextRange.isAttachedNode(this.endNode_)) &&
|
|
(!(goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||
|
|
this.getBrowserRangeWrapper_().isRangeInDocument());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.isCollapsed = function() {
|
|
return this.getBrowserRangeWrapper_().isCollapsed();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getText = function() {
|
|
return this.getBrowserRangeWrapper_().getText();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getHtmlFragment = function() {
|
|
// TODO(robbyw): Generalize the code in browserrange so it is static and
|
|
// just takes an iterator. This would mean we don't always have to create a
|
|
// browser range.
|
|
return this.getBrowserRangeWrapper_().getHtmlFragment();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getValidHtml = function() {
|
|
return this.getBrowserRangeWrapper_().getValidHtml();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.getPastableHtml = function() {
|
|
// TODO(robbyw): Get any attributes the table or tr has.
|
|
|
|
var html = this.getValidHtml();
|
|
|
|
if (html.match(/^\s*<td\b/i)) {
|
|
// Match html starting with a TD.
|
|
html = '<table><tbody><tr>' + html + '</tr></tbody></table>';
|
|
} else if (html.match(/^\s*<tr\b/i)) {
|
|
// Match html starting with a TR.
|
|
html = '<table><tbody>' + html + '</tbody></table>';
|
|
} else if (html.match(/^\s*<tbody\b/i)) {
|
|
// Match html starting with a TBODY.
|
|
html = '<table>' + html + '</table>';
|
|
} else if (html.match(/^\s*<li\b/i)) {
|
|
// Match html starting with an LI.
|
|
var container = this.getContainer();
|
|
var tagType = goog.dom.TagName.UL;
|
|
while (container) {
|
|
if (container.tagName == goog.dom.TagName.OL) {
|
|
tagType = goog.dom.TagName.OL;
|
|
break;
|
|
} else if (container.tagName == goog.dom.TagName.UL) {
|
|
break;
|
|
}
|
|
container = container.parentNode;
|
|
}
|
|
html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');
|
|
}
|
|
|
|
return html;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns a TextRangeIterator over the contents of the range. Regardless of
|
|
* the direction of the range, the iterator will move in document order.
|
|
* @param {boolean=} opt_keys Unused for this iterator.
|
|
* @return {goog.dom.TextRangeIterator} An iterator over tags in the range.
|
|
* @override
|
|
*/
|
|
goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
|
|
return new goog.dom.TextRangeIterator(this.getStartNode(),
|
|
this.getStartOffset(), this.getEndNode(), this.getEndOffset());
|
|
};
|
|
|
|
|
|
// RANGE ACTIONS
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.select = function() {
|
|
this.getBrowserRangeWrapper_().select(this.isReversed_);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.removeContents = function() {
|
|
this.getBrowserRangeWrapper_().removeContents();
|
|
this.clearCachedValues_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Surrounds the text range with the specified element (on Mozilla) or with a
|
|
* clone of the specified element (on IE). Returns a reference to the
|
|
* surrounding element if the operation was successful; returns null if the
|
|
* operation failed.
|
|
* @param {Element} element The element with which the selection is to be
|
|
* surrounded.
|
|
* @return {Element} The surrounding element (same as the argument on Mozilla,
|
|
* but not on IE), or null if unsuccessful.
|
|
*/
|
|
goog.dom.TextRange.prototype.surroundContents = function(element) {
|
|
var output = this.getBrowserRangeWrapper_().surroundContents(element);
|
|
this.clearCachedValues_();
|
|
return output;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.insertNode = function(node, before) {
|
|
var output = this.getBrowserRangeWrapper_().insertNode(node, before);
|
|
this.clearCachedValues_();
|
|
return output;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
|
|
this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
|
|
this.clearCachedValues_();
|
|
};
|
|
|
|
|
|
// SAVE/RESTORE
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.saveUsingDom = function() {
|
|
return new goog.dom.DomSavedTextRange_(this);
|
|
};
|
|
|
|
|
|
// RANGE MODIFICATION
|
|
|
|
|
|
/** @override */
|
|
goog.dom.TextRange.prototype.collapse = function(toAnchor) {
|
|
var toStart = this.isReversed() ? !toAnchor : toAnchor;
|
|
|
|
if (this.browserRangeWrapper_) {
|
|
this.browserRangeWrapper_.collapse(toStart);
|
|
}
|
|
|
|
if (toStart) {
|
|
this.endNode_ = this.startNode_;
|
|
this.endOffset_ = this.startOffset_;
|
|
} else {
|
|
this.startNode_ = this.endNode_;
|
|
this.startOffset_ = this.endOffset_;
|
|
}
|
|
|
|
// Collapsed ranges can't be reversed
|
|
this.isReversed_ = false;
|
|
};
|
|
|
|
|
|
// SAVED RANGE OBJECTS
|
|
|
|
|
|
|
|
/**
|
|
* A SavedRange implementation using DOM endpoints.
|
|
* @param {goog.dom.AbstractRange} range The range to save.
|
|
* @constructor
|
|
* @extends {goog.dom.SavedRange}
|
|
* @private
|
|
*/
|
|
goog.dom.DomSavedTextRange_ = function(range) {
|
|
/**
|
|
* The anchor node.
|
|
* @type {Node}
|
|
* @private
|
|
*/
|
|
this.anchorNode_ = range.getAnchorNode();
|
|
|
|
/**
|
|
* The anchor node offset.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.anchorOffset_ = range.getAnchorOffset();
|
|
|
|
/**
|
|
* The focus node.
|
|
* @type {Node}
|
|
* @private
|
|
*/
|
|
this.focusNode_ = range.getFocusNode();
|
|
|
|
/**
|
|
* The focus node offset.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.focusOffset_ = range.getFocusOffset();
|
|
};
|
|
goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
|
|
|
|
|
|
/**
|
|
* @return {goog.dom.AbstractRange} The restored range.
|
|
* @override
|
|
*/
|
|
goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
|
|
return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_,
|
|
this.focusNode_, this.focusOffset_);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
|
|
goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
|
|
|
|
this.anchorNode_ = null;
|
|
this.focusNode_ = null;
|
|
};
|