Path: blob/trunk/third_party/closure/goog/dom/browserrange/w3crange.js
1865 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 Definition of the W3C spec following range wrapper.16*17* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.18*19* @author [email protected] (Robby Walker)20*/212223goog.provide('goog.dom.browserrange.W3cRange');2425goog.require('goog.array');26goog.require('goog.dom');27goog.require('goog.dom.NodeType');28goog.require('goog.dom.RangeEndpoint');29goog.require('goog.dom.TagName');30goog.require('goog.dom.browserrange.AbstractRange');31goog.require('goog.string');32goog.require('goog.userAgent');33343536/**37* The constructor for W3C specific browser ranges.38* @param {Range} range The range object.39* @constructor40* @extends {goog.dom.browserrange.AbstractRange}41*/42goog.dom.browserrange.W3cRange = function(range) {43this.range_ = range;44};45goog.inherits(46goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange);474849/**50* Returns a browser range spanning the given node's contents.51* @param {Node} node The node to select.52* @return {!Range} A browser range spanning the node's contents.53* @protected54*/55goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {56var nodeRange = goog.dom.getOwnerDocument(node).createRange();5758if (node.nodeType == goog.dom.NodeType.TEXT) {59nodeRange.setStart(node, 0);60nodeRange.setEnd(node, node.length);61} else {62/** @suppress {missingRequire} */63if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {64var rangeParent = node.parentNode;65var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);66nodeRange.setStart(rangeParent, rangeStartOffset);67nodeRange.setEnd(rangeParent, rangeStartOffset + 1);68} else {69var tempNode, leaf = node;70while ((tempNode = leaf.firstChild) &&71/** @suppress {missingRequire} */72goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {73leaf = tempNode;74}75nodeRange.setStart(leaf, 0);7677leaf = node;78/** @suppress {missingRequire} Circular dep with browserrange */79while ((tempNode = leaf.lastChild) &&80goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {81leaf = tempNode;82}83nodeRange.setEnd(84leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ?85leaf.childNodes.length :86leaf.length);87}88}8990return nodeRange;91};929394/**95* Returns a browser range spanning the given nodes.96* @param {Node} startNode The node to start with - should not be a BR.97* @param {number} startOffset The offset within the start node.98* @param {Node} endNode The node to end with - should not be a BR.99* @param {number} endOffset The offset within the end node.100* @return {!Range} A browser range spanning the node's contents.101* @protected102*/103goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(104startNode, startOffset, endNode, endOffset) {105// Create and return the range.106var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();107nodeRange.setStart(startNode, startOffset);108nodeRange.setEnd(endNode, endOffset);109return nodeRange;110};111112113/**114* Creates a range object that selects the given node's text.115* @param {Node} node The node to select.116* @return {!goog.dom.browserrange.W3cRange} A Gecko range wrapper object.117*/118goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {119return new goog.dom.browserrange.W3cRange(120goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node));121};122123124/**125* Creates a range object that selects between the given nodes.126* @param {Node} startNode The node to start with.127* @param {number} startOffset The offset within the start node.128* @param {Node} endNode The node to end with.129* @param {number} endOffset The offset within the end node.130* @return {!goog.dom.browserrange.W3cRange} A wrapper object.131*/132goog.dom.browserrange.W3cRange.createFromNodes = function(133startNode, startOffset, endNode, endOffset) {134return new goog.dom.browserrange.W3cRange(135goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(136startNode, startOffset, endNode, endOffset));137};138139140/**141* @return {!goog.dom.browserrange.W3cRange} A clone of this range.142* @override143*/144goog.dom.browserrange.W3cRange.prototype.clone = function() {145return new this.constructor(this.range_.cloneRange());146};147148149/** @override */150goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {151return this.range_;152};153154155/** @override */156goog.dom.browserrange.W3cRange.prototype.getContainer = function() {157return this.range_.commonAncestorContainer;158};159160161/** @override */162goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {163return this.range_.startContainer;164};165166167/** @override */168goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {169return this.range_.startOffset;170};171172173/** @override */174goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {175return this.range_.endContainer;176};177178179/** @override */180goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {181return this.range_.endOffset;182};183184185/** @override */186goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints =187function(range, thisEndpoint, otherEndpoint) {188return this.range_.compareBoundaryPoints(189otherEndpoint == goog.dom.RangeEndpoint.START ?190(thisEndpoint == goog.dom.RangeEndpoint.START ?191goog.global['Range'].START_TO_START :192goog.global['Range'].START_TO_END) :193(thisEndpoint == goog.dom.RangeEndpoint.START ?194goog.global['Range'].END_TO_START :195goog.global['Range'].END_TO_END),196/** @type {Range} */ (range));197};198199200/** @override */201goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {202return this.range_.collapsed;203};204205206/** @override */207goog.dom.browserrange.W3cRange.prototype.getText = function() {208return this.range_.toString();209};210211212/** @override */213goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {214var div = goog.dom.getDomHelper(this.range_.startContainer)215.createDom(goog.dom.TagName.DIV);216div.appendChild(this.range_.cloneContents());217var result = div.innerHTML;218219if (goog.string.startsWith(result, '<') ||220!this.isCollapsed() && !goog.string.contains(result, '<')) {221// We attempt to mimic IE, which returns no containing element when a222// only text nodes are selected, does return the containing element when223// the selection is empty, and does return the element when multiple nodes224// are selected.225return result;226}227228var container = this.getContainer();229container = container.nodeType == goog.dom.NodeType.ELEMENT ?230container :231container.parentNode;232233var html = goog.dom.getOuterHtml(234/** @type {!Element} */ (container.cloneNode(false)));235return html.replace('>', '>' + result);236};237238239// SELECTION MODIFICATION240241242/** @override */243goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {244var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));245this.selectInternal(win.getSelection(), reverse);246};247248249/**250* Select this range.251* @param {Selection} selection Browser selection object.252* @param {*} reverse Whether to select this range in reverse.253* @protected254*/255goog.dom.browserrange.W3cRange.prototype.selectInternal = function(256selection, reverse) {257// Browser-specific tricks are needed to create reversed selections258// programatically. For this generic W3C codepath, ignore the reverse259// parameter.260selection.removeAllRanges();261selection.addRange(this.range_);262};263264265/** @override */266goog.dom.browserrange.W3cRange.prototype.removeContents = function() {267var range = this.range_;268range.extractContents();269270if (range.startContainer.hasChildNodes()) {271// Remove any now empty nodes surrounding the extracted contents.272var rangeStartContainer =273range.startContainer.childNodes[range.startOffset];274if (rangeStartContainer) {275var rangePrevious = rangeStartContainer.previousSibling;276277if (goog.dom.getRawTextContent(rangeStartContainer) == '') {278goog.dom.removeNode(rangeStartContainer);279}280281if (rangePrevious && goog.dom.getRawTextContent(rangePrevious) == '') {282goog.dom.removeNode(rangePrevious);283}284}285}286287if (goog.userAgent.EDGE_OR_IE) {288// Unfortunately, when deleting a portion of a single text node, IE creates289// an extra text node instead of modifying the nodeValue of the start node.290// We normalize for that behavior here, similar to code in291// goog.dom.browserrange.IeRange#removeContents292// See https://connect.microsoft.com/IE/feedback/details/746591293var startNode = this.getStartNode();294var startOffset = this.getStartOffset();295var endNode = this.getEndNode();296var endOffset = this.getEndOffset();297var sibling = startNode.nextSibling;298if (startNode == endNode && startNode.parentNode &&299startNode.nodeType == goog.dom.NodeType.TEXT && sibling &&300sibling.nodeType == goog.dom.NodeType.TEXT) {301startNode.nodeValue += sibling.nodeValue;302goog.dom.removeNode(sibling);303304// Modifying the node value clears the range offsets. Reselect the305// position in the modified start node.306range.setStart(startNode, startOffset);307range.setEnd(endNode, endOffset);308}309}310};311312313/** @override */314goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {315this.range_.surroundContents(element);316return element;317};318319320/** @override */321goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {322var range = this.range_.cloneRange();323range.collapse(before);324range.insertNode(node);325range.detach();326327return node;328};329330331/** @override */332goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(333startNode, endNode) {334var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));335/** @suppress {missingRequire} */336var selectionRange = goog.dom.Range.createFromWindow(win);337if (selectionRange) {338var sNode = selectionRange.getStartNode();339var eNode = selectionRange.getEndNode();340var sOffset = selectionRange.getStartOffset();341var eOffset = selectionRange.getEndOffset();342}343344var clone1 = this.range_.cloneRange();345var clone2 = this.range_.cloneRange();346347clone1.collapse(false);348clone2.collapse(true);349350clone1.insertNode(endNode);351clone2.insertNode(startNode);352353clone1.detach();354clone2.detach();355356if (selectionRange) {357// There are 4 ways that surroundWithNodes can wreck the saved358// selection object. All of them happen when an inserted node splits359// a text node, and one of the end points of the selection was in the360// latter half of that text node.361//362// Clients of this library should use saveUsingCarets to avoid this363// problem. Unfortunately, saveUsingCarets uses this method, so that's364// not really an option for us. :( We just recompute the offsets.365var isInsertedNode = function(n) { return n == startNode || n == endNode; };366if (sNode.nodeType == goog.dom.NodeType.TEXT) {367while (sOffset > sNode.length) {368sOffset -= sNode.length;369do {370sNode = sNode.nextSibling;371} while (isInsertedNode(sNode));372}373}374375if (eNode.nodeType == goog.dom.NodeType.TEXT) {376while (eOffset > eNode.length) {377eOffset -= eNode.length;378do {379eNode = eNode.nextSibling;380} while (isInsertedNode(eNode));381}382}383384/** @suppress {missingRequire} */385goog.dom.Range386.createFromNodes(387sNode, /** @type {number} */ (sOffset), eNode,388/** @type {number} */ (eOffset))389.select();390}391};392393394/** @override */395goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {396this.range_.collapse(toStart);397};398399400