blob: 599b50ca53eaaf1bba4ed3fe8544eb36e8edf9fe [file] [log] [blame]
// 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)
*/
goog.provide('goog.dom.browserrange.W3cRange');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.browserrange.AbstractRange');
goog.require('goog.string');
goog.require('goog.userAgent');
/**
* 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 {
/** @suppress {missingRequire} */
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) &&
/** @suppress {missingRequire} */
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
leaf = tempNode;
}
nodeRange.setStart(leaf, 0);
leaf = node;
while ((tempNode = leaf.lastChild) &&
/** @suppress {missingRequire} */
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);
}
}
}
if (goog.userAgent.IE) {
// Unfortunately, when deleting a portion of a single text node, IE creates
// an extra text node instead of modifying the nodeValue of the start node.
// We normalize for that behavior here, similar to code in
// goog.dom.browserrange.IeRange#removeContents
// See https://connect.microsoft.com/IE/feedback/details/746591
var startNode = this.getStartNode();
var startOffset = this.getStartOffset();
var endNode = this.getEndNode();
var endOffset = this.getEndOffset();
var sibling = startNode.nextSibling;
if (startNode == endNode && startNode.parentNode &&
startNode.nodeType == goog.dom.NodeType.TEXT &&
sibling && sibling.nodeType == goog.dom.NodeType.TEXT) {
startNode.nodeValue += sibling.nodeValue;
goog.dom.removeNode(sibling);
// Modifying the node value clears the range offsets. Reselect the
// position in the modified start node.
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
}
}
};
/** @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()));
/** @suppress {missingRequire} */
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));
}
}
/** @suppress {missingRequire} */
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);
};