Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,350 @@
// 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 browser range interface.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.dom.browserrange.AbstractRange');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.TagName');
goog.require('goog.dom.TextRangeIterator');
goog.require('goog.iter');
goog.require('goog.math.Coordinate');
goog.require('goog.string');
goog.require('goog.string.StringBuffer');
goog.require('goog.userAgent');
/**
* The constructor for abstract ranges. Don't call this from subclasses.
* @constructor
*/
goog.dom.browserrange.AbstractRange = function() {
};
/**
* @return {goog.dom.browserrange.AbstractRange} A clone of this range.
*/
goog.dom.browserrange.AbstractRange.prototype.clone = goog.abstractMethod;
/**
* Returns the browser native implementation of the range. Please refrain from
* using this function - if you find you need the range please add wrappers for
* the functionality you need rather than just using the native range.
* @return {Range|TextRange} The browser native range object.
*/
goog.dom.browserrange.AbstractRange.prototype.getBrowserRange =
goog.abstractMethod;
/**
* Returns the deepest node in the tree that contains the entire range.
* @return {Node} The deepest node that contains the entire range.
*/
goog.dom.browserrange.AbstractRange.prototype.getContainer =
goog.abstractMethod;
/**
* Returns the node the range starts in.
* @return {Node} The element or text node the range starts in.
*/
goog.dom.browserrange.AbstractRange.prototype.getStartNode =
goog.abstractMethod;
/**
* Returns the offset into the node the range starts in.
* @return {number} The offset into the node the range starts in. For text
* nodes, this is an offset into the node value. For elements, this is
* an offset into the childNodes array.
*/
goog.dom.browserrange.AbstractRange.prototype.getStartOffset =
goog.abstractMethod;
/**
* @return {goog.math.Coordinate} The coordinate of the selection start node
* and offset.
*/
goog.dom.browserrange.AbstractRange.prototype.getStartPosition = function() {
goog.asserts.assert(this.range_.getClientRects,
'Getting selection coordinates is not supported.');
var rects = this.range_.getClientRects();
if (rects.length) {
return new goog.math.Coordinate(rects[0]['left'], rects[0]['top']);
}
return null;
};
/**
* Returns the node the range ends in.
* @return {Node} The element or text node the range ends in.
*/
goog.dom.browserrange.AbstractRange.prototype.getEndNode =
goog.abstractMethod;
/**
* Returns the offset into the node the range ends in.
* @return {number} The offset into the node the range ends in. For text
* nodes, this is an offset into the node value. For elements, this is
* an offset into the childNodes array.
*/
goog.dom.browserrange.AbstractRange.prototype.getEndOffset =
goog.abstractMethod;
/**
* @return {goog.math.Coordinate} The coordinate of the selection end node
* and offset.
*/
goog.dom.browserrange.AbstractRange.prototype.getEndPosition = function() {
goog.asserts.assert(this.range_.getClientRects,
'Getting selection coordinates is not supported.');
var rects = this.range_.getClientRects();
if (rects.length) {
var lastRect = goog.array.peek(rects);
return new goog.math.Coordinate(lastRect['right'], lastRect['bottom']);
}
return null;
};
/**
* Compares one endpoint of this range with the endpoint of another browser
* native range object.
* @param {Range|TextRange} range The browser native range to compare against.
* @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range
* to compare with.
* @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the other
* range to compare with.
* @return {number} 0 if the endpoints are equal, negative if this range
* endpoint comes before the other range endpoint, and positive otherwise.
*/
goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints =
goog.abstractMethod;
/**
* Tests if this range contains the given range.
* @param {goog.dom.browserrange.AbstractRange} abstractRange The range to test.
* @param {boolean=} opt_allowPartial If not set or false, the range must be
* entirely contained in the selection for this function to return true.
* @return {boolean} Whether this range contains the given range.
*/
goog.dom.browserrange.AbstractRange.prototype.containsRange =
function(abstractRange, opt_allowPartial) {
// IE sometimes misreports the boundaries for collapsed ranges. So if the
// other range is collapsed, make sure the whole range is contained. This is
// logically equivalent, and works around IE's bug.
var checkPartial = opt_allowPartial && !abstractRange.isCollapsed();
var range = abstractRange.getBrowserRange();
var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
/** @preserveTry */
try {
if (checkPartial) {
// There are two ways to not overlap. Being before, and being after.
// Before is represented by this.end before range.start: comparison < 0.
// After is represented by this.start after range.end: comparison > 0.
// The below is the negation of not overlapping.
return this.compareBrowserRangeEndpoints(range, end, start) >= 0 &&
this.compareBrowserRangeEndpoints(range, start, end) <= 0;
} else {
// Return true if this range bounds the parameter range from both sides.
return this.compareBrowserRangeEndpoints(range, end, end) >= 0 &&
this.compareBrowserRangeEndpoints(range, start, start) <= 0;
}
} catch (e) {
if (!goog.userAgent.IE) {
throw e;
}
// IE sometimes throws exceptions when one range is invalid, i.e. points
// to a node that has been removed from the document. Return false in this
// case.
return false;
}
};
/**
* Tests if this range contains the given node.
* @param {Node} node The node to test.
* @param {boolean=} opt_allowPartial If not set or false, the node must be
* entirely contained in the selection for this function to return true.
* @return {boolean} Whether this range contains the given node.
*/
goog.dom.browserrange.AbstractRange.prototype.containsNode = function(node,
opt_allowPartial) {
return this.containsRange(
goog.dom.browserrange.createRangeFromNodeContents(node),
opt_allowPartial);
};
/**
* Tests if the selection is collapsed - i.e. is just a caret.
* @return {boolean} Whether the range is collapsed.
*/
goog.dom.browserrange.AbstractRange.prototype.isCollapsed =
goog.abstractMethod;
/**
* @return {string} The text content of the range.
*/
goog.dom.browserrange.AbstractRange.prototype.getText =
goog.abstractMethod;
/**
* Returns the HTML fragment this range selects. This is slow on all browsers.
* @return {string} HTML fragment of the range, does not include context
* containing elements.
*/
goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() {
var output = new goog.string.StringBuffer();
goog.iter.forEach(this, function(node, ignore, it) {
if (node.nodeType == goog.dom.NodeType.TEXT) {
output.append(goog.string.htmlEscape(node.nodeValue.substring(
it.getStartTextOffset(), it.getEndTextOffset())));
} else if (node.nodeType == goog.dom.NodeType.ELEMENT) {
if (it.isEndTag()) {
if (goog.dom.canHaveChildren(node)) {
output.append('</' + node.tagName + '>');
}
} else {
var shallow = node.cloneNode(false);
var html = goog.dom.getOuterHtml(shallow);
if (goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) {
// For an LI, IE just returns "<li>" with no closing tag
output.append(html);
} else {
var index = html.lastIndexOf('<');
output.append(index ? html.substr(0, index) : html);
}
}
}
}, this);
return output.toString();
};
/**
* Returns valid HTML for this range. This is fast on IE, and semi-fast on
* other browsers.
* @return {string} Valid HTML of the range, including context containing
* elements.
*/
goog.dom.browserrange.AbstractRange.prototype.getValidHtml =
goog.abstractMethod;
/**
* Returns a RangeIterator 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.RangeIterator} An iterator over tags in the range.
*/
goog.dom.browserrange.AbstractRange.prototype.__iterator__ = function(
opt_keys) {
return new goog.dom.TextRangeIterator(this.getStartNode(),
this.getStartOffset(), this.getEndNode(), this.getEndOffset());
};
// SELECTION MODIFICATION
/**
* Set this range as the selection in its window.
* @param {boolean=} opt_reverse Whether to select the range in reverse,
* if possible.
*/
goog.dom.browserrange.AbstractRange.prototype.select =
goog.abstractMethod;
/**
* Removes the contents of the range from the document. As a side effect, the
* selection will be collapsed. The behavior of content removal is normalized
* across browsers. For instance, IE sometimes creates extra text nodes that
* a W3C browser does not. That behavior is corrected for.
*/
goog.dom.browserrange.AbstractRange.prototype.removeContents =
goog.abstractMethod;
/**
* 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.browserrange.AbstractRange.prototype.surroundContents =
goog.abstractMethod;
/**
* Inserts a node before (or after) the range. The range may be disrupted
* beyond recovery because of the way this splits nodes.
* @param {Node} node The node to insert.
* @param {boolean} before True to insert before, false to insert after.
* @return {Node} The node added to the document. This may be different
* than the node parameter because on IE we have to clone it.
*/
goog.dom.browserrange.AbstractRange.prototype.insertNode =
goog.abstractMethod;
/**
* Surrounds this range with the two given nodes. The range may be disrupted
* beyond recovery because of the way this splits nodes.
* @param {Element} startNode The node to insert at the start.
* @param {Element} endNode The node to insert at the end.
*/
goog.dom.browserrange.AbstractRange.prototype.surroundWithNodes =
goog.abstractMethod;
/**
* Collapses the range to one of its boundary points.
* @param {boolean} toStart Whether to collapse to the start of the range.
*/
goog.dom.browserrange.AbstractRange.prototype.collapse =
goog.abstractMethod;
@@ -0,0 +1,149 @@
// 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 browser range namespace and interface, as
* well as several useful utility functions.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
* @author robbyw@google.com (Robby Walker)
*
* @supported IE6, IE7, FF1.5+, Safari.
*/
goog.provide('goog.dom.browserrange');
goog.provide('goog.dom.browserrange.Error');
goog.require('goog.dom');
goog.require('goog.dom.BrowserFeature');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.browserrange.GeckoRange');
goog.require('goog.dom.browserrange.IeRange');
goog.require('goog.dom.browserrange.OperaRange');
goog.require('goog.dom.browserrange.W3cRange');
goog.require('goog.dom.browserrange.WebKitRange');
goog.require('goog.userAgent');
/**
* Common error constants.
* @enum {string}
*/
goog.dom.browserrange.Error = {
NOT_IMPLEMENTED: 'Not Implemented'
};
// NOTE(robbyw): While it would be nice to eliminate the duplicate switches
// below, doing so uncovers bugs in the JsCompiler in which
// necessary code is stripped out.
/**
* Static method that returns the proper type of browser range.
* @param {Range|TextRange} range A browser range object.
* @return {!goog.dom.browserrange.AbstractRange} A wrapper object.
*/
goog.dom.browserrange.createRange = function(range) {
if (goog.dom.BrowserFeature.LEGACY_IE_RANGES) {
return new goog.dom.browserrange.IeRange(
/** @type {TextRange} */ (range),
goog.dom.getOwnerDocument(range.parentElement()));
} else if (goog.userAgent.WEBKIT) {
return new goog.dom.browserrange.WebKitRange(
/** @type {Range} */ (range));
} else if (goog.userAgent.GECKO) {
return new goog.dom.browserrange.GeckoRange(
/** @type {Range} */ (range));
} else if (goog.userAgent.OPERA) {
return new goog.dom.browserrange.OperaRange(
/** @type {Range} */ (range));
} else {
// Default other browsers, including Opera, to W3c ranges.
return new goog.dom.browserrange.W3cRange(
/** @type {Range} */ (range));
}
};
/**
* Static method that returns the proper type of browser range.
* @param {Node} node The node to select.
* @return {!goog.dom.browserrange.AbstractRange} A wrapper object.
*/
goog.dom.browserrange.createRangeFromNodeContents = function(node) {
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
return goog.dom.browserrange.IeRange.createFromNodeContents(node);
} else if (goog.userAgent.WEBKIT) {
return goog.dom.browserrange.WebKitRange.createFromNodeContents(node);
} else if (goog.userAgent.GECKO) {
return goog.dom.browserrange.GeckoRange.createFromNodeContents(node);
} else if (goog.userAgent.OPERA) {
return goog.dom.browserrange.OperaRange.createFromNodeContents(node);
} else {
// Default other browsers to W3c ranges.
return goog.dom.browserrange.W3cRange.createFromNodeContents(node);
}
};
/**
* Static method that returns the proper type of browser range.
* @param {Node} startNode The node to start with.
* @param {number} startOffset The offset within the node to start. This is
* either the index into the childNodes array for element startNodes or
* the index into the character array for text startNodes.
* @param {Node} endNode The node to end with.
* @param {number} endOffset The offset within the node to end. This is
* either the index into the childNodes array for element endNodes or
* the index into the character array for text endNodes.
* @return {!goog.dom.browserrange.AbstractRange} A wrapper object.
*/
goog.dom.browserrange.createRangeFromNodes = function(startNode, startOffset,
endNode, endOffset) {
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
return goog.dom.browserrange.IeRange.createFromNodes(startNode, startOffset,
endNode, endOffset);
} else if (goog.userAgent.WEBKIT) {
return goog.dom.browserrange.WebKitRange.createFromNodes(startNode,
startOffset, endNode, endOffset);
} else if (goog.userAgent.GECKO) {
return goog.dom.browserrange.GeckoRange.createFromNodes(startNode,
startOffset, endNode, endOffset);
} else if (goog.userAgent.OPERA) {
return goog.dom.browserrange.OperaRange.createFromNodes(startNode,
startOffset, endNode, endOffset);
} else {
// Default other browsers to W3c ranges.
return goog.dom.browserrange.W3cRange.createFromNodes(startNode,
startOffset, endNode, endOffset);
}
};
/**
* Tests whether the given node can contain a range end point.
* @param {Node} node The node to check.
* @return {boolean} Whether the given node can contain a range end point.
*/
goog.dom.browserrange.canContainRangeEndpoint = function(node) {
// NOTE(user, bloom): This is not complete, as divs with style -
// 'display:inline-block' or 'position:absolute' can also not contain range
// endpoints. A more complete check is to see if that element can be partially
// selected (can be container) or not.
return goog.dom.canHaveChildren(node) ||
node.nodeType == goog.dom.NodeType.TEXT;
};
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.dom.browserrange</title>
<script src="../../base.js"></script>
<script>
goog.require('goog.dom.browserrangeTest');
</script>
</head>
<body>
<div id="sandbox"></div>
<div id="test1">Text</div><div id="test2">abc<br id="br">def</div>
<div id="cetest" contentEditable="true"><div>abc<br id="br2"></div></div>
<div id="empty"></div>
<div id="removeTest"><div>Text that<br>will be deleted</div></div>
<div id="removeTestEmptyNode"></div>
<div id="removeTestSingleNode"><div>Text Text</div></div>
<div id="removeTestMidNode"><div>0123456789</div></div>
<div id="removeTestMidMultipleNodes"><div>0123456789</div><div>0123456789</div></div>
<div id="outer">outer<div id="inner">inner</div>outer2</div>
<div id="dynamic"></div>
<div id="onlybr"><br/></div>
</body>
</html>
@@ -0,0 +1,633 @@
// 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.
goog.provide('goog.dom.browserrangeTest');
goog.setTestOnly('goog.dom.browserrangeTest');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.TagName');
goog.require('goog.dom.browserrange');
goog.require('goog.testing.dom');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var test1;
var test2;
var cetest;
var empty;
var dynamic;
var onlybrdiv;
function setUpPage() {
test1 = goog.dom.getElement('test1');
test2 = goog.dom.getElement('test2');
cetest = goog.dom.getElement('cetest');
empty = goog.dom.getElement('empty');
dynamic = goog.dom.getElement('dynamic');
onlybrdiv = goog.dom.getElement('onlybr');
}
function testCreate() {
assertNotNull('Browser range object can be created for node',
goog.dom.browserrange.createRangeFromNodeContents(test1));
}
function testRangeEndPoints() {
var container = cetest.firstChild;
var range = goog.dom.browserrange.createRangeFromNodes(
container, 2, container, 2);
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
var endNode = selRange.getEndNode();
var startOffset = selRange.getStartOffset();
var endOffset = selRange.getEndOffset();
if (goog.userAgent.WEBKIT) {
assertEquals('Start node should have text: abc',
'abc', startNode.nodeValue);
assertEquals('End node should have text: abc', 'abc', endNode.nodeValue);
assertEquals('Start offset should be 3', 3, startOffset);
assertEquals('End offset should be 3', 3, endOffset);
} else {
assertEquals('Start node should be the first div', container, startNode);
assertEquals('End node should be the first div', container, endNode);
assertEquals('Start offset should be 2', 2, startOffset);
assertEquals('End offset should be 2', 2, endOffset);
}
}
function testCreateFromNodeContents() {
var range = goog.dom.Range.createFromNodeContents(onlybrdiv);
goog.testing.dom.assertRangeEquals(onlybrdiv, 0, onlybrdiv, 1, range);
}
function normalizeHtml(str) {
return str.toLowerCase().replace(/[\n\r\f"]/g, '');
}
// TODO(robbyw): We really need tests for (and code fixes for)
// createRangeFromNodes in the following cases:
// * BR boundary (before + after)
function testCreateFromNodes() {
var start = test1.firstChild;
var range = goog.dom.browserrange.createRangeFromNodes(start, 2,
test2.firstChild, 2);
assertNotNull('Browser range object can be created for W3C node range',
range);
assertEquals('Start node should be selected at start endpoint', start,
range.getStartNode());
assertEquals('Selection should start at offset 2', 2,
range.getStartOffset());
assertEquals('Text node should be selected at end endpoint',
test2.firstChild, range.getEndNode());
assertEquals('Selection should end at offset 2', 2, range.getEndOffset());
assertTrue('Text content should be "xt\\s*ab"',
/xt\s*ab/.test(range.getText()));
assertFalse('Nodes range is not collapsed', range.isCollapsed());
assertEquals('Should contain correct html fragment',
'xt</div><div id=test2>ab',
normalizeHtml(range.getHtmlFragment()));
assertEquals('Should contain correct valid html',
'<div id=test1>xt</div><div id=test2>ab</div>',
normalizeHtml(range.getValidHtml()));
}
function testTextNode() {
var range = goog.dom.browserrange.createRangeFromNodeContents(
test1.firstChild);
assertEquals('Text node should be selected at start endpoint', 'Text',
range.getStartNode().nodeValue);
assertEquals('Selection should start at offset 0', 0,
range.getStartOffset());
assertEquals('Text node should be selected at end endpoint', 'Text',
range.getEndNode().nodeValue);
assertEquals('Selection should end at offset 4', 'Text'.length,
range.getEndOffset());
assertEquals('Container should be text node', goog.dom.NodeType.TEXT,
range.getContainer().nodeType);
assertEquals('Text content should be "Text"', 'Text', range.getText());
assertFalse('Text range is not collapsed', range.isCollapsed());
assertEquals('Should contain correct html fragment', 'Text',
range.getHtmlFragment());
assertEquals('Should contain correct valid html',
'Text', range.getValidHtml());
}
function testTextNodes() {
goog.dom.removeChildren(dynamic);
dynamic.appendChild(goog.dom.createTextNode('Part1'));
dynamic.appendChild(goog.dom.createTextNode('Part2'));
var range = goog.dom.browserrange.createRangeFromNodes(
dynamic.firstChild, 0, dynamic.lastChild, 5);
assertEquals('Text node 1 should be selected at start endpoint', 'Part1',
range.getStartNode().nodeValue);
assertEquals('Selection should start at offset 0', 0,
range.getStartOffset());
assertEquals('Text node 2 should be selected at end endpoint', 'Part2',
range.getEndNode().nodeValue);
assertEquals('Selection should end at offset 5', 'Part2'.length,
range.getEndOffset());
assertEquals('Container should be DIV', goog.dom.TagName.DIV,
range.getContainer().tagName);
assertEquals('Text content should be "Part1Part2"', 'Part1Part2',
range.getText());
assertFalse('Text range is not collapsed', range.isCollapsed());
assertEquals('Should contain correct html fragment', 'Part1Part2',
range.getHtmlFragment());
assertEquals('Should contain correct valid html',
'part1part2',
normalizeHtml(range.getValidHtml()));
}
function testDiv() {
var range = goog.dom.browserrange.createRangeFromNodeContents(test2);
assertEquals('Text node "abc" should be selected at start endpoint', 'abc',
range.getStartNode().nodeValue);
assertEquals('Selection should start at offset 0', 0,
range.getStartOffset());
assertEquals('Text node "def" should be selected at end endpoint', 'def',
range.getEndNode().nodeValue);
assertEquals('Selection should end at offset 3', 'def'.length,
range.getEndOffset());
assertEquals('Container should be DIV', 'DIV',
range.getContainer().tagName);
assertTrue('Div text content should be "abc\\s*def"',
/abc\s*def/.test(range.getText()));
assertEquals('Should contain correct html fragment', 'abc<br id=br>def',
normalizeHtml(range.getHtmlFragment()));
assertEquals('Should contain correct valid html',
'<div id=test2>abc<br id=br>def</div>',
normalizeHtml(range.getValidHtml()));
assertFalse('Div range is not collapsed', range.isCollapsed());
}
function testEmptyNodeHtmlInsert() {
var range = goog.dom.browserrange.createRangeFromNodeContents(empty);
var html = '<b>hello</b>';
range.insertNode(goog.dom.htmlToDocumentFragment(html));
assertEquals('Html is not inserted correctly', html,
normalizeHtml(empty.innerHTML));
goog.dom.removeChildren(empty);
}
function testEmptyNode() {
var range = goog.dom.browserrange.createRangeFromNodeContents(empty);
assertEquals('DIV be selected at start endpoint', 'DIV',
range.getStartNode().tagName);
assertEquals('Selection should start at offset 0', 0,
range.getStartOffset());
assertEquals('DIV should be selected at end endpoint', 'DIV',
range.getEndNode().tagName);
assertEquals('Selection should end at offset 0', 0,
range.getEndOffset());
assertEquals('Container should be DIV', 'DIV',
range.getContainer().tagName);
assertEquals('Empty text content should be ""', '', range.getText());
assertTrue('Empty range is collapsed', range.isCollapsed());
assertEquals('Should contain correct valid html', '<div id=empty></div>',
normalizeHtml(range.getValidHtml()));
assertEquals('Should contain no html fragment', '',
range.getHtmlFragment());
}
function testRemoveContents() {
var outer = goog.dom.getElement('removeTest');
var range = goog.dom.browserrange.createRangeFromNodeContents(
outer.firstChild);
range.removeContents();
assertEquals('Removed range content should be ""', '', range.getText());
assertTrue('Removed range is now collapsed', range.isCollapsed());
assertEquals('Outer div has 1 child now', 1, outer.childNodes.length);
assertEquals('Inner div is empty', 0, outer.firstChild.childNodes.length);
}
function testRemoveContentsEmptyNode() {
var outer = goog.dom.getElement('removeTestEmptyNode');
var range = goog.dom.browserrange.createRangeFromNodeContents(
outer);
range.removeContents();
assertEquals('Removed range content should be ""', '', range.getText());
assertTrue('Removed range is now collapsed', range.isCollapsed());
assertEquals('Outer div should have 0 children now',
0, outer.childNodes.length);
}
function testRemoveContentsSingleNode() {
var outer = goog.dom.getElement('removeTestSingleNode');
var range = goog.dom.browserrange.createRangeFromNodeContents(
outer.firstChild);
range.removeContents();
assertEquals('Removed range content should be ""', '', range.getText());
assertTrue('Removed range is now collapsed', range.isCollapsed());
assertEquals('', goog.dom.getTextContent(outer));
}
function testRemoveContentsMidNode() {
var outer = goog.dom.getElement('removeTestMidNode');
var textNode = outer.firstChild.firstChild;
var range = goog.dom.browserrange.createRangeFromNodes(
textNode, 1, textNode, 4);
assertEquals('Previous range content should be "123"', '123',
range.getText());
range.removeContents();
assertEquals('Removed range content should be "0456789"', '0456789',
goog.dom.getTextContent(outer));
}
function testRemoveContentsMidMultipleNodes() {
var outer = goog.dom.getElement('removeTestMidMultipleNodes');
var firstTextNode = outer.firstChild.firstChild;
var lastTextNode = outer.lastChild.firstChild;
var range = goog.dom.browserrange.createRangeFromNodes(
firstTextNode, 1, lastTextNode, 4);
assertEquals('Previous range content', '1234567890123',
range.getText().replace(/\s/g, ''));
range.removeContents();
assertEquals('Removed range content should be "0456789"', '0456789',
goog.dom.getTextContent(outer).replace(/\s/g, ''));
}
function testRemoveDivCaretRange() {
var outer = goog.dom.getElement('sandbox');
outer.innerHTML = '<div>Test1</div><div></div>';
var range = goog.dom.browserrange.createRangeFromNodes(
outer.lastChild, 0, outer.lastChild, 0);
range.removeContents();
range.insertNode(goog.dom.createDom('span', undefined, 'Hello'), true);
assertEquals('Resulting contents', 'Test1Hello',
goog.dom.getTextContent(outer).replace(/\s/g, ''));
}
function testCollapse() {
var range = goog.dom.browserrange.createRangeFromNodeContents(test2);
assertFalse('Div range is not collapsed', range.isCollapsed());
range.collapse();
assertTrue('Div range is collapsed after call to empty()',
range.isCollapsed());
range = goog.dom.browserrange.createRangeFromNodeContents(empty);
assertTrue('Empty range is collapsed', range.isCollapsed());
range.collapse();
assertTrue('Empty range is still collapsed', range.isCollapsed());
}
function testIdWithSpecialCharacters() {
goog.dom.removeChildren(dynamic);
dynamic.appendChild(goog.dom.createTextNode('1'));
dynamic.appendChild(goog.dom.createDom('div', {id: '<>'}));
dynamic.appendChild(goog.dom.createTextNode('2'));
var range = goog.dom.browserrange.createRangeFromNodes(
dynamic.firstChild, 0, dynamic.lastChild, 1);
// Difference in special character handling is ok.
assertContains('Should have correct html fragment',
normalizeHtml(range.getHtmlFragment()),
[
'1<div id=<>></div>2', // IE
'1<div id=&lt;>></div>2', // WebKit
'1<div id=&lt;&gt;></div>2' // Others
]);
}
function testEndOfChildren() {
dynamic.innerHTML =
'<span id="a">123<br>456</span><span id="b">text</span>';
var range = goog.dom.browserrange.createRangeFromNodes(
goog.dom.getElement('a'), 3, goog.dom.getElement('b'), 1);
assertEquals('Should have correct text.', 'text', range.getText());
}
function testEndOfDiv() {
dynamic.innerHTML = '<div id="a">abc</div><div id="b">def</div>';
var a = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(a, 1, a, 1);
var expectedStartNode = a;
var expectedStartOffset = 1;
var expectedEndNode = a;
var expectedEndOffset = 1;
assertEquals('startNode is wrong', expectedStartNode, range.getStartNode());
assertEquals('startOffset is wrong',
expectedStartOffset, range.getStartOffset());
assertEquals('endNode is wrong', expectedEndNode, range.getEndNode());
assertEquals('endOffset is wrong', expectedEndOffset, range.getEndOffset());
}
function testRangeEndingWithBR() {
dynamic.innerHTML = '<span id="a">123<br>456</span>';
var spanElem = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(
spanElem, 0, spanElem, 2);
var htmlText = range.getValidHtml().toLowerCase();
assertContains('Should include BR in HTML.', 'br', htmlText);
assertEquals('Should have correct text.', '123', range.getText());
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Start node should be span', spanElem, startNode);
} else {
assertEquals('Startnode should have text:123',
'123', startNode.nodeValue);
}
assertEquals('Startoffset should be 0', 0, selRange.getStartOffset());
var endNode = selRange.getEndNode();
assertEquals('Endnode should be span', spanElem, endNode);
assertEquals('Endoffset should be 2', 2, selRange.getEndOffset());
}
function testRangeEndingWithBR2() {
dynamic.innerHTML = '<span id="a">123<br></span>';
var spanElem = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(
spanElem, 0, spanElem, 2);
var htmlText = range.getValidHtml().toLowerCase();
assertContains('Should include BR in HTML.', 'br', htmlText);
assertEquals('Should have correct text.', '123', range.getText());
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Start node should be span', spanElem, startNode);
} else {
assertEquals('Start node should have text:123',
'123', startNode.nodeValue);
}
assertEquals('Startoffset should be 0', 0, selRange.getStartOffset());
var endNode = selRange.getEndNode();
if (goog.userAgent.WEBKIT) {
assertEquals('Endnode should have text', '123', endNode.nodeValue);
assertEquals('Endoffset should be 3', 3, selRange.getEndOffset());
} else {
assertEquals('Endnode should be span', spanElem, endNode);
assertEquals('Endoffset should be 2', 2, selRange.getEndOffset());
}
}
function testRangeEndingBeforeBR() {
dynamic.innerHTML = '<span id="a">123<br>456</span>';
var spanElem = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(
spanElem, 0, spanElem, 1);
var htmlText = range.getValidHtml().toLowerCase();
assertNotContains('Should not include BR in HTML.', 'br', htmlText);
assertEquals('Should have correct text.', '123', range.getText());
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Start node should be span', spanElem, startNode);
} else {
assertEquals('Startnode should have text:123',
'123', startNode.nodeValue);
}
assertEquals('Startoffset should be 0', 0, selRange.getStartOffset());
var endNode = selRange.getEndNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Endnode should be span', spanElem, endNode);
assertEquals('Endoffset should be 1', 1, selRange.getEndOffset());
} else {
assertEquals('Endnode should have text:123', '123', endNode.nodeValue);
assertEquals('Endoffset should be 3', 3, selRange.getEndOffset());
}
}
function testRangeStartingWithBR() {
dynamic.innerHTML = '<span id="a">123<br>456</span>';
var spanElem = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(
spanElem, 1, spanElem, 3);
var htmlText = range.getValidHtml().toLowerCase();
assertContains('Should include BR in HTML.', 'br', htmlText);
// Firefox returns '456' as the range text while IE returns '\r\n456'.
// Therefore skipping the text check.
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
assertEquals('Start node should be span', spanElem, startNode);
assertEquals('Startoffset should be 1', 1, selRange.getStartOffset());
var endNode = selRange.getEndNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Endnode should be span', spanElem, endNode);
assertEquals('Endoffset should be 3', 3, selRange.getEndOffset());
} else {
assertEquals('Endnode should have text:456', '456', endNode.nodeValue);
assertEquals('Endoffset should be 3', 3, selRange.getEndOffset());
}
}
function testRangeStartingAfterBR() {
dynamic.innerHTML = '<span id="a">123<br>4567</span>';
var spanElem = goog.dom.getElement('a');
var range = goog.dom.browserrange.createRangeFromNodes(
spanElem, 2, spanElem, 3);
var htmlText = range.getValidHtml().toLowerCase();
assertNotContains('Should not include BR in HTML.', 'br', htmlText);
assertEquals('Should have correct text.', '4567', range.getText());
range.select();
var selRange = goog.dom.Range.createFromWindow();
var startNode = selRange.getStartNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Start node should be span', spanElem, startNode);
assertEquals('Startoffset should be 2', 2, selRange.getStartOffset());
} else {
assertEquals('Startnode should have text:4567',
'4567', startNode.nodeValue);
assertEquals('Startoffset should be 0', 0, selRange.getStartOffset());
}
var endNode = selRange.getEndNode();
if (goog.userAgent.GECKO ||
(goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9))) {
assertEquals('Endnode should be span', spanElem, endNode);
assertEquals('Endoffset should be 3', 3, selRange.getEndOffset());
} else {
assertEquals('Endnode should have text:4567', '4567', endNode.nodeValue);
assertEquals('Endoffset should be 4', 4, selRange.getEndOffset());
}
}
function testCollapsedRangeBeforeBR() {
dynamic.innerHTML = '<span id="a">123<br>456</span>';
var range = goog.dom.browserrange.createRangeFromNodes(
goog.dom.getElement('a'), 1, goog.dom.getElement('a'), 1);
// Firefox returns <span id="a"></span> as the range HTML while IE returns
// empty string. Therefore skipping the HTML check.
assertEquals('Should have no text.', '', range.getText());
}
function testCollapsedRangeAfterBR() {
dynamic.innerHTML = '<span id="a">123<br>456</span>';
var range = goog.dom.browserrange.createRangeFromNodes(
goog.dom.getElement('a'), 2, goog.dom.getElement('a'), 2);
// Firefox returns <span id="a"></span> as the range HTML while IE returns
// empty string. Therefore skipping the HTML check.
assertEquals('Should have no text.', '', range.getText());
}
function testCompareBrowserRangeEndpoints() {
var outer = goog.dom.getElement('outer');
var inner = goog.dom.getElement('inner');
var range_outer = goog.dom.browserrange.createRangeFromNodeContents(outer);
var range_inner = goog.dom.browserrange.createRangeFromNodeContents(inner);
assertEquals(
'The start of the inner selection should be after the outer.',
1,
range_inner.compareBrowserRangeEndpoints(
range_outer.getBrowserRange(),
goog.dom.RangeEndpoint.START,
goog.dom.RangeEndpoint.START));
assertEquals(
"The start of the inner selection should be before the outer's end.",
-1,
range_inner.compareBrowserRangeEndpoints(
range_outer.getBrowserRange(),
goog.dom.RangeEndpoint.START,
goog.dom.RangeEndpoint.END));
assertEquals(
"The end of the inner selection should be after the outer's start.",
1,
range_inner.compareBrowserRangeEndpoints(
range_outer.getBrowserRange(),
goog.dom.RangeEndpoint.END,
goog.dom.RangeEndpoint.START));
assertEquals(
"The end of the inner selection should be before the outer's end.",
-1,
range_inner.compareBrowserRangeEndpoints(
range_outer.getBrowserRange(),
goog.dom.RangeEndpoint.END,
goog.dom.RangeEndpoint.END));
}
/**
* Regression test for a bug in IeRange.insertNode_ where if the node to be
* inserted was not an element (e.g. a text node), it would clone the node
* in the inserting process but return the original node instead of the newly
* created and inserted node.
*/
function testInsertNodeNonElement() {
dynamic.innerHTML = 'beforeafter';
var range = goog.dom.browserrange.createRangeFromNodes(
dynamic.firstChild, 6, dynamic.firstChild, 6);
var newNode = goog.dom.createTextNode('INSERTED');
var inserted = range.insertNode(newNode, false);
assertEquals('Text should be inserted between "before" and "after"',
'beforeINSERTEDafter',
goog.dom.getRawTextContent(dynamic));
assertEquals('Node returned by insertNode() should be a child of the div' +
' containing the text',
dynamic,
inserted.parentNode);
}
function testSelectOverwritesOldSelection() {
goog.dom.browserrange.createRangeFromNodes(test1, 0, test1, 1).select();
goog.dom.browserrange.createRangeFromNodes(test2, 0, test2, 1).select();
assertEquals('The old selection must be replaced with the new one',
'abc', goog.dom.Range.createFromWindow().getText());
}
// Following testcase is special for IE. The comparison of ranges created in
// testcases with a range over empty span using native inRange fails. So the
// fallback mechanism is needed.
function testGetContainerInTextNodesAroundEmptySpan() {
dynamic.innerHTML = 'abc<span></span>def';
var abc = dynamic.firstChild;
var def = dynamic.lastChild;
var range;
range = goog.dom.browserrange.createRangeFromNodes(abc, 1, abc, 1);
assertEquals('textNode abc should be the range container',
abc, range.getContainer());
assertEquals('textNode abc should be the range start node',
abc, range.getStartNode());
assertEquals('textNode abc should be the range end node',
abc, range.getEndNode());
range = goog.dom.browserrange.createRangeFromNodes(def, 1, def, 1);
assertEquals('textNode def should be the range container',
def, range.getContainer());
assertEquals('textNode def should be the range start node',
def, range.getStartNode());
assertEquals('textNode def should be the range end node',
def, range.getEndNode());
}
@@ -0,0 +1,88 @@
// 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 Gecko specific range wrapper. Inherits most
* functionality from W3CRange, but adds exceptions as necessary.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.dom.browserrange.GeckoRange');
goog.require('goog.dom.browserrange.W3cRange');
/**
* The constructor for Gecko specific browser ranges.
* @param {Range} range The range object.
* @constructor
* @extends {goog.dom.browserrange.W3cRange}
* @final
*/
goog.dom.browserrange.GeckoRange = function(range) {
goog.dom.browserrange.W3cRange.call(this, range);
};
goog.inherits(goog.dom.browserrange.GeckoRange, goog.dom.browserrange.W3cRange);
/**
* Creates a range object that selects the given node's text.
* @param {Node} node The node to select.
* @return {!goog.dom.browserrange.GeckoRange} A Gecko range wrapper object.
*/
goog.dom.browserrange.GeckoRange.createFromNodeContents = function(node) {
return new goog.dom.browserrange.GeckoRange(
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 node to start.
* @param {Node} endNode The node to end with.
* @param {number} endOffset The offset within the node to end.
* @return {!goog.dom.browserrange.GeckoRange} A wrapper object.
*/
goog.dom.browserrange.GeckoRange.createFromNodes = function(startNode,
startOffset, endNode, endOffset) {
return new goog.dom.browserrange.GeckoRange(
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode,
startOffset, endNode, endOffset));
};
/** @override */
goog.dom.browserrange.GeckoRange.prototype.selectInternal = function(
selection, reversed) {
if (!reversed || this.isCollapsed()) {
// The base implementation for select() is more robust, and works fine for
// collapsed and forward ranges. This works around
// https://bugzilla.mozilla.org/show_bug.cgi?id=773137, and is tested by
// range_test.html's testFocusedElementDisappears.
goog.dom.browserrange.GeckoRange.base(
this, 'selectInternal', selection, reversed);
} else {
// Reversed selection -- start with a caret on the end node, and extend it
// back to the start. Unfortunately, collapse() fails when focus is
// invalid.
selection.collapse(this.getEndNode(), this.getEndOffset());
selection.extend(this.getStartNode(), this.getStartOffset());
}
};
@@ -0,0 +1,951 @@
// 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 IE browser specific range wrapper.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.dom.browserrange.IeRange');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.TagName');
goog.require('goog.dom.browserrange.AbstractRange');
goog.require('goog.log');
goog.require('goog.string');
/**
* The constructor for IE specific browser ranges.
* @param {TextRange} range The range object.
* @param {Document} doc The document the range exists in.
* @constructor
* @extends {goog.dom.browserrange.AbstractRange}
* @final
*/
goog.dom.browserrange.IeRange = function(range, doc) {
/**
* The browser range object this class wraps.
* @type {TextRange}
* @private
*/
this.range_ = range;
/**
* The document the range exists in.
* @type {Document}
* @private
*/
this.doc_ = doc;
};
goog.inherits(goog.dom.browserrange.IeRange,
goog.dom.browserrange.AbstractRange);
/**
* Logging object.
* @type {goog.log.Logger}
* @private
*/
goog.dom.browserrange.IeRange.logger_ =
goog.log.getLogger('goog.dom.browserrange.IeRange');
/**
* Returns a browser range spanning the given node's contents.
* @param {Node} node The node to select.
* @return {!TextRange} A browser range spanning the node's contents.
* @private
*/
goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {
var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
// Elements are easy.
nodeRange.moveToElementText(node);
// Note(user) : If there are no child nodes of the element, the
// range.htmlText includes the element's outerHTML. The range created above
// is not collapsed, and should be collapsed explicitly.
// Example : node = <div></div>
// But if the node is sth like <br>, it shouldnt be collapsed.
if (goog.dom.browserrange.canContainRangeEndpoint(node) &&
!node.childNodes.length) {
nodeRange.collapse(false);
}
} else {
// Text nodes are hard.
// Compute the offset from the nearest element related position.
var offset = 0;
var sibling = node;
while (sibling = sibling.previousSibling) {
var nodeType = sibling.nodeType;
if (nodeType == goog.dom.NodeType.TEXT) {
offset += sibling.length;
} else if (nodeType == goog.dom.NodeType.ELEMENT) {
// Move to the space after this element.
nodeRange.moveToElementText(sibling);
break;
}
}
if (!sibling) {
nodeRange.moveToElementText(node.parentNode);
}
nodeRange.collapse(!sibling);
if (offset) {
nodeRange.move('character', offset);
}
nodeRange.moveEnd('character', node.length);
}
return nodeRange;
};
/**
* Returns a browser range spanning 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 {!TextRange} A browser range spanning the node's contents.
* @private
*/
goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(startNode,
startOffset, endNode, endOffset) {
// Create a range starting at the correct start position.
var child, collapse = false;
if (startNode.nodeType == goog.dom.NodeType.ELEMENT) {
if (startOffset > startNode.childNodes.length) {
goog.log.error(goog.dom.browserrange.IeRange.logger_,
'Cannot have startOffset > startNode child count');
}
child = startNode.childNodes[startOffset];
collapse = !child;
startNode = child || startNode.lastChild || startNode;
startOffset = 0;
}
var leftRange = goog.dom.browserrange.IeRange.
getBrowserRangeForNode_(startNode);
// This happens only when startNode is a text node.
if (startOffset) {
leftRange.move('character', startOffset);
}
// The range movements in IE are still an approximation to the standard W3C
// behavior, and IE has its trickery when it comes to htmlText and text
// properties of the range. So we short-circuit computation whenever we can.
if (startNode == endNode && startOffset == endOffset) {
leftRange.collapse(true);
return leftRange;
}
// This can happen only when the startNode is an element, and there is no node
// at the given offset. We start at the last point inside the startNode in
// that case.
if (collapse) {
leftRange.collapse(false);
}
// Create a range that ends at the right position.
collapse = false;
if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {
if (endOffset > endNode.childNodes.length) {
goog.log.error(goog.dom.browserrange.IeRange.logger_,
'Cannot have endOffset > endNode child count');
}
child = endNode.childNodes[endOffset];
endNode = child || endNode.lastChild || endNode;
endOffset = 0;
collapse = !child;
}
var rightRange = goog.dom.browserrange.IeRange.
getBrowserRangeForNode_(endNode);
rightRange.collapse(!collapse);
if (endOffset) {
rightRange.moveEnd('character', endOffset);
}
// Merge and return.
leftRange.setEndPoint('EndToEnd', rightRange);
return leftRange;
};
/**
* Create a range object that selects the given node's text.
* @param {Node} node The node to select.
* @return {!goog.dom.browserrange.IeRange} An IE range wrapper object.
*/
goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {
var range = new goog.dom.browserrange.IeRange(
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node),
goog.dom.getOwnerDocument(node));
if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;
range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);
range.endOffset_ = range.startOffset_ + 1;
} else {
// Note(user) : Emulate the behavior of W3CRange - Go to deepest possible
// range containers on both edges. It seems W3CRange did this to match the
// IE behavior, and now it is a circle. Changing W3CRange may break clients
// in all sorts of ways.
var tempNode, leaf = node;
while ((tempNode = leaf.firstChild) &&
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
leaf = tempNode;
}
range.startNode_ = leaf;
range.startOffset_ = 0;
leaf = node;
while ((tempNode = leaf.lastChild) &&
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
leaf = tempNode;
}
range.endNode_ = leaf;
range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ?
leaf.childNodes.length : leaf.length;
range.parentNode_ = node;
}
return range;
};
/**
* Static method that returns the proper type of browser range.
* @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.AbstractRange} A wrapper object.
*/
goog.dom.browserrange.IeRange.createFromNodes = function(startNode,
startOffset, endNode, endOffset) {
var range = new goog.dom.browserrange.IeRange(
goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(startNode,
startOffset, endNode, endOffset),
goog.dom.getOwnerDocument(startNode));
range.startNode_ = startNode;
range.startOffset_ = startOffset;
range.endNode_ = endNode;
range.endOffset_ = endOffset;
return range;
};
// Even though goog.dom.TextRange does similar caching to below, keeping these
// caches allows for better performance in the get*Offset methods.
/**
* Lazy cache of the node containing the entire selection.
* @type {Node}
* @private
*/
goog.dom.browserrange.IeRange.prototype.parentNode_ = null;
/**
* Lazy cache of the node containing the start of the selection.
* @type {Node}
* @private
*/
goog.dom.browserrange.IeRange.prototype.startNode_ = null;
/**
* Lazy cache of the node containing the end of the selection.
* @type {Node}
* @private
*/
goog.dom.browserrange.IeRange.prototype.endNode_ = null;
/**
* Lazy cache of the offset in startNode_ where this range starts.
* @type {number}
* @private
*/
goog.dom.browserrange.IeRange.prototype.startOffset_ = -1;
/**
* Lazy cache of the offset in endNode_ where this range ends.
* @type {number}
* @private
*/
goog.dom.browserrange.IeRange.prototype.endOffset_ = -1;
/**
* @return {!goog.dom.browserrange.IeRange} A clone of this range.
* @override
*/
goog.dom.browserrange.IeRange.prototype.clone = function() {
var range = new goog.dom.browserrange.IeRange(
this.range_.duplicate(), this.doc_);
range.parentNode_ = this.parentNode_;
range.startNode_ = this.startNode_;
range.endNode_ = this.endNode_;
return range;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {
return this.range_;
};
/**
* Clears the cached values for containers.
* @private
*/
goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {
this.parentNode_ = this.startNode_ = this.endNode_ = null;
this.startOffset_ = this.endOffset_ = -1;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getContainer = function() {
if (!this.parentNode_) {
var selectText = this.range_.text;
// If the selection ends with spaces, we need to remove these to get the
// parent container of only the real contents. This is to get around IE's
// inconsistency where it selects the spaces after a word when you double
// click, but leaves out the spaces during execCommands.
var range = this.range_.duplicate();
// We can't use goog.string.trimRight, as that will remove other whitespace
// too.
var rightTrimmedSelectText = selectText.replace(/ +$/, '');
var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;
if (numSpacesAtEnd) {
range.moveEnd('character', -numSpacesAtEnd);
}
// Get the parent node. This should be the end, but alas, it is not.
var parent = range.parentElement();
var htmlText = range.htmlText;
var htmlTextLen = goog.string.stripNewlines(htmlText).length;
if (this.isCollapsed() && htmlTextLen > 0) {
return (this.parentNode_ = parent);
}
// Deal with selection bug where IE thinks one of the selection's children
// is actually the selection's parent. Relies on the assumption that the
// HTML text of the parent container is longer than the length of the
// selection's HTML text.
// Also note IE will sometimes insert \r and \n whitespace, which should be
// disregarded. Otherwise the loop may run too long and return wrong parent
while (htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {
parent = parent.parentNode;
}
// Deal with IE's selecting the outer tags when you double click
// If the innerText is the same, then we just want the inner node
while (parent.childNodes.length == 1 &&
parent.innerText == goog.dom.browserrange.IeRange.getNodeText_(
parent.firstChild)) {
// A container should be an element which can have children or a text
// node. Elements like IMG, BR, etc. can not be containers.
if (!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {
break;
}
parent = parent.firstChild;
}
// If the selection is empty, we may need to do extra work to position it
// properly.
if (selectText.length == 0) {
parent = this.findDeepestContainer_(parent);
}
this.parentNode_ = parent;
}
return this.parentNode_;
};
/**
* Helper method to find the deepest parent for this range, starting
* the search from {@code node}, which must contain the range.
* @param {Node} node The node to start the search from.
* @return {Node} The deepest parent for this range.
* @private
*/
goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {
var childNodes = node.childNodes;
for (var i = 0, len = childNodes.length; i < len; i++) {
var child = childNodes[i];
if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
var childRange =
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);
var start = goog.dom.RangeEndpoint.START;
var end = goog.dom.RangeEndpoint.END;
// There are two types of erratic nodes where the range over node has
// different htmlText than the node's outerHTML.
// Case 1 - A node with magic &nbsp; child. In this case :
// nodeRange.htmlText shows &nbsp; ('<p>&nbsp;</p>), while
// node.outerHTML doesn't show the magic node (<p></p>).
// Case 2 - Empty span. In this case :
// node.outerHTML shows '<span></span>'
// node.htmlText is just empty string ''.
var isChildRangeErratic = (childRange.htmlText != child.outerHTML);
// Moreover the inRange comparison fails only when the
var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;
// In case 2 mentioned above, childRange is also collapsed. So we need to
// compare start of this range with both start and end of child range.
var inChildRange = isNativeInRangeErratic ?
(this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 &&
this.compareBrowserRangeEndpoints(childRange, start, end) <= 0) :
this.range_.inRange(childRange);
if (inChildRange) {
return this.findDeepestContainer_(child);
}
}
}
return node;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getStartNode = function() {
if (!this.startNode_) {
this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);
if (this.isCollapsed()) {
this.endNode_ = this.startNode_;
}
}
return this.startNode_;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {
if (this.startOffset_ < 0) {
this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);
if (this.isCollapsed()) {
this.endOffset_ = this.startOffset_;
}
}
return this.startOffset_;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getEndNode = function() {
if (this.isCollapsed()) {
return this.getStartNode();
}
if (!this.endNode_) {
this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END);
}
return this.endNode_;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {
if (this.isCollapsed()) {
return this.getStartOffset();
}
if (this.endOffset_ < 0) {
this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);
if (this.isCollapsed()) {
this.startOffset_ = this.endOffset_;
}
}
return this.endOffset_;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(
range, thisEndpoint, otherEndpoint) {
return this.range_.compareEndPoints(
(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') +
'To' +
(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
range);
};
/**
* Recurses to find the correct node for the given endpoint.
* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the node for.
* @param {Node=} opt_node Optional node to start the search from.
* @return {Node} The deepest node containing the endpoint.
* @private
*/
goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(endpoint,
opt_node) {
/** @type {Node} */
var node = opt_node || this.getContainer();
// If we're at a leaf in the DOM, we're done.
if (!node || !node.firstChild) {
return node;
}
var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
var isStartEndpoint = endpoint == start;
// Find the first/last child that overlaps the selection.
// NOTE(user) : One of the children can be the magic &nbsp; node. This
// node will have only nodeType property as valid and accessible. All other
// dom related properties like ownerDocument, parentNode, nextSibling etc
// cause error when accessed. Therefore use the for-loop on childNodes to
// iterate.
for (var j = 0, length = node.childNodes.length; j < length; j++) {
var i = isStartEndpoint ? j : length - j - 1;
var child = node.childNodes[i];
var childRange;
try {
childRange = goog.dom.browserrange.createRangeFromNodeContents(child);
} catch (e) {
// If the child is the magic &nbsp; node, then the above will throw
// error. The magic node exists only when editing using keyboard, so can
// not add any unit test.
continue;
}
var ieRange = childRange.getBrowserRange();
// Case 1 : Finding end points when this range is collapsed.
// Note that in case of collapsed range, getEnd{Node,Offset} call
// getStart{Node,Offset}.
if (this.isCollapsed()) {
// Handle situations where caret is not in a text node. In such cases,
// the adjacent child won't be a valid range endpoint container.
if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
// The following handles a scenario like <div><BR>[caret]<BR></div>,
// where point should be (div, 1).
if (this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {
this.startOffset_ = this.endOffset_ = i;
return node;
}
} else if (childRange.containsRange(this)) {
// For collapsed range, we should invert the containsRange check with
// childRange.
return this.getEndpointNode_(endpoint, child);
}
// Case 2 - The first child encountered to have overlap this range is
// contained entirely in this range.
} else if (this.containsRange(childRange)) {
// If it is an element which can not be a range endpoint container, the
// current child offset can be used to deduce the endpoint offset.
if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
// Container can't be any deeper, so current node is the container.
if (isStartEndpoint) {
this.startOffset_ = i;
} else {
this.endOffset_ = i + 1;
}
return node;
}
// If child can contain range endpoints, recurse inside this child.
return this.getEndpointNode_(endpoint, child);
// Case 3 - Partial non-adjacency overlap.
} else if (this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 &&
this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {
// If this child overlaps the selection partially, recurse down to find
// the first/last child the next level down that overlaps the selection
// completely. We do not consider edge-adjacency (== 0) as overlap.
return this.getEndpointNode_(endpoint, child);
}
}
// None of the children of this node overlapped the selection, that means
// the selection starts/ends in this node directly.
return node;
};
/**
* Compares one endpoint of this range with the endpoint of a node.
* For internal methods, we should prefer this method to containsNode.
* containsNode has a lot of false negatives when we're dealing with
* {@code <br>} tags.
*
* @param {Node} node The node to compare against.
* @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range
* to compare with.
* @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the node
* to compare with.
* @return {number} 0 if the endpoints are equal, negative if this range
* endpoint comes before the other node endpoint, and positive otherwise.
* @private
*/
goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ =
function(node, thisEndpoint, otherEndpoint) {
return this.range_.compareEndPoints(
(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') +
'To' +
(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
goog.dom.browserrange.createRangeFromNodeContents(node).
getBrowserRange());
};
/**
* Returns the offset into the start/end container.
* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the offset for.
* @param {Node=} opt_container The container to get the offset relative to.
* Defaults to the value returned by getStartNode/getEndNode.
* @return {number} The offset.
* @private
*/
goog.dom.browserrange.IeRange.prototype.getOffset_ = function(endpoint,
opt_container) {
var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;
var container = opt_container ||
(isStartEndpoint ? this.getStartNode() : this.getEndNode());
if (container.nodeType == goog.dom.NodeType.ELEMENT) {
// Find the first/last child that overlaps the selection
var children = container.childNodes;
var len = children.length;
var edge = isStartEndpoint ? 0 : len - 1;
var sign = isStartEndpoint ? 1 : - 1;
// We find the index in the child array of the endpoint of the selection.
for (var i = edge; i >= 0 && i < len; i += sign) {
var child = children[i];
// Ignore the child nodes, which could be end point containers.
if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
continue;
}
// Stop looping when we reach the edge of the selection.
var endPointCompare =
this.compareNodeEndpoints_(child, endpoint, endpoint);
if (endPointCompare == 0) {
return isStartEndpoint ? i : i + 1;
}
}
// When starting from the end in an empty container, we erroneously return
// -1: fix this to return 0.
return i == -1 ? 0 : i;
} else {
// Get a temporary range object.
var range = this.range_.duplicate();
// Create a range that selects the entire container.
var nodeRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(
container);
// Now, intersect our range with the container range - this should give us
// the part of our selection that is in the container.
range.setEndPoint(isStartEndpoint ? 'EndToEnd' : 'StartToStart', nodeRange);
var rangeLength = range.text.length;
return isStartEndpoint ? container.length - rangeLength : rangeLength;
}
};
/**
* Returns the text of the given node. Uses IE specific properties.
* @param {Node} node The node to retrieve the text of.
* @return {string} The node's text.
* @private
*/
goog.dom.browserrange.IeRange.getNodeText_ = function(node) {
return node.nodeType == goog.dom.NodeType.TEXT ?
node.nodeValue : node.innerText;
};
/**
* Tests whether this range is valid (i.e. whether its endpoints are still in
* the document). A range becomes invalid when, after this object was created,
* either one or both of its endpoints are removed from the document. Use of
* an invalid range can lead to runtime errors, particularly in IE.
* @return {boolean} Whether the range is valid.
*/
goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {
var range = this.doc_.body.createTextRange();
range.moveToElementText(this.doc_.body);
return this.containsRange(
new goog.dom.browserrange.IeRange(range, this.doc_), true);
};
/** @override */
goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {
// Note(user) : The earlier implementation used (range.text == ''), but this
// fails when (range.htmlText == '<br>')
// Alternative: this.range_.htmlText == '';
return this.range_.compareEndPoints('StartToEnd', this.range_) == 0;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getText = function() {
return this.range_.text;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {
return this.range_.htmlText;
};
// SELECTION MODIFICATION
/** @override */
goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {
// IE doesn't support programmatic reversed selections.
this.range_.select();
};
/** @override */
goog.dom.browserrange.IeRange.prototype.removeContents = function() {
// NOTE: Sometimes htmlText is non-empty, but the range is actually empty.
// TODO(gboyer): The htmlText check is probably unnecessary, but I left it in
// for paranoia.
if (!this.isCollapsed() && this.range_.htmlText) {
// Store some before-removal state.
var startNode = this.getStartNode();
var endNode = this.getEndNode();
var oldText = this.range_.text;
// IE sometimes deletes nodes unrelated to the selection. This trick fixes
// that problem most of the time. Even though it looks like a no-op, it is
// somehow changing IE's internal state such that empty unrelated nodes are
// no longer deleted.
var clone = this.range_.duplicate();
clone.moveStart('character', 1);
clone.moveStart('character', -1);
// However, sometimes moving the start back and forth ends up changing the
// range.
// TODO(gboyer): This condition used to happen for empty ranges, but (1)
// never worked, and (2) the isCollapsed call should protect against empty
// ranges better than before. However, this is left for paranoia.
if (clone.text == oldText) {
this.range_ = clone;
}
// Use the browser's native deletion code.
this.range_.text = '';
this.clearCachedValues_();
// Unfortunately, when deleting a portion of a single text node, IE creates
// an extra text node unlike other browsers which just change the text in
// the node. We normalize for that behavior here, making IE behave like all
// the other browsers.
var newStartNode = this.getStartNode();
var newStartOffset = this.getStartOffset();
/** @preserveTry */
try {
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);
// Make sure to reselect the appropriate position.
this.range_ = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(
newStartNode);
this.range_.move('character', newStartOffset);
this.clearCachedValues_();
}
} catch (e) {
// IE throws errors on orphaned nodes.
}
}
};
/**
* @param {TextRange} range The range to get a dom helper for.
* @return {!goog.dom.DomHelper} A dom helper for the document the range
* resides in.
* @private
*/
goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {
return goog.dom.getDomHelper(range.parentElement());
};
/**
* Pastes the given element into the given range, returning the resulting
* element.
* @param {TextRange} range The range to paste into.
* @param {Element} element The node to insert a copy of.
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper object for the document
* the range resides in.
* @return {Element} The resulting copy of element.
* @private
*/
goog.dom.browserrange.IeRange.pasteElement_ = function(range, element,
opt_domHelper) {
opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(
range);
// Make sure the node has a unique id.
var id;
var originalId = id = element.id;
if (!id) {
id = element.id = goog.string.createUniqueString();
}
// Insert (a clone of) the node.
range.pasteHTML(element.outerHTML);
// Pasting the outerHTML of the modified element into the document creates
// a clone of the element argument. We want to return a reference to the
// clone, not the original. However we need to remove the temporary ID
// first.
element = opt_domHelper.getElement(id);
// If element is null here, we failed.
if (element) {
if (!originalId) {
element.removeAttribute('id');
}
}
return element;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {
// Make sure the element is detached from the document.
goog.dom.removeNode(element);
// IE more or less guarantees that range.htmlText is well-formed & valid.
element.innerHTML = this.range_.htmlText;
element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);
// If element is null here, we failed.
if (element) {
this.range_.moveToElementText(element);
}
this.clearCachedValues_();
return element;
};
/**
* Internal handler for inserting a node.
* @param {TextRange} clone A clone of this range's browser range object.
* @param {Node} node The node to insert.
* @param {boolean} before Whether to insert the node before or after the range.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use.
* @return {Node} The resulting copy of node.
* @private
*/
goog.dom.browserrange.IeRange.insertNode_ = function(clone, node,
before, opt_domHelper) {
// Get a DOM helper.
opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(
clone);
// If it's not an element, wrap it in one.
var isNonElement;
if (node.nodeType != goog.dom.NodeType.ELEMENT) {
isNonElement = true;
node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node);
}
clone.collapse(before);
node = goog.dom.browserrange.IeRange.pasteElement_(clone,
/** @type {!Element} */ (node), opt_domHelper);
// If we didn't want an element, unwrap the element and return the node.
if (isNonElement) {
// pasteElement_() may have returned a copy of the wrapper div, and the
// node it wraps could also be a new copy. So we must extract that new
// node from the new wrapper.
var newNonElement = node.firstChild;
opt_domHelper.flattenElement(node);
node = newNonElement;
}
return node;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {
var output = goog.dom.browserrange.IeRange.insertNode_(
this.range_.duplicate(), node, before);
this.clearCachedValues_();
return output;
};
/** @override */
goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(
startNode, endNode) {
var clone1 = this.range_.duplicate();
var clone2 = this.range_.duplicate();
goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);
goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);
this.clearCachedValues_();
};
/** @override */
goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {
this.range_.collapse(toStart);
if (toStart) {
this.endNode_ = this.startNode_;
this.endOffset_ = this.startOffset_;
} else {
this.startNode_ = this.endNode_;
this.startOffset_ = this.endOffset_;
}
};
@@ -0,0 +1,84 @@
// Copyright 2009 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 Opera specific range wrapper. Inherits most
* functionality from W3CRange, but adds exceptions as necessary.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
*/
goog.provide('goog.dom.browserrange.OperaRange');
goog.require('goog.dom.browserrange.W3cRange');
/**
* The constructor for Opera specific browser ranges.
* @param {Range} range The range object.
* @constructor
* @extends {goog.dom.browserrange.W3cRange}
* @final
*/
goog.dom.browserrange.OperaRange = function(range) {
goog.dom.browserrange.W3cRange.call(this, range);
};
goog.inherits(goog.dom.browserrange.OperaRange, goog.dom.browserrange.W3cRange);
/**
* Creates a range object that selects the given node's text.
* @param {Node} node The node to select.
* @return {!goog.dom.browserrange.OperaRange} A Opera range wrapper object.
*/
goog.dom.browserrange.OperaRange.createFromNodeContents = function(node) {
return new goog.dom.browserrange.OperaRange(
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 node to start.
* @param {Node} endNode The node to end with.
* @param {number} endOffset The offset within the node to end.
* @return {!goog.dom.browserrange.OperaRange} A wrapper object.
*/
goog.dom.browserrange.OperaRange.createFromNodes = function(startNode,
startOffset, endNode, endOffset) {
return new goog.dom.browserrange.OperaRange(
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode,
startOffset, endNode, endOffset));
};
/** @override */
goog.dom.browserrange.OperaRange.prototype.selectInternal = function(
selection, reversed) {
// Avoid using addRange as we have to removeAllRanges first, which
// blurs editable fields in Opera.
selection.collapse(this.getStartNode(), this.getStartOffset());
if (this.getEndNode() != this.getStartNode() ||
this.getEndOffset() != this.getStartOffset()) {
selection.extend(this.getEndNode(), this.getEndOffset());
}
// This can happen if the range isn't in an editable field.
if (selection.rangeCount == 0) {
selection.addRange(this.range_);
}
};
@@ -0,0 +1,394 @@
// 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);
};
@@ -0,0 +1,113 @@
// 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 WebKit specific range wrapper. Inherits most
* functionality from W3CRange, but adds exceptions as necessary.
*
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.dom.browserrange.WebKitRange');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.browserrange.W3cRange');
goog.require('goog.userAgent');
/**
* The constructor for WebKit specific browser ranges.
* @param {Range} range The range object.
* @constructor
* @extends {goog.dom.browserrange.W3cRange}
* @final
*/
goog.dom.browserrange.WebKitRange = function(range) {
goog.dom.browserrange.W3cRange.call(this, range);
};
goog.inherits(goog.dom.browserrange.WebKitRange,
goog.dom.browserrange.W3cRange);
/**
* Creates a range object that selects the given node's text.
* @param {Node} node The node to select.
* @return {!goog.dom.browserrange.WebKitRange} A WebKit range wrapper object.
*/
goog.dom.browserrange.WebKitRange.createFromNodeContents = function(node) {
return new goog.dom.browserrange.WebKitRange(
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.WebKitRange} A wrapper object.
*/
goog.dom.browserrange.WebKitRange.createFromNodes = function(startNode,
startOffset, endNode, endOffset) {
return new goog.dom.browserrange.WebKitRange(
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode,
startOffset, endNode, endOffset));
};
/** @override */
goog.dom.browserrange.WebKitRange.prototype.compareBrowserRangeEndpoints =
function(range, thisEndpoint, otherEndpoint) {
// Webkit pre-528 has some bugs where compareBoundaryPoints() doesn't work the
// way it is supposed to, but if we reverse the sense of two comparisons,
// it works fine.
// https://bugs.webkit.org/show_bug.cgi?id=20738
if (goog.userAgent.isVersionOrHigher('528')) {
return (goog.dom.browserrange.WebKitRange.superClass_.
compareBrowserRangeEndpoints.call(
this, 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'].END_TO_START) : // Sense reversed
(thisEndpoint == goog.dom.RangeEndpoint.START ?
goog.global['Range'].START_TO_END : // Sense reversed
goog.global['Range'].END_TO_END),
/** @type {Range} */ (range));
};
/** @override */
goog.dom.browserrange.WebKitRange.prototype.selectInternal = function(
selection, reversed) {
// Unselect everything. This addresses a bug in Webkit where it sometimes
// caches the old selection.
// https://bugs.webkit.org/show_bug.cgi?id=20117
selection.removeAllRanges();
if (reversed) {
selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(),
this.getStartNode(), this.getStartOffset());
} else {
selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(),
this.getEndNode(), this.getEndOffset());
}
};