366 lines
11 KiB
JavaScript
366 lines
11 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 Definition of the W3C spec following range wrapper.
|
|
*
|
|
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
|
|
*
|
|
* @author robbyw@google.com (Robby Walker)
|
|
* @author ojan@google.com (Ojan Vafai)
|
|
* @author jparent@google.com (Julie Parent)
|
|
*/
|
|
|
|
|
|
goog.provide('goog.dom.browserrange.W3cRange');
|
|
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.NodeType');
|
|
goog.require('goog.dom.RangeEndpoint');
|
|
goog.require('goog.dom.browserrange.AbstractRange');
|
|
goog.require('goog.string');
|
|
|
|
|
|
|
|
/**
|
|
* The constructor for W3C specific browser ranges.
|
|
* @param {Range} range The range object.
|
|
* @constructor
|
|
* @extends {goog.dom.browserrange.AbstractRange}
|
|
*/
|
|
goog.dom.browserrange.W3cRange = function(range) {
|
|
this.range_ = range;
|
|
};
|
|
goog.inherits(goog.dom.browserrange.W3cRange,
|
|
goog.dom.browserrange.AbstractRange);
|
|
|
|
|
|
/**
|
|
* Returns a browser range spanning the given node's contents.
|
|
* @param {Node} node The node to select.
|
|
* @return {Range} A browser range spanning the node's contents.
|
|
* @protected
|
|
*/
|
|
goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {
|
|
var nodeRange = goog.dom.getOwnerDocument(node).createRange();
|
|
|
|
if (node.nodeType == goog.dom.NodeType.TEXT) {
|
|
nodeRange.setStart(node, 0);
|
|
nodeRange.setEnd(node, node.length);
|
|
} else {
|
|
if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
|
|
var rangeParent = node.parentNode;
|
|
var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);
|
|
nodeRange.setStart(rangeParent, rangeStartOffset);
|
|
nodeRange.setEnd(rangeParent, rangeStartOffset + 1);
|
|
} else {
|
|
var tempNode, leaf = node;
|
|
while ((tempNode = leaf.firstChild) &&
|
|
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
|
|
leaf = tempNode;
|
|
}
|
|
nodeRange.setStart(leaf, 0);
|
|
|
|
leaf = node;
|
|
while ((tempNode = leaf.lastChild) &&
|
|
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
|
|
leaf = tempNode;
|
|
}
|
|
nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ?
|
|
leaf.childNodes.length : leaf.length);
|
|
}
|
|
}
|
|
|
|
return nodeRange;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns a browser range spanning the given nodes.
|
|
* @param {Node} startNode The node to start with - should not be a BR.
|
|
* @param {number} startOffset The offset within the start node.
|
|
* @param {Node} endNode The node to end with - should not be a BR.
|
|
* @param {number} endOffset The offset within the end node.
|
|
* @return {Range} A browser range spanning the node's contents.
|
|
* @protected
|
|
*/
|
|
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode,
|
|
startOffset, endNode, endOffset) {
|
|
// Create and return the range.
|
|
var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();
|
|
nodeRange.setStart(startNode, startOffset);
|
|
nodeRange.setEnd(endNode, endOffset);
|
|
return nodeRange;
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a range object that selects the given node's text.
|
|
* @param {Node} node The node to select.
|
|
* @return {goog.dom.browserrange.W3cRange} A Gecko range wrapper object.
|
|
*/
|
|
goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {
|
|
return new goog.dom.browserrange.W3cRange(
|
|
goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node));
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a range object that selects between the given nodes.
|
|
* @param {Node} startNode The node to start with.
|
|
* @param {number} startOffset The offset within the start node.
|
|
* @param {Node} endNode The node to end with.
|
|
* @param {number} endOffset The offset within the end node.
|
|
* @return {goog.dom.browserrange.W3cRange} A wrapper object.
|
|
*/
|
|
goog.dom.browserrange.W3cRange.createFromNodes = function(startNode,
|
|
startOffset, endNode, endOffset) {
|
|
return new goog.dom.browserrange.W3cRange(
|
|
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode,
|
|
startOffset, endNode, endOffset));
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {goog.dom.browserrange.W3cRange} A clone of this range.
|
|
* @override
|
|
*/
|
|
goog.dom.browserrange.W3cRange.prototype.clone = function() {
|
|
return new this.constructor(this.range_.cloneRange());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {
|
|
return this.range_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getContainer = function() {
|
|
return this.range_.commonAncestorContainer;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {
|
|
return this.range_.startContainer;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {
|
|
return this.range_.startOffset;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {
|
|
return this.range_.endContainer;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {
|
|
return this.range_.endOffset;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints =
|
|
function(range, thisEndpoint, otherEndpoint) {
|
|
return this.range_.compareBoundaryPoints(
|
|
otherEndpoint == goog.dom.RangeEndpoint.START ?
|
|
(thisEndpoint == goog.dom.RangeEndpoint.START ?
|
|
goog.global['Range'].START_TO_START :
|
|
goog.global['Range'].START_TO_END) :
|
|
(thisEndpoint == goog.dom.RangeEndpoint.START ?
|
|
goog.global['Range'].END_TO_START :
|
|
goog.global['Range'].END_TO_END),
|
|
/** @type {Range} */ (range));
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {
|
|
return this.range_.collapsed;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getText = function() {
|
|
return this.range_.toString();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {
|
|
var div = goog.dom.getDomHelper(this.range_.startContainer).createDom('div');
|
|
div.appendChild(this.range_.cloneContents());
|
|
var result = div.innerHTML;
|
|
|
|
if (goog.string.startsWith(result, '<') ||
|
|
!this.isCollapsed() && !goog.string.contains(result, '<')) {
|
|
// We attempt to mimic IE, which returns no containing element when a
|
|
// only text nodes are selected, does return the containing element when
|
|
// the selection is empty, and does return the element when multiple nodes
|
|
// are selected.
|
|
return result;
|
|
}
|
|
|
|
var container = this.getContainer();
|
|
container = container.nodeType == goog.dom.NodeType.ELEMENT ? container :
|
|
container.parentNode;
|
|
|
|
var html = goog.dom.getOuterHtml(
|
|
/** @type {Element} */ (container.cloneNode(false)));
|
|
return html.replace('>', '>' + result);
|
|
};
|
|
|
|
|
|
// SELECTION MODIFICATION
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {
|
|
var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
|
|
this.selectInternal(win.getSelection(), reverse);
|
|
};
|
|
|
|
|
|
/**
|
|
* Select this range.
|
|
* @param {Selection} selection Browser selection object.
|
|
* @param {*} reverse Whether to select this range in reverse.
|
|
* @protected
|
|
*/
|
|
goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection,
|
|
reverse) {
|
|
// Browser-specific tricks are needed to create reversed selections
|
|
// programatically. For this generic W3C codepath, ignore the reverse
|
|
// parameter.
|
|
selection.removeAllRanges();
|
|
selection.addRange(this.range_);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.removeContents = function() {
|
|
var range = this.range_;
|
|
range.extractContents();
|
|
|
|
if (range.startContainer.hasChildNodes()) {
|
|
// Remove any now empty nodes surrounding the extracted contents.
|
|
var rangeStartContainer =
|
|
range.startContainer.childNodes[range.startOffset];
|
|
if (rangeStartContainer) {
|
|
var rangePrevious = rangeStartContainer.previousSibling;
|
|
|
|
if (goog.dom.getRawTextContent(rangeStartContainer) == '') {
|
|
goog.dom.removeNode(rangeStartContainer);
|
|
}
|
|
|
|
if (rangePrevious && goog.dom.getRawTextContent(rangePrevious) == '') {
|
|
goog.dom.removeNode(rangePrevious);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {
|
|
this.range_.surroundContents(element);
|
|
return element;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {
|
|
var range = this.range_.cloneRange();
|
|
range.collapse(before);
|
|
range.insertNode(node);
|
|
range.detach();
|
|
|
|
return node;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(
|
|
startNode, endNode) {
|
|
var win = goog.dom.getWindow(
|
|
goog.dom.getOwnerDocument(this.getStartNode()));
|
|
var selectionRange = goog.dom.Range.createFromWindow(win);
|
|
if (selectionRange) {
|
|
var sNode = selectionRange.getStartNode();
|
|
var eNode = selectionRange.getEndNode();
|
|
var sOffset = selectionRange.getStartOffset();
|
|
var eOffset = selectionRange.getEndOffset();
|
|
}
|
|
|
|
var clone1 = this.range_.cloneRange();
|
|
var clone2 = this.range_.cloneRange();
|
|
|
|
clone1.collapse(false);
|
|
clone2.collapse(true);
|
|
|
|
clone1.insertNode(endNode);
|
|
clone2.insertNode(startNode);
|
|
|
|
clone1.detach();
|
|
clone2.detach();
|
|
|
|
if (selectionRange) {
|
|
// There are 4 ways that surroundWithNodes can wreck the saved
|
|
// selection object. All of them happen when an inserted node splits
|
|
// a text node, and one of the end points of the selection was in the
|
|
// latter half of that text node.
|
|
//
|
|
// Clients of this library should use saveUsingCarets to avoid this
|
|
// problem. Unfortunately, saveUsingCarets uses this method, so that's
|
|
// not really an option for us. :( We just recompute the offsets.
|
|
var isInsertedNode = function(n) {
|
|
return n == startNode || n == endNode;
|
|
};
|
|
if (sNode.nodeType == goog.dom.NodeType.TEXT) {
|
|
while (sOffset > sNode.length) {
|
|
sOffset -= sNode.length;
|
|
do {
|
|
sNode = sNode.nextSibling;
|
|
} while (isInsertedNode(sNode));
|
|
}
|
|
}
|
|
|
|
if (eNode.nodeType == goog.dom.NodeType.TEXT) {
|
|
while (eOffset > eNode.length) {
|
|
eOffset -= eNode.length;
|
|
do {
|
|
eNode = eNode.nextSibling;
|
|
} while (isInsertedNode(eNode));
|
|
}
|
|
}
|
|
|
|
goog.dom.Range.createFromNodes(
|
|
sNode, /** @type {number} */ (sOffset),
|
|
eNode, /** @type {number} */ (eOffset)).select();
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {
|
|
this.range_.collapse(toStart);
|
|
};
|