Adding mapbox-gl branch
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for working with ranges comprised of multiple
|
||||
* sub-ranges.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.AbstractMultiRange');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new multi range with no properties. Do not use this
|
||||
* constructor: use one of the goog.dom.Range.createFrom* methods instead.
|
||||
* @constructor
|
||||
* @extends {goog.dom.AbstractRange}
|
||||
*/
|
||||
goog.dom.AbstractMultiRange = function() {
|
||||
};
|
||||
goog.inherits(goog.dom.AbstractMultiRange, goog.dom.AbstractRange);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.AbstractMultiRange.prototype.containsRange = function(
|
||||
otherRange, opt_allowPartial) {
|
||||
// TODO(user): This will incorrectly return false if two (or more) adjacent
|
||||
// elements are both in the control range, and are also in the text range
|
||||
// being compared to.
|
||||
var ranges = this.getTextRanges();
|
||||
var otherRanges = otherRange.getTextRanges();
|
||||
|
||||
var fn = opt_allowPartial ? goog.array.some : goog.array.every;
|
||||
return fn(otherRanges, function(otherRange) {
|
||||
return goog.array.some(ranges, function(range) {
|
||||
return range.containsRange(otherRange, opt_allowPartial);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.AbstractMultiRange.prototype.insertNode = function(node, before) {
|
||||
if (before) {
|
||||
goog.dom.insertSiblingBefore(node, this.getStartNode());
|
||||
} else {
|
||||
goog.dom.insertSiblingAfter(node, this.getEndNode());
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.AbstractMultiRange.prototype.surroundWithNodes = function(startNode,
|
||||
endNode) {
|
||||
this.insertNode(startNode, true);
|
||||
this.insertNode(endNode, false);
|
||||
};
|
||||
@@ -0,0 +1,529 @@
|
||||
// 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 Interface definitions for working with ranges
|
||||
* in HTML documents.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.AbstractRange');
|
||||
goog.provide('goog.dom.RangeIterator');
|
||||
goog.provide('goog.dom.RangeType');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.SavedCaretRange');
|
||||
goog.require('goog.dom.TagIterator');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Types of ranges.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.RangeType = {
|
||||
TEXT: 'text',
|
||||
CONTROL: 'control',
|
||||
MULTI: 'mutli'
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new selection with no properties. Do not use this constructor -
|
||||
* use one of the goog.dom.Range.from* methods instead.
|
||||
* @constructor
|
||||
*/
|
||||
goog.dom.AbstractRange = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the browser native selection object from the given window.
|
||||
* @param {Window} win The window to get the selection object from.
|
||||
* @return {Object} The browser native selection object, or null if it could
|
||||
* not be retrieved.
|
||||
*/
|
||||
goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) {
|
||||
if (win.getSelection) {
|
||||
// W3C
|
||||
return win.getSelection();
|
||||
} else {
|
||||
// IE
|
||||
var doc = win.document;
|
||||
var sel = doc.selection;
|
||||
if (sel) {
|
||||
// IE has a bug where it sometimes returns a selection from the wrong
|
||||
// document. Catching these cases now helps us avoid problems later.
|
||||
try {
|
||||
var range = sel.createRange();
|
||||
// Only TextRanges have a parentElement method.
|
||||
if (range.parentElement) {
|
||||
if (range.parentElement().document != doc) {
|
||||
return null;
|
||||
}
|
||||
} else if (!range.length ||
|
||||
/** @type {ControlRange} */ (range).item(0).document != doc) {
|
||||
// For ControlRanges, check that the range has items, and that
|
||||
// the first item in the range is in the correct document.
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
// If the selection is in the wrong document, and the wrong document is
|
||||
// in a different domain, IE will throw an exception.
|
||||
return null;
|
||||
}
|
||||
// TODO(user|robbyw) Sometimes IE 6 returns a selection instance
|
||||
// when there is no selection. This object has a 'type' property equals
|
||||
// to 'None' and a typeDetail property bound to undefined. Ideally this
|
||||
// function should not return this instance.
|
||||
return sel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests if the given Object is a controlRange.
|
||||
* @param {Object} range The range object to test.
|
||||
* @return {boolean} Whether the given Object is a controlRange.
|
||||
*/
|
||||
goog.dom.AbstractRange.isNativeControlRange = function(range) {
|
||||
// For now, tests for presence of a control range function.
|
||||
return !!range && !!range.addElement;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.AbstractRange} A clone of this range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.clone = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.RangeType} The type of range represented by this object.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getType = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {Range|TextRange} The native browser range object.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the native browser range object, overwriting any state this range was
|
||||
* storing.
|
||||
* @param {Range|TextRange} nativeRange The native browser range object.
|
||||
* @return {boolean} Whether the given range was accepted. If not, the caller
|
||||
* will need to call goog.dom.Range.createFromBrowserRange to create a new
|
||||
* range object.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The number of text ranges in this range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Get the i-th text range in this range. The behavior is undefined if
|
||||
* i >= getTextRangeCount or i < 0.
|
||||
* @param {number} i The range number to retrieve.
|
||||
* @return {goog.dom.TextRange} The i-th text range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Gets an array of all text ranges this range is comprised of. For non-multi
|
||||
* ranges, returns a single element array containing this.
|
||||
* @return {!Array<goog.dom.TextRange>} Array of text ranges.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getTextRanges = function() {
|
||||
var output = [];
|
||||
for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {
|
||||
output.push(this.getTextRange(i));
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The deepest node that contains the entire range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the deepest element in the tree that contains the entire range.
|
||||
* @return {Element} The deepest element that contains the entire range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getContainerElement = function() {
|
||||
var node = this.getContainer();
|
||||
return /** @type {Element} */ (
|
||||
node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The element or text node the range starts in. For text
|
||||
* ranges, the range comprises all text between the start and end position.
|
||||
* For other types of range, start and end give bounds of the range but
|
||||
* do not imply all nodes in those bounds are selected.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @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.AbstractRange.prototype.getStartOffset = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.math.Coordinate} The coordinate of the selection start node
|
||||
* and offset.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getStartPosition = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The element or text node the range ends in.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @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.AbstractRange.prototype.getEndOffset = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.math.Coordinate} The coordinate of the selection end
|
||||
* node and offset.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getEndPosition = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The element or text node the range is anchored at.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getAnchorNode = function() {
|
||||
return this.isReversed() ? this.getEndNode() : this.getStartNode();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The offset into the node the range is anchored at. For
|
||||
* text nodes, this is an offset into the node value. For elements, this
|
||||
* is an offset into the childNodes array.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getAnchorOffset = function() {
|
||||
return this.isReversed() ? this.getEndOffset() : this.getStartOffset();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The element or text node the range is focused at - i.e. where
|
||||
* the cursor is.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getFocusNode = function() {
|
||||
return this.isReversed() ? this.getStartNode() : this.getEndNode();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The offset into the node the range is focused at - i.e.
|
||||
* where the cursor is. For text nodes, this is an offset into the node
|
||||
* value. For elements, this is an offset into the childNodes array.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getFocusOffset = function() {
|
||||
return this.isReversed() ? this.getStartOffset() : this.getEndOffset();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the selection is reversed.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.isReversed = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Document} The document this selection is a part of.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getDocument = function() {
|
||||
// Using start node in IE was crashing the browser in some cases so use
|
||||
// getContainer for that browser. It's also faster for IE, but still slower
|
||||
// than start node for other browsers so we continue to use getStartNode when
|
||||
// it is not problematic. See bug 1687309.
|
||||
return goog.dom.getOwnerDocument(goog.userAgent.IE ?
|
||||
this.getContainer() : this.getStartNode());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Window} The window this selection is a part of.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getWindow = function() {
|
||||
return goog.dom.getWindow(this.getDocument());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests if this range contains the given range.
|
||||
* @param {goog.dom.AbstractRange} range The range to test.
|
||||
* @param {boolean=} opt_allowPartial If true, the range can be partially
|
||||
* contained in the selection, otherwise the range must be entirely
|
||||
* contained.
|
||||
* @return {boolean} Whether this range contains the given range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Tests if this range contains the given node.
|
||||
* @param {Node} node The node to test for.
|
||||
* @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.AbstractRange.prototype.containsNode = function(node,
|
||||
opt_allowPartial) {
|
||||
return this.containsRange(goog.dom.Range.createFromNodeContents(node),
|
||||
opt_allowPartial);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the range is collapsed.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} The text content of the range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getText = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the HTML fragment this range selects. This is slow on all browsers.
|
||||
* The HTML fragment may not be valid HTML, for instance if the user selects
|
||||
* from a to b inclusively in the following html:
|
||||
*
|
||||
* >div<a>/div<b
|
||||
*
|
||||
* This method will return
|
||||
*
|
||||
* a</div>b
|
||||
*
|
||||
* If you need valid HTML, use {@link #getValidHtml} instead.
|
||||
*
|
||||
* @return {string} HTML fragment of the range, does not include context
|
||||
* containing elements.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* 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.AbstractRange.prototype.getValidHtml = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Returns pastable HTML for this range. This guarantees that any child items
|
||||
* that must have specific ancestors will have them, for instance all TDs will
|
||||
* be contained in a TR in a TBODY in a TABLE and all LIs will be contained in
|
||||
* a UL or OL as appropriate. This is semi-fast on all browsers.
|
||||
* @return {string} Pastable HTML of the range, including context containing
|
||||
* elements.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.getPastableHtml = 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.AbstractRange.prototype.__iterator__ = goog.abstractMethod;
|
||||
|
||||
|
||||
// RANGE ACTIONS
|
||||
|
||||
|
||||
/**
|
||||
* Sets this range as the selection in its window.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.select = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Removes the contents of the range from the document.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.removeContents = 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.AbstractRange.prototype.insertNode = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Replaces the range contents with (possibly a copy of) the given node. The
|
||||
* range may be disrupted beyond recovery because of the way this splits nodes.
|
||||
* @param {Node} node The node to insert.
|
||||
* @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.AbstractRange.prototype.replaceContentsWithNode = function(node) {
|
||||
if (!this.isCollapsed()) {
|
||||
this.removeContents();
|
||||
}
|
||||
|
||||
return this.insertNode(node, true);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;
|
||||
|
||||
|
||||
// SAVE/RESTORE
|
||||
|
||||
|
||||
/**
|
||||
* Saves the range so that if the start and end nodes are left alone, it can
|
||||
* be restored.
|
||||
* @return {!goog.dom.SavedRange} A range representation that can be restored
|
||||
* as long as the endpoint nodes of the selection are not modified.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Saves the range using HTML carets. As long as the carets remained in the
|
||||
* HTML, the range can be restored...even when the HTML is copied across
|
||||
* documents.
|
||||
* @return {goog.dom.SavedCaretRange?} A range representation that can be
|
||||
* restored as long as carets are not removed. Returns null if carets
|
||||
* could not be created.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.saveUsingCarets = function() {
|
||||
return (this.getStartNode() && this.getEndNode()) ?
|
||||
new goog.dom.SavedCaretRange(this) : null;
|
||||
};
|
||||
|
||||
|
||||
// RANGE MODIFICATION
|
||||
|
||||
|
||||
/**
|
||||
* Collapses the range to one of its boundary points.
|
||||
* @param {boolean} toAnchor Whether to collapse to the anchor of the range.
|
||||
*/
|
||||
goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod;
|
||||
|
||||
// RANGE ITERATION
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Subclass of goog.dom.TagIterator that iterates over a DOM range. It
|
||||
* adds functions to determine the portion of each text node that is selected.
|
||||
* @param {Node} node The node to start traversal at. When null, creates an
|
||||
* empty iterator.
|
||||
* @param {boolean=} opt_reverse Whether to traverse nodes in reverse.
|
||||
* @constructor
|
||||
* @extends {goog.dom.TagIterator}
|
||||
*/
|
||||
goog.dom.RangeIterator = function(node, opt_reverse) {
|
||||
goog.dom.TagIterator.call(this, node, opt_reverse, true);
|
||||
};
|
||||
goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator);
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The offset into the current node, or -1 if the current node
|
||||
* is not a text node.
|
||||
*/
|
||||
goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The end offset into the current node, or -1 if the current
|
||||
* node is not a text node.
|
||||
*/
|
||||
goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} node The iterator's start node.
|
||||
*/
|
||||
goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {Node} The iterator's end node.
|
||||
*/
|
||||
goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether a call to next will fail.
|
||||
*/
|
||||
goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod;
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2009 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.abstractrange
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.dom.AbstractRangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="javascript:"<html><body contenteditable>asdf""
|
||||
id="a">
|
||||
</iframe>
|
||||
<iframe src="javascript:"<html><body contenteditable>asdf""
|
||||
id="b">
|
||||
</iframe>
|
||||
<iframe src="javascript:"<html><body contenteditable><img>""
|
||||
id="c">
|
||||
</iframe>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.dom.AbstractRangeTest');
|
||||
goog.setTestOnly('goog.dom.AbstractRangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
function testCorrectDocument() {
|
||||
var a = goog.dom.getElement('a').contentWindow;
|
||||
var b = goog.dom.getElement('b').contentWindow;
|
||||
|
||||
a.document.body.focus();
|
||||
var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(a);
|
||||
assertNotNull('Selection must not be null', selection);
|
||||
var range = goog.dom.Range.createFromBrowserSelection(selection);
|
||||
assertEquals('getBrowserSelectionForWindow must return selection in the ' +
|
||||
'correct document', a.document, range.getDocument());
|
||||
|
||||
// This is intended to trip up Internet Explorer --
|
||||
// see http://b/2048934
|
||||
b.document.body.focus();
|
||||
selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(a);
|
||||
// Some (non-IE) browsers keep a separate selection state for each document
|
||||
// in the same browser window. That's fine, as long as the selection object
|
||||
// requested from the window object is correctly associated with that
|
||||
// window's document.
|
||||
if (selection != null && selection.rangeCount != 0) {
|
||||
range = goog.dom.Range.createFromBrowserSelection(selection);
|
||||
assertEquals('getBrowserSelectionForWindow must return selection in ' +
|
||||
'the correct document', a.document, range.getDocument());
|
||||
} else {
|
||||
assertTrue(selection == null || selection.rangeCount == 0);
|
||||
}
|
||||
}
|
||||
|
||||
function testSelectionIsControlRange() {
|
||||
var c = goog.dom.getElement('c').contentWindow;
|
||||
// Only IE supports control ranges
|
||||
if (c.document.body.createControlRange) {
|
||||
var controlRange = c.document.body.createControlRange();
|
||||
controlRange.add(c.document.getElementsByTagName('img')[0]);
|
||||
controlRange.select();
|
||||
var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(c);
|
||||
assertNotNull('Selection must not be null', selection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
// Copyright 2014 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 goog.dom.animationFrame permits work to be done in-sync with
|
||||
* the render refresh rate of the browser and to divide work up globally based
|
||||
* on whether the intent is to measure or to mutate the DOM. The latter avoids
|
||||
* repeated style recalculation which can be really slow.
|
||||
*
|
||||
* Goals of the API:
|
||||
* <ul>
|
||||
* <li>Make it easy to schedule work for the next animation frame.
|
||||
* <li>Make it easy to only do work once per animation frame, even if two
|
||||
* events fire that trigger the same work.
|
||||
* <li>Make it easy to do all work in two phases to avoid repeated style
|
||||
* recalculation caused by interleaved reads and writes.
|
||||
* <li>Avoid creating closures per schedule operation.
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* Programmatic:
|
||||
* <pre>
|
||||
* var animationTask = goog.dom.animationFrame.createTask({
|
||||
* measure: function(state) {
|
||||
* state.width = goog.style.getSize(elem).width;
|
||||
* this.animationTask();
|
||||
* },
|
||||
* mutate: function(state) {
|
||||
* goog.style.setWidth(elem, Math.floor(state.width / 2));
|
||||
* }
|
||||
* }, this);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* See also
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.animationFrame');
|
||||
goog.provide('goog.dom.animationFrame.Spec');
|
||||
goog.provide('goog.dom.animationFrame.State');
|
||||
|
||||
goog.require('goog.dom.animationFrame.polyfill');
|
||||
|
||||
// Install the polyfill.
|
||||
goog.dom.animationFrame.polyfill.install();
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: number,
|
||||
* fn: !Function,
|
||||
* context: (!Object|undefined)
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.animationFrame.Task_;
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* measureTask: goog.dom.animationFrame.Task_,
|
||||
* mutateTask: goog.dom.animationFrame.Task_,
|
||||
* state: (!Object|undefined),
|
||||
* args: (!Array|undefined),
|
||||
* isScheduled: boolean
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.animationFrame.TaskSet_;
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* measure: (!Function|undefined),
|
||||
* mutate: (!Function|undefined)
|
||||
* }}
|
||||
*/
|
||||
goog.dom.animationFrame.Spec;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A type to represent state. Users may add properties as desired.
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.dom.animationFrame.State = function() {};
|
||||
|
||||
|
||||
/**
|
||||
* Saves a set of tasks to be executed in the next requestAnimationFrame phase.
|
||||
* This list is initialized once before any event firing occurs. It is not
|
||||
* affected by the fired events or the requestAnimationFrame processing (unless
|
||||
* a new event is created during the processing).
|
||||
* @private {!Array<!Array<goog.dom.animationFrame.TaskSet_>>}
|
||||
*/
|
||||
goog.dom.animationFrame.tasks_ = [[], []];
|
||||
|
||||
|
||||
/**
|
||||
* Values are 0 or 1, for whether the first or second array should be used to
|
||||
* lookup or add tasks.
|
||||
* @private {number}
|
||||
*/
|
||||
goog.dom.animationFrame.doubleBufferIndex_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Whether we have already requested an animation frame that hasn't happened
|
||||
* yet.
|
||||
* @private {boolean}
|
||||
*/
|
||||
goog.dom.animationFrame.requestedFrame_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Counter to generate IDs for tasks.
|
||||
* @private {number}
|
||||
*/
|
||||
goog.dom.animationFrame.taskId_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the animationframe runTasks_ loop is currently running.
|
||||
* @private {boolean}
|
||||
*/
|
||||
goog.dom.animationFrame.running_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Returns a function that schedules the two passed-in functions to be run upon
|
||||
* the next animation frame. Calling the function again during the same
|
||||
* animation frame does nothing.
|
||||
*
|
||||
* The function under the "measure" key will run first and together with all
|
||||
* other functions scheduled under this key and the function under "mutate" will
|
||||
* run after that.
|
||||
*
|
||||
* @param {{
|
||||
* measure: (function(this:THIS, !goog.dom.animationFrame.State)|undefined),
|
||||
* mutate: (function(this:THIS, !goog.dom.animationFrame.State)|undefined)
|
||||
* }} spec
|
||||
* @param {THIS=} opt_context Context in which to run the function.
|
||||
* @return {function(...?)}
|
||||
* @template THIS
|
||||
*/
|
||||
goog.dom.animationFrame.createTask = function(spec, opt_context) {
|
||||
var genericSpec = /** @type {!goog.dom.animationFrame.Spec} */ (spec);
|
||||
var id = goog.dom.animationFrame.taskId_++;
|
||||
var measureTask = {
|
||||
id: id,
|
||||
fn: spec.measure,
|
||||
context: opt_context
|
||||
};
|
||||
var mutateTask = {
|
||||
id: id,
|
||||
fn: spec.mutate,
|
||||
context: opt_context
|
||||
};
|
||||
|
||||
var taskSet = {
|
||||
measureTask: measureTask,
|
||||
mutateTask: mutateTask,
|
||||
state: {},
|
||||
args: undefined,
|
||||
isScheduled: false
|
||||
};
|
||||
|
||||
return function() {
|
||||
// Default the context to the one that was used to call the tasks scheduler
|
||||
// (this function).
|
||||
if (!opt_context) {
|
||||
measureTask.context = this;
|
||||
mutateTask.context = this;
|
||||
}
|
||||
|
||||
// Save args and state.
|
||||
if (arguments.length > 0) {
|
||||
// The state argument goes last. That is kinda horrible but compatible
|
||||
// with {@see wiz.async.method}.
|
||||
if (!taskSet.args) {
|
||||
taskSet.args = [];
|
||||
}
|
||||
taskSet.args.length = 0;
|
||||
taskSet.args.push.apply(taskSet.args, arguments);
|
||||
taskSet.args.push(taskSet.state);
|
||||
} else {
|
||||
if (!taskSet.args || taskSet.args.length == 0) {
|
||||
taskSet.args = [taskSet.state];
|
||||
} else {
|
||||
taskSet.args[0] = taskSet.state;
|
||||
taskSet.args.length = 1;
|
||||
}
|
||||
}
|
||||
if (!taskSet.isScheduled) {
|
||||
taskSet.isScheduled = true;
|
||||
var tasksArray = goog.dom.animationFrame.tasks_[
|
||||
goog.dom.animationFrame.doubleBufferIndex_];
|
||||
tasksArray.push(taskSet);
|
||||
}
|
||||
goog.dom.animationFrame.requestAnimationFrame_();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Run scheduled tasks.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.animationFrame.runTasks_ = function() {
|
||||
goog.dom.animationFrame.running_ = true;
|
||||
goog.dom.animationFrame.requestedFrame_ = false;
|
||||
var tasksArray = goog.dom.animationFrame
|
||||
.tasks_[goog.dom.animationFrame.doubleBufferIndex_];
|
||||
var taskLength = tasksArray.length;
|
||||
|
||||
// During the runTasks_, if there is a recursive call to queue up more
|
||||
// task(s) for the next frame, we use double-buffering for that.
|
||||
goog.dom.animationFrame.doubleBufferIndex_ =
|
||||
(goog.dom.animationFrame.doubleBufferIndex_ + 1) % 2;
|
||||
|
||||
var task;
|
||||
|
||||
// Run all the measure tasks first.
|
||||
for (var i = 0; i < taskLength; ++i) {
|
||||
task = tasksArray[i];
|
||||
var measureTask = task.measureTask;
|
||||
task.isScheduled = false;
|
||||
if (measureTask.fn) {
|
||||
// TODO (perumaal): Handle any exceptions thrown by the lambda.
|
||||
measureTask.fn.apply(measureTask.context, task.args);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the mutate tasks next.
|
||||
for (var i = 0; i < taskLength; ++i) {
|
||||
task = tasksArray[i];
|
||||
var mutateTask = task.mutateTask;
|
||||
task.isScheduled = false;
|
||||
if (mutateTask.fn) {
|
||||
// TODO (perumaal): Handle any exceptions thrown by the lambda.
|
||||
mutateTask.fn.apply(mutateTask.context, task.args);
|
||||
}
|
||||
|
||||
// Clear state for next vsync.
|
||||
task.state = {};
|
||||
}
|
||||
|
||||
// Clear the tasks array as we have finished processing all the tasks.
|
||||
tasksArray.length = 0;
|
||||
goog.dom.animationFrame.running_ = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the animationframe is currently running. For use
|
||||
* by callers who need not to delay tasks scheduled during runTasks_ for an
|
||||
* additional frame.
|
||||
*/
|
||||
goog.dom.animationFrame.isRunning = function() {
|
||||
return goog.dom.animationFrame.running_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Request {@see goog.dom.animationFrame.runTasks_} to be called upon the
|
||||
* next animation frame if we haven't done so already.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.animationFrame.requestAnimationFrame_ = function() {
|
||||
if (goog.dom.animationFrame.requestedFrame_) {
|
||||
return;
|
||||
}
|
||||
goog.dom.animationFrame.requestedFrame_ = true;
|
||||
window.requestAnimationFrame(goog.dom.animationFrame.runTasks_);
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright 2014 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 Tests for goog.dom.animationFrame.
|
||||
*/
|
||||
|
||||
goog.setTestOnly();
|
||||
|
||||
goog.require('goog.dom.animationFrame');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
|
||||
var NEXT_FRAME = goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT;
|
||||
var mockClock;
|
||||
var t0, t1;
|
||||
var result;
|
||||
|
||||
function setUp() {
|
||||
mockClock = new goog.testing.MockClock(true);
|
||||
result = '';
|
||||
t0 = goog.dom.animationFrame.createTask({
|
||||
measure: function() {
|
||||
result += 'me0';
|
||||
},
|
||||
mutate: function() {
|
||||
result += 'mu0';
|
||||
}
|
||||
});
|
||||
t1 = goog.dom.animationFrame.createTask({
|
||||
measure: function() {
|
||||
result += 'me1';
|
||||
},
|
||||
mutate: function() {
|
||||
result += 'mu1';
|
||||
}
|
||||
});
|
||||
assertEquals('', result);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
mockClock.dispose();
|
||||
}
|
||||
|
||||
function testCreateTask_one() {
|
||||
t0();
|
||||
assertEquals('', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0mu0', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0mu0', result);
|
||||
t0();
|
||||
t0(); // Should do nothing.
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0mu0me0mu0', result);
|
||||
}
|
||||
|
||||
function testCreateTask_onlyMutate() {
|
||||
t0 = goog.dom.animationFrame.createTask({
|
||||
mutate: function() {
|
||||
result += 'mu0';
|
||||
}
|
||||
});
|
||||
t0();
|
||||
assertEquals('', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('mu0', result);
|
||||
}
|
||||
|
||||
function testCreateTask_onlyMeasure() {
|
||||
t0 = goog.dom.animationFrame.createTask({
|
||||
mutate: function() {
|
||||
result += 'me0';
|
||||
}
|
||||
});
|
||||
t0();
|
||||
assertEquals('', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0', result);
|
||||
}
|
||||
|
||||
function testCreateTask_two() {
|
||||
t0();
|
||||
t1();
|
||||
assertEquals('', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0me1mu0mu1', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0me1mu0mu1', result);
|
||||
t0();
|
||||
t1();
|
||||
t0();
|
||||
t1();
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('me0me1mu0mu1me0me1mu0mu1', result);
|
||||
}
|
||||
|
||||
function testCreateTask_recurse() {
|
||||
var stop = false;
|
||||
var recurse = goog.dom.animationFrame.createTask({
|
||||
measure: function() {
|
||||
if (!stop) {
|
||||
recurse();
|
||||
}
|
||||
result += 're0';
|
||||
},
|
||||
mutate: function() {
|
||||
result += 'ru0';
|
||||
}
|
||||
});
|
||||
recurse();
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0re0ru0', result);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0re0ru0re0ru0', result);
|
||||
t0();
|
||||
stop = true;
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0re0ru0re0ru0re0me0ru0mu0', result);
|
||||
|
||||
// Recursion should have stopped now.
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0re0ru0re0ru0re0me0ru0mu0', result);
|
||||
assertFalse(goog.dom.animationFrame.requestedFrame_);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('re0ru0re0ru0re0ru0re0me0ru0mu0', result);
|
||||
assertFalse(goog.dom.animationFrame.requestedFrame_);
|
||||
}
|
||||
|
||||
function testCreateTask_recurseTwoMethodsWithState() {
|
||||
var stop = false;
|
||||
var recurse1 = goog.dom.animationFrame.createTask({
|
||||
measure: function(state) {
|
||||
if (!stop) {
|
||||
recurse2();
|
||||
}
|
||||
result += 'r1e0';
|
||||
state.text = 'T0';
|
||||
},
|
||||
mutate: function(state) {
|
||||
result += 'r1u0' + state.text;
|
||||
}
|
||||
});
|
||||
var recurse2 = goog.dom.animationFrame.createTask({
|
||||
measure: function(state) {
|
||||
if (!stop) {
|
||||
recurse1();
|
||||
}
|
||||
result += 'r2e0';
|
||||
state.text = 'T1';
|
||||
},
|
||||
mutate: function(state) {
|
||||
result += 'r2u0' + state.text;
|
||||
}
|
||||
});
|
||||
|
||||
var taskLength = goog.dom.animationFrame.tasks_[0].length;
|
||||
|
||||
recurse1();
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
// Only recurse1 executed.
|
||||
assertEquals('r1e0r1u0T0', result);
|
||||
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
// Recurse2 executed and queueup recurse1.
|
||||
assertEquals('r1e0r1u0T0r2e0r2u0T1', result);
|
||||
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
// Recurse1 executed and queueup recurse2.
|
||||
assertEquals('r1e0r1u0T0r2e0r2u0T1r1e0r1u0T0', result);
|
||||
|
||||
stop = true;
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
// Recurse2 executed and should have stopped.
|
||||
assertEquals('r1e0r1u0T0r2e0r2u0T1r1e0r1u0T0r2e0r2u0T1', result);
|
||||
assertFalse(goog.dom.animationFrame.requestedFrame_);
|
||||
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('r1e0r1u0T0r2e0r2u0T1r1e0r1u0T0r2e0r2u0T1', result);
|
||||
assertFalse(goog.dom.animationFrame.requestedFrame_);
|
||||
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('r1e0r1u0T0r2e0r2u0T1r1e0r1u0T0r2e0r2u0T1', result);
|
||||
assertFalse(goog.dom.animationFrame.requestedFrame_);
|
||||
}
|
||||
|
||||
function testCreateTask_args() {
|
||||
var context = {context: true};
|
||||
var s = goog.dom.animationFrame.createTask({
|
||||
measure: function(state) {
|
||||
assertEquals(context, this);
|
||||
assertUndefined(state.foo);
|
||||
state.foo = 'foo';
|
||||
},
|
||||
mutate: function(state) {
|
||||
assertEquals(context, this);
|
||||
result += state.foo;
|
||||
}
|
||||
}, context);
|
||||
s();
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('foo', result);
|
||||
|
||||
var dynamicContext = goog.dom.animationFrame.createTask({
|
||||
measure: function(state) {
|
||||
assertEquals(context, this);
|
||||
},
|
||||
mutate: function(state) {
|
||||
assertEquals(context, this);
|
||||
result += 'bar';
|
||||
}
|
||||
});
|
||||
dynamicContext.call(context);
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('foobar', result);
|
||||
|
||||
var moreArgs = goog.dom.animationFrame.createTask({
|
||||
measure: function(event, state) {
|
||||
assertEquals(context, this);
|
||||
assertEquals('event', event);
|
||||
state.baz = 'baz';
|
||||
},
|
||||
mutate: function(event, state) {
|
||||
assertEquals('event', event);
|
||||
assertEquals(context, this);
|
||||
result += state.baz;
|
||||
}
|
||||
});
|
||||
moreArgs.call(context, 'event');
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertEquals('foobarbaz', result);
|
||||
}
|
||||
|
||||
function testIsRunning() {
|
||||
var result = '';
|
||||
var task = goog.dom.animationFrame.createTask({
|
||||
measure: function() {
|
||||
result += 'me';
|
||||
assertTrue(goog.dom.animationFrame.isRunning());
|
||||
},
|
||||
mutate: function() {
|
||||
result += 'mu';
|
||||
assertTrue(goog.dom.animationFrame.isRunning());
|
||||
}
|
||||
});
|
||||
task();
|
||||
assertFalse(goog.dom.animationFrame.isRunning());
|
||||
mockClock.tick(NEXT_FRAME);
|
||||
assertFalse(goog.dom.animationFrame.isRunning());
|
||||
assertEquals('memu', result);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2014 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 A polyfill for window.requestAnimationFrame and
|
||||
* window.cancelAnimationFrame.
|
||||
* Code based on https://gist.github.com/paulirish/1579671
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.animationFrame.polyfill');
|
||||
|
||||
|
||||
/**
|
||||
* @define {boolean} If true, will install the requestAnimationFrame polyfill.
|
||||
*/
|
||||
goog.define('goog.dom.animationFrame.polyfill.ENABLED', true);
|
||||
|
||||
|
||||
/**
|
||||
* Installs the requestAnimationFrame (and cancelAnimationFrame) polyfill.
|
||||
*/
|
||||
goog.dom.animationFrame.polyfill.install =
|
||||
goog.dom.animationFrame.polyfill.ENABLED ? function() {
|
||||
var vendors = ['ms', 'moz', 'webkit', 'o'];
|
||||
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||
window.requestAnimationFrame = window[vendors[x] +
|
||||
'RequestAnimationFrame'];
|
||||
window.cancelAnimationFrame = window[vendors[x] +
|
||||
'CancelAnimationFrame'] ||
|
||||
window[vendors[x] + 'CancelRequestAnimationFrame'];
|
||||
}
|
||||
|
||||
if (!window.requestAnimationFrame) {
|
||||
var lastTime = 0;
|
||||
window.requestAnimationFrame = function(callback, element) {
|
||||
var currTime = new Date().getTime();
|
||||
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||||
lastTime = currTime + timeToCall;
|
||||
return window.setTimeout(function() {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
};
|
||||
|
||||
if (!window.cancelAnimationFrame) {
|
||||
window.cancelAnimationFrame = function(id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
}
|
||||
} : goog.nullFunction;
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright 2006 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 Methods for annotating occurrences of query terms in text or
|
||||
* in a DOM tree. Adapted from Gmail code.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.annotate');
|
||||
goog.provide('goog.dom.annotate.AnnotateFn');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.safe');
|
||||
goog.require('goog.html.SafeHtml');
|
||||
|
||||
|
||||
/**
|
||||
* A function that takes:
|
||||
* (1) the number of the term that is "hit",
|
||||
* (2) the HTML (search term) to be annotated,
|
||||
* and returns the annotated term as an HTML.
|
||||
* @typedef {function(number, !goog.html.SafeHtml): !goog.html.SafeHtml}
|
||||
*/
|
||||
goog.dom.annotate.AnnotateFn;
|
||||
|
||||
|
||||
/**
|
||||
* Calls {@code annotateFn} for each occurrence of a search term in text nodes
|
||||
* under {@code node}. Returns the number of hits.
|
||||
*
|
||||
* @param {Node} node A DOM node.
|
||||
* @param {Array<!Array<string|boolean>>} terms
|
||||
* An array of [searchTerm, matchWholeWordOnly] tuples.
|
||||
* The matchWholeWordOnly value is a per-term attribute because some terms
|
||||
* may be CJK, while others are not. (For correctness, matchWholeWordOnly
|
||||
* should always be false for CJK terms.).
|
||||
* @param {goog.dom.annotate.AnnotateFn} annotateFn
|
||||
* @param {*=} opt_ignoreCase Whether to ignore the case of the query
|
||||
* terms when looking for matches.
|
||||
* @param {Array<string>=} opt_classesToSkip Nodes with one of these CSS class
|
||||
* names (and its descendants) will be skipped.
|
||||
* @param {number=} opt_maxMs Number of milliseconds after which this function,
|
||||
* if still annotating, should stop and return.
|
||||
*
|
||||
* @return {boolean} Whether any terms were annotated.
|
||||
*/
|
||||
goog.dom.annotate.annotateTerms = function(node, terms, annotateFn,
|
||||
opt_ignoreCase,
|
||||
opt_classesToSkip,
|
||||
opt_maxMs) {
|
||||
if (opt_ignoreCase) {
|
||||
terms = goog.dom.annotate.lowercaseTerms_(terms);
|
||||
}
|
||||
var stopTime = opt_maxMs > 0 ? goog.now() + opt_maxMs : 0;
|
||||
|
||||
return goog.dom.annotate.annotateTermsInNode_(
|
||||
node, terms, annotateFn, opt_ignoreCase, opt_classesToSkip || [],
|
||||
stopTime, 0);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The maximum recursion depth allowed. Any DOM nodes deeper than this are
|
||||
* ignored.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.MAX_RECURSION_ = 200;
|
||||
|
||||
|
||||
/**
|
||||
* The node types whose descendants should not be affected by annotation.
|
||||
* @private {Array<string>}
|
||||
*/
|
||||
goog.dom.annotate.NODES_TO_SKIP_ = ['SCRIPT', 'STYLE', 'TEXTAREA'];
|
||||
|
||||
|
||||
/**
|
||||
* Recursive helper function.
|
||||
*
|
||||
* @param {Node} node A DOM node.
|
||||
* @param {Array<!Array<string|boolean>>} terms
|
||||
* An array of [searchTerm, matchWholeWordOnly] tuples.
|
||||
* The matchWholeWordOnly value is a per-term attribute because some terms
|
||||
* may be CJK, while others are not. (For correctness, matchWholeWordOnly
|
||||
* should always be false for CJK terms.).
|
||||
* @param {goog.dom.annotate.AnnotateFn} annotateFn
|
||||
* @param {*} ignoreCase Whether to ignore the case of the query terms
|
||||
* when looking for matches.
|
||||
* @param {Array<string>} classesToSkip Nodes with one of these CSS class
|
||||
* names will be skipped (as will their descendants).
|
||||
* @param {number} stopTime Deadline for annotation operation (ignored if 0).
|
||||
* @param {number} recursionLevel How deep this recursive call is; pass the
|
||||
* value 0 in the initial call.
|
||||
* @return {boolean} Whether any terms were annotated.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.annotateTermsInNode_ =
|
||||
function(node, terms, annotateFn, ignoreCase, classesToSkip,
|
||||
stopTime, recursionLevel) {
|
||||
if ((stopTime > 0 && goog.now() >= stopTime) ||
|
||||
recursionLevel > goog.dom.annotate.MAX_RECURSION_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var annotated = false;
|
||||
|
||||
if (node.nodeType == goog.dom.NodeType.TEXT) {
|
||||
var html = goog.dom.annotate.helpAnnotateText_(node.nodeValue, terms,
|
||||
annotateFn, ignoreCase);
|
||||
if (html != null) {
|
||||
// Replace the text with the annotated html. First we put the html into
|
||||
// a temporary node, to get its DOM structure. To avoid adding a wrapper
|
||||
// element as a side effect, we'll only actually use the temporary node's
|
||||
// children.
|
||||
var tempNode = goog.dom.getOwnerDocument(node).createElement('SPAN');
|
||||
goog.dom.safe.setInnerHtml(tempNode, html);
|
||||
|
||||
var parentNode = node.parentNode;
|
||||
var nodeToInsert;
|
||||
while ((nodeToInsert = tempNode.firstChild) != null) {
|
||||
// Each parentNode.insertBefore call removes the inserted node from
|
||||
// tempNode's list of children.
|
||||
parentNode.insertBefore(nodeToInsert, node);
|
||||
}
|
||||
|
||||
parentNode.removeChild(node);
|
||||
annotated = true;
|
||||
}
|
||||
} else if (node.hasChildNodes() &&
|
||||
!goog.array.contains(goog.dom.annotate.NODES_TO_SKIP_,
|
||||
node.tagName)) {
|
||||
var classes = node.className.split(/\s+/);
|
||||
var skip = goog.array.some(classes, function(className) {
|
||||
return goog.array.contains(classesToSkip, className);
|
||||
});
|
||||
|
||||
if (!skip) {
|
||||
++recursionLevel;
|
||||
var curNode = node.firstChild;
|
||||
var numTermsAnnotated = 0;
|
||||
while (curNode) {
|
||||
var nextNode = curNode.nextSibling;
|
||||
var curNodeAnnotated = goog.dom.annotate.annotateTermsInNode_(
|
||||
curNode, terms, annotateFn, ignoreCase, classesToSkip,
|
||||
stopTime, recursionLevel);
|
||||
annotated = annotated || curNodeAnnotated;
|
||||
curNode = nextNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return annotated;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Regular expression that matches non-word characters.
|
||||
*
|
||||
* Performance note: Testing a one-character string using this regex is as fast
|
||||
* as the equivalent string test ("a-zA-Z0-9_".indexOf(c) < 0), give or take a
|
||||
* few percent. (The regex is about 5% faster in IE 6 and about 4% slower in
|
||||
* Firefox 1.5.) If performance becomes critical, it may be better to convert
|
||||
* the character to a numerical char code and check whether it falls in the
|
||||
* word character ranges. A quick test suggests that could be 33% faster.
|
||||
*
|
||||
* @type {RegExp}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.NONWORD_RE_ = /\W/;
|
||||
|
||||
|
||||
/**
|
||||
* Annotates occurrences of query terms in plain text. This process consists of
|
||||
* identifying all occurrences of all query terms, calling a provided function
|
||||
* to get the appropriate replacement HTML for each occurrence, and
|
||||
* HTML-escaping all the text.
|
||||
*
|
||||
* @param {string} text The plain text to be searched.
|
||||
* @param {Array<Array<?>>} terms An array of
|
||||
* [{string} searchTerm, {boolean} matchWholeWordOnly] tuples.
|
||||
* The matchWholeWordOnly value is a per-term attribute because some terms
|
||||
* may be CJK, while others are not. (For correctness, matchWholeWordOnly
|
||||
* should always be false for CJK terms.).
|
||||
* @param {goog.dom.annotate.AnnotateFn} annotateFn
|
||||
* @param {*=} opt_ignoreCase Whether to ignore the case of the query
|
||||
* terms when looking for matches.
|
||||
* @return {goog.html.SafeHtml} The HTML equivalent of {@code text} with terms
|
||||
* annotated, or null if the text did not contain any of the terms.
|
||||
*/
|
||||
goog.dom.annotate.annotateText = function(text, terms, annotateFn,
|
||||
opt_ignoreCase) {
|
||||
if (opt_ignoreCase) {
|
||||
terms = goog.dom.annotate.lowercaseTerms_(terms);
|
||||
}
|
||||
return goog.dom.annotate.helpAnnotateText_(text, terms, annotateFn,
|
||||
opt_ignoreCase);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Annotates occurrences of query terms in plain text. This process consists of
|
||||
* identifying all occurrences of all query terms, calling a provided function
|
||||
* to get the appropriate replacement HTML for each occurrence, and
|
||||
* HTML-escaping all the text.
|
||||
*
|
||||
* @param {string} text The plain text to be searched.
|
||||
* @param {Array<Array<?>>} terms An array of
|
||||
* [{string} searchTerm, {boolean} matchWholeWordOnly] tuples.
|
||||
* If {@code ignoreCase} is true, each search term must already be lowercase.
|
||||
* The matchWholeWordOnly value is a per-term attribute because some terms
|
||||
* may be CJK, while others are not. (For correctness, matchWholeWordOnly
|
||||
* should always be false for CJK terms.).
|
||||
* @param {goog.dom.annotate.AnnotateFn} annotateFn
|
||||
* @param {*} ignoreCase Whether to ignore the case of the query terms
|
||||
* when looking for matches.
|
||||
* @return {goog.html.SafeHtml} The HTML equivalent of {@code text} with terms
|
||||
* annotated, or null if the text did not contain any of the terms.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.helpAnnotateText_ = function(text, terms, annotateFn,
|
||||
ignoreCase) {
|
||||
var hit = false;
|
||||
var resultHtml = null;
|
||||
var textToSearch = ignoreCase ? text.toLowerCase() : text;
|
||||
var textLen = textToSearch.length;
|
||||
var numTerms = terms.length;
|
||||
|
||||
// Each element will be an array of hit positions for the term.
|
||||
var termHits = new Array(numTerms);
|
||||
|
||||
// First collect all the hits into allHits.
|
||||
for (var i = 0; i < numTerms; i++) {
|
||||
var term = terms[i];
|
||||
var hits = [];
|
||||
var termText = term[0];
|
||||
if (termText != '') {
|
||||
var matchWholeWordOnly = term[1];
|
||||
var termLen = termText.length;
|
||||
var pos = 0;
|
||||
// Find each hit for term t and append to termHits.
|
||||
while (pos < textLen) {
|
||||
var hitPos = textToSearch.indexOf(termText, pos);
|
||||
if (hitPos == -1) {
|
||||
break;
|
||||
} else {
|
||||
var prevCharPos = hitPos - 1;
|
||||
var nextCharPos = hitPos + termLen;
|
||||
if (!matchWholeWordOnly ||
|
||||
((prevCharPos < 0 ||
|
||||
goog.dom.annotate.NONWORD_RE_.test(
|
||||
textToSearch.charAt(prevCharPos))) &&
|
||||
(nextCharPos >= textLen ||
|
||||
goog.dom.annotate.NONWORD_RE_.test(
|
||||
textToSearch.charAt(nextCharPos))))) {
|
||||
hits.push(hitPos);
|
||||
hit = true;
|
||||
}
|
||||
pos = hitPos + termLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
termHits[i] = hits;
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
var html = [];
|
||||
var pos = 0;
|
||||
|
||||
while (true) {
|
||||
// First determine which of the n terms is the next hit.
|
||||
var termIndexOfNextHit;
|
||||
var posOfNextHit = -1;
|
||||
|
||||
for (var i = 0; i < numTerms; i++) {
|
||||
var hits = termHits[i];
|
||||
// pull off the position of the next hit of term t
|
||||
// (it's always the first in the array because we're shifting
|
||||
// hits off the front of the array as we process them)
|
||||
// this is the next candidate to consider for the next overall hit
|
||||
if (!goog.array.isEmpty(hits)) {
|
||||
var hitPos = hits[0];
|
||||
|
||||
// Discard any hits embedded in the previous hit.
|
||||
while (hitPos >= 0 && hitPos < pos) {
|
||||
hits.shift();
|
||||
hitPos = goog.array.isEmpty(hits) ? -1 : hits[0];
|
||||
}
|
||||
|
||||
if (hitPos >= 0 && (posOfNextHit < 0 || hitPos < posOfNextHit)) {
|
||||
termIndexOfNextHit = i;
|
||||
posOfNextHit = hitPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quit if there are no more hits.
|
||||
if (posOfNextHit < 0) break;
|
||||
|
||||
// Remove the next hit from our hit list.
|
||||
termHits[termIndexOfNextHit].shift();
|
||||
|
||||
// Append everything from the end of the last hit up to this one.
|
||||
html.push(text.substr(pos, posOfNextHit - pos));
|
||||
|
||||
// Append the annotated term.
|
||||
var termLen = terms[termIndexOfNextHit][0].length;
|
||||
var termHtml = goog.html.SafeHtml.htmlEscape(
|
||||
text.substr(posOfNextHit, termLen));
|
||||
html.push(
|
||||
annotateFn(goog.asserts.assertNumber(termIndexOfNextHit), termHtml));
|
||||
|
||||
pos = posOfNextHit + termLen;
|
||||
}
|
||||
|
||||
// Append everything after the last hit.
|
||||
html.push(text.substr(pos));
|
||||
return goog.html.SafeHtml.concat(html);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Converts terms to lowercase.
|
||||
*
|
||||
* @param {Array<Array<?>>} terms An array of
|
||||
* [{string} searchTerm, {boolean} matchWholeWordOnly] tuples.
|
||||
* @return {!Array<Array<?>>} An array of
|
||||
* [{string} searchTerm, {boolean} matchWholeWordOnly] tuples.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.lowercaseTerms_ = function(terms) {
|
||||
var lowercaseTerms = [];
|
||||
for (var i = 0; i < terms.length; ++i) {
|
||||
var term = terms[i];
|
||||
lowercaseTerms[i] = [term[0].toLowerCase(), term[1]];
|
||||
}
|
||||
return lowercaseTerms;
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.annotate</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.annotateTest');
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.c0 {background-color:#ff0}
|
||||
.c1 {background-color:#0ff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span id="p">Tom & Jerry</span>
|
||||
<table>
|
||||
<tr id="q">
|
||||
<td>This <b>little</b> piggy</td>
|
||||
<td class="s">That little <i>pig</i>gy</td>
|
||||
</tr>
|
||||
<tr id="r">
|
||||
<td>This <b>little</b> piggy</td>
|
||||
<td class="s">That little <i>pig</i>gy</td>
|
||||
</tr>
|
||||
<tr id="t">
|
||||
<td>This <b>little</b> piggy</td>
|
||||
<td class="s">That little <i>Pig</i>gy</td>
|
||||
</tr>
|
||||
<tr id="u">
|
||||
<td>This <b>little</b> piggy</td>
|
||||
<td class="s">That little <i>Pig</i>gy</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div id="o">
|
||||
<object classid="clsid:SAMPLE-UNRECOGNIZED-ID" width="100" height="50">
|
||||
<param name="BorderStyle" value="1" />
|
||||
<param name="MousePointer" value="0" />
|
||||
<param name="Enabled" value="1" />
|
||||
<param name="Min" value="0" />
|
||||
<param name="Max" value="10" />
|
||||
Your browser cannot display this object.
|
||||
</object>
|
||||
</div>
|
||||
|
||||
<script id="script">var variable;</script>
|
||||
<style id="style" type="text/css">.orange{color:orange}</style>
|
||||
<span id="comment"><!-- note --></span>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2006 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.annotateTest');
|
||||
goog.setTestOnly('goog.dom.annotateTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.annotate');
|
||||
goog.require('goog.html.SafeHtml');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var $ = goog.dom.getElement;
|
||||
|
||||
var TEXT = 'This little piggy cried "Wee! Wee! Wee!" all the way home.';
|
||||
|
||||
function doAnnotation(termIndex, termHtml) {
|
||||
return goog.html.SafeHtml.create('span', {'class': 'c' + termIndex},
|
||||
termHtml);
|
||||
}
|
||||
|
||||
// goog.dom.annotate.annotateText tests
|
||||
|
||||
function testAnnotateText() {
|
||||
var terms = [['pig', true]];
|
||||
var html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
assertEquals(null, html);
|
||||
|
||||
terms = [['pig', false]];
|
||||
html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This little <span class="c0">pig</span>gy cried ' +
|
||||
'"Wee! Wee! Wee!" all the way home.', html);
|
||||
|
||||
terms = [[' piggy ', true]];
|
||||
html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
assertEquals(null, html);
|
||||
|
||||
terms = [[' piggy ', false]];
|
||||
html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This little<span class="c0"> piggy </span>cried ' +
|
||||
'"Wee! Wee! Wee!" all the way home.', html);
|
||||
|
||||
terms = [['goose', true], ['piggy', true]];
|
||||
html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This little <span class="c1">piggy</span> cried ' +
|
||||
'"Wee! Wee! Wee!" all the way home.', html);
|
||||
}
|
||||
|
||||
function testAnnotateTextHtmlEscaping() {
|
||||
var terms = [['a', false]];
|
||||
var html = goog.dom.annotate.annotateText('&a', terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('&<span class="c0">a</span>', html);
|
||||
|
||||
terms = [['a', false]];
|
||||
html = goog.dom.annotate.annotateText('a&', terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('<span class="c0">a</span>&', html);
|
||||
|
||||
terms = [['&', false]];
|
||||
html = goog.dom.annotate.annotateText('&', terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('<span class="c0">&</span>', html);
|
||||
}
|
||||
|
||||
function testAnnotateTextIgnoreCase() {
|
||||
var terms = [['wEe', true]];
|
||||
var html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation, true);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This little piggy cried "<span class="c0">Wee</span>! ' +
|
||||
'<span class="c0">Wee</span>! <span class="c0">Wee</span>!' +
|
||||
'" all the way home.', html);
|
||||
|
||||
terms = [['WEE!', true], ['HE', false]];
|
||||
html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation, true);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This little piggy cried "<span class="c0">Wee!</span> ' +
|
||||
'<span class="c0">Wee!</span> <span class="c0">Wee!</span>' +
|
||||
'" all t<span class="c1">he</span> way home.', html);
|
||||
}
|
||||
|
||||
function testAnnotateTextOverlappingTerms() {
|
||||
var terms = [['tt', false], ['little', false]];
|
||||
var html = goog.dom.annotate.annotateText(TEXT, terms, doAnnotation);
|
||||
html = goog.html.SafeHtml.unwrap(html);
|
||||
assertEquals('This <span class="c1">little</span> piggy cried "Wee! ' +
|
||||
'Wee! Wee!" all the way home.', html);
|
||||
}
|
||||
|
||||
// goog.dom.annotate.annotateTerms tests
|
||||
|
||||
function testAnnotateTerms() {
|
||||
var terms = [['pig', true]];
|
||||
assertFalse(goog.dom.annotate.annotateTerms($('p'), terms, doAnnotation));
|
||||
assertEquals('Tom & Jerry', $('p').innerHTML);
|
||||
|
||||
terms = [['Tom', true]];
|
||||
assertTrue(goog.dom.annotate.annotateTerms($('p'), terms, doAnnotation));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('p'));
|
||||
assertEquals(1, spans.length);
|
||||
assertEquals('Tom', spans[0].innerHTML);
|
||||
assertEquals(' & Jerry', spans[0].nextSibling.nodeValue);
|
||||
}
|
||||
|
||||
function testAnnotateTermsInTable() {
|
||||
var terms = [['pig', false]];
|
||||
assertTrue(goog.dom.annotate.annotateTerms($('q'), terms, doAnnotation));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('q'));
|
||||
assertEquals(2, spans.length);
|
||||
assertEquals('pig', spans[0].innerHTML);
|
||||
assertEquals('gy', spans[0].nextSibling.nodeValue);
|
||||
assertEquals('pig', spans[1].innerHTML);
|
||||
assertEquals('I', spans[1].parentNode.tagName);
|
||||
}
|
||||
|
||||
function testAnnotateTermsWithClassExclusions() {
|
||||
var terms = [['pig', false]];
|
||||
var classesToIgnore = ['s'];
|
||||
assertTrue(goog.dom.annotate.annotateTerms($('r'), terms, doAnnotation,
|
||||
false, classesToIgnore));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('r'));
|
||||
assertEquals(1, spans.length);
|
||||
assertEquals('pig', spans[0].innerHTML);
|
||||
assertEquals('gy', spans[0].nextSibling.nodeValue);
|
||||
}
|
||||
|
||||
function testAnnotateTermsIgnoreCase() {
|
||||
var terms1 = [['pig', false]];
|
||||
assertTrue(goog.dom.annotate.annotateTerms(
|
||||
$('t'), terms1, doAnnotation, true));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('t'));
|
||||
assertEquals(2, spans.length);
|
||||
assertEquals('pig', spans[0].innerHTML);
|
||||
assertEquals('gy', spans[0].nextSibling.nodeValue);
|
||||
assertEquals('Pig', spans[1].innerHTML);
|
||||
|
||||
var terms2 = [['Pig', false]];
|
||||
assertTrue(goog.dom.annotate.annotateTerms(
|
||||
$('u'), terms2, doAnnotation, true));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('u'));
|
||||
assertEquals(2, spans.length);
|
||||
assertEquals('pig', spans[0].innerHTML);
|
||||
assertEquals('gy', spans[0].nextSibling.nodeValue);
|
||||
assertEquals('Pig', spans[1].innerHTML);
|
||||
}
|
||||
|
||||
function testAnnotateTermsInObject() {
|
||||
var terms = [['object', true]];
|
||||
assertTrue(goog.dom.annotate.annotateTerms($('o'), terms, doAnnotation));
|
||||
var spans = goog.dom.getElementsByTagNameAndClass('SPAN', 'c0', $('o'));
|
||||
assertEquals(1, spans.length);
|
||||
assertEquals('object', spans[0].innerHTML);
|
||||
}
|
||||
|
||||
function testAnnotateTermsInScript() {
|
||||
var terms = [['variable', true]];
|
||||
assertFalse(goog.dom.annotate.annotateTerms($('script'), terms,
|
||||
doAnnotation));
|
||||
}
|
||||
|
||||
function testAnnotateTermsInStyle() {
|
||||
var terms = [['color', true]];
|
||||
assertFalse(goog.dom.annotate.annotateTerms($('style'), terms,
|
||||
doAnnotation));
|
||||
}
|
||||
|
||||
function testAnnotateTermsInHtmlComment() {
|
||||
var terms = [['note', true]];
|
||||
assertFalse(goog.dom.annotate.annotateTerms($('comment'), terms,
|
||||
doAnnotation));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2010 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 Browser capability checks for the dom package.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.BrowserFeature');
|
||||
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Enum of browser capabilities.
|
||||
* @enum {boolean}
|
||||
*/
|
||||
goog.dom.BrowserFeature = {
|
||||
/**
|
||||
* Whether attributes 'name' and 'type' can be added to an element after it's
|
||||
* created. False in Internet Explorer prior to version 9.
|
||||
*/
|
||||
CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE ||
|
||||
goog.userAgent.isDocumentModeOrHigher(9),
|
||||
|
||||
/**
|
||||
* Whether we can use element.children to access an element's Element
|
||||
* children. Available since Gecko 1.9.1, IE 9. (IE<9 also includes comment
|
||||
* nodes in the collection.)
|
||||
*/
|
||||
CAN_USE_CHILDREN_ATTRIBUTE: !goog.userAgent.GECKO && !goog.userAgent.IE ||
|
||||
goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9) ||
|
||||
goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9.1'),
|
||||
|
||||
/**
|
||||
* Opera, Safari 3, and Internet Explorer 9 all support innerText but they
|
||||
* include text nodes in script and style tags. Not document-mode-dependent.
|
||||
*/
|
||||
CAN_USE_INNER_TEXT: (
|
||||
goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')),
|
||||
|
||||
/**
|
||||
* MSIE, Opera, and Safari>=4 support element.parentElement to access an
|
||||
* element's parent if it is an Element.
|
||||
*/
|
||||
CAN_USE_PARENT_ELEMENT_PROPERTY: goog.userAgent.IE || goog.userAgent.OPERA ||
|
||||
goog.userAgent.WEBKIT,
|
||||
|
||||
/**
|
||||
* Whether NoScope elements need a scoped element written before them in
|
||||
* innerHTML.
|
||||
* MSDN: http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx#1
|
||||
*/
|
||||
INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE,
|
||||
|
||||
/**
|
||||
* Whether we use legacy IE range API.
|
||||
*/
|
||||
LEGACY_IE_RANGES: goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)
|
||||
};
|
||||
@@ -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=<>></div>2', // WebKit
|
||||
'1<div id=<>></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 child. In this case :
|
||||
// nodeRange.htmlText shows ('<p> </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 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 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());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright 2012 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 A viewport size monitor that buffers RESIZE events until the
|
||||
* window size has stopped changing, within a specified period of time. For
|
||||
* every RESIZE event dispatched, this will dispatch up to two *additional*
|
||||
* events:
|
||||
* - {@link #EventType.RESIZE_WIDTH} if the viewport's width has changed since
|
||||
* the last buffered dispatch.
|
||||
* - {@link #EventType.RESIZE_HEIGHT} if the viewport's height has changed since
|
||||
* the last buffered dispatch.
|
||||
* You likely only need to listen to one of the three events. But if you need
|
||||
* more, just be cautious of duplicating effort.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.BufferedViewportSizeMonitor');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.async.Delay');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.EventType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new BufferedViewportSizeMonitor.
|
||||
* @param {!goog.dom.ViewportSizeMonitor} viewportSizeMonitor The
|
||||
* underlying viewport size monitor.
|
||||
* @param {number=} opt_bufferMs The buffer time, in ms. If not specified, this
|
||||
* value defaults to {@link #RESIZE_EVENT_DELAY_MS_}.
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor = function(
|
||||
viewportSizeMonitor, opt_bufferMs) {
|
||||
goog.dom.BufferedViewportSizeMonitor.base(this, 'constructor');
|
||||
|
||||
/**
|
||||
* The underlying viewport size monitor.
|
||||
* @type {goog.dom.ViewportSizeMonitor}
|
||||
* @private
|
||||
*/
|
||||
this.viewportSizeMonitor_ = viewportSizeMonitor;
|
||||
|
||||
/**
|
||||
* The current size of the viewport.
|
||||
* @type {goog.math.Size}
|
||||
* @private
|
||||
*/
|
||||
this.currentSize_ = this.viewportSizeMonitor_.getSize();
|
||||
|
||||
/**
|
||||
* The resize buffer time in ms.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.resizeBufferMs_ = opt_bufferMs ||
|
||||
goog.dom.BufferedViewportSizeMonitor.RESIZE_EVENT_DELAY_MS_;
|
||||
|
||||
/**
|
||||
* Listener key for the viewport size monitor.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
this.listenerKey_ = goog.events.listen(
|
||||
viewportSizeMonitor,
|
||||
goog.events.EventType.RESIZE,
|
||||
this.handleResize_,
|
||||
false,
|
||||
this);
|
||||
};
|
||||
goog.inherits(goog.dom.BufferedViewportSizeMonitor, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* Additional events to dispatch.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.EventType = {
|
||||
RESIZE_HEIGHT: goog.events.getUniqueId('resizeheight'),
|
||||
RESIZE_WIDTH: goog.events.getUniqueId('resizewidth')
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Delay for the resize event.
|
||||
* @type {goog.async.Delay}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.prototype.resizeDelay_;
|
||||
|
||||
|
||||
/**
|
||||
* Default number of milliseconds to wait after a resize event to relayout the
|
||||
* page.
|
||||
* @type {number}
|
||||
* @const
|
||||
* @private
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.RESIZE_EVENT_DELAY_MS_ = 100;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.BufferedViewportSizeMonitor.prototype.disposeInternal =
|
||||
function() {
|
||||
goog.events.unlistenByKey(this.listenerKey_);
|
||||
goog.dom.BufferedViewportSizeMonitor.base(this, 'disposeInternal');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles resize events on the underlying ViewportMonitor.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.prototype.handleResize_ =
|
||||
function() {
|
||||
// Lazily create when needed.
|
||||
if (!this.resizeDelay_) {
|
||||
this.resizeDelay_ = new goog.async.Delay(
|
||||
this.onWindowResize_,
|
||||
this.resizeBufferMs_,
|
||||
this);
|
||||
this.registerDisposable(this.resizeDelay_);
|
||||
}
|
||||
this.resizeDelay_.start();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Window resize callback that determines whether to reflow the view contents.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.prototype.onWindowResize_ =
|
||||
function() {
|
||||
if (this.viewportSizeMonitor_.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var previousSize = this.currentSize_;
|
||||
var currentSize = this.viewportSizeMonitor_.getSize();
|
||||
|
||||
goog.asserts.assert(currentSize,
|
||||
'Viewport size should be set at this point');
|
||||
|
||||
this.currentSize_ = currentSize;
|
||||
|
||||
if (previousSize) {
|
||||
|
||||
var resized = false;
|
||||
|
||||
// Width has changed
|
||||
if (previousSize.width != currentSize.width) {
|
||||
this.dispatchEvent(
|
||||
goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_WIDTH);
|
||||
resized = true;
|
||||
}
|
||||
|
||||
// Height has changed
|
||||
if (previousSize.height != currentSize.height) {
|
||||
this.dispatchEvent(
|
||||
goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_HEIGHT);
|
||||
resized = true;
|
||||
}
|
||||
|
||||
// If either has changed, this is a resize event.
|
||||
if (resized) {
|
||||
this.dispatchEvent(goog.events.EventType.RESIZE);
|
||||
}
|
||||
|
||||
} else {
|
||||
// If we didn't have a previous size, we consider all events to have
|
||||
// changed.
|
||||
this.dispatchEvent(
|
||||
goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_HEIGHT);
|
||||
this.dispatchEvent(
|
||||
goog.dom.BufferedViewportSizeMonitor.EventType.RESIZE_WIDTH);
|
||||
this.dispatchEvent(goog.events.EventType.RESIZE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current size of the viewport.
|
||||
* @return {goog.math.Size?} The current viewport size.
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor.prototype.getSize = function() {
|
||||
return this.currentSize_ ? this.currentSize_.clone() : null;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2012 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>
|
||||
<title>Tests for goog.dom.BufferedViewportSizeMonitor</title>
|
||||
<script type="text/javascript" src="../base.js"></script>
|
||||
<script type="text/javascript">
|
||||
goog.require('goog.dom.BufferedViewportSizeMonitorTest');
|
||||
</script>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2012 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 Tests for goog.dom.BufferedViewportSizeMonitor.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/** @suppress {extraProvide} */
|
||||
goog.provide('goog.dom.BufferedViewportSizeMonitorTest');
|
||||
|
||||
goog.require('goog.dom.BufferedViewportSizeMonitor');
|
||||
goog.require('goog.dom.ViewportSizeMonitor');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.math.Size');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.events.Event');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
goog.setTestOnly('goog.dom.BufferedViewportSizeMonitorTest');
|
||||
|
||||
var RESIZE_DELAY = goog.dom.BufferedViewportSizeMonitor.RESIZE_EVENT_DELAY_MS_;
|
||||
var INITIAL_SIZE = new goog.math.Size(111, 111);
|
||||
|
||||
var mockControl;
|
||||
var viewportSizeMonitor;
|
||||
var bufferedVsm;
|
||||
var timer = new goog.testing.MockClock();
|
||||
var resizeEventCount = 0;
|
||||
var size;
|
||||
|
||||
var resizeCallback = function() {
|
||||
resizeEventCount++;
|
||||
};
|
||||
|
||||
function setUp() {
|
||||
timer.install();
|
||||
|
||||
size = INITIAL_SIZE;
|
||||
viewportSizeMonitor = new goog.dom.ViewportSizeMonitor();
|
||||
viewportSizeMonitor.getSize = function() { return size; };
|
||||
bufferedVsm = new goog.dom.BufferedViewportSizeMonitor(viewportSizeMonitor);
|
||||
|
||||
goog.events.listen(bufferedVsm, goog.events.EventType.RESIZE, resizeCallback);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.events.unlisten(
|
||||
bufferedVsm, goog.events.EventType.RESIZE, resizeCallback);
|
||||
resizeEventCount = 0;
|
||||
timer.uninstall();
|
||||
}
|
||||
|
||||
function testInitialSizes() {
|
||||
assertTrue(goog.math.Size.equals(INITIAL_SIZE, bufferedVsm.getSize()));
|
||||
}
|
||||
|
||||
function testWindowResize() {
|
||||
assertEquals(0, resizeEventCount);
|
||||
resize(100, 100);
|
||||
timer.tick(RESIZE_DELAY - 1);
|
||||
assertEquals(
|
||||
'No resize expected before the delay is fired', 0, resizeEventCount);
|
||||
timer.tick(1);
|
||||
assertEquals('Expected resize after delay', 1, resizeEventCount);
|
||||
assertTrue(goog.math.Size.equals(
|
||||
new goog.math.Size(100, 100), bufferedVsm.getSize()));
|
||||
}
|
||||
|
||||
function testWindowResize_eventBatching() {
|
||||
assertEquals('No resize calls expected before resize events',
|
||||
0, resizeEventCount);
|
||||
resize(100, 100);
|
||||
timer.tick(RESIZE_DELAY - 1);
|
||||
resize(200, 200);
|
||||
assertEquals(
|
||||
'No resize expected before the delay is fired', 0, resizeEventCount);
|
||||
timer.tick(1);
|
||||
assertEquals(
|
||||
'No resize expected when delay is restarted', 0, resizeEventCount);
|
||||
timer.tick(RESIZE_DELAY);
|
||||
assertEquals('Expected resize after delay', 1, resizeEventCount);
|
||||
}
|
||||
|
||||
function testWindowResize_noChange() {
|
||||
resize(100, 100);
|
||||
timer.tick(RESIZE_DELAY);
|
||||
assertEquals(1, resizeEventCount);
|
||||
resize(100, 100);
|
||||
timer.tick(RESIZE_DELAY);
|
||||
assertEquals(
|
||||
'No resize expected when size doesn\'t change', 1, resizeEventCount);
|
||||
assertTrue(goog.math.Size.equals(
|
||||
new goog.math.Size(100, 100), bufferedVsm.getSize()));
|
||||
}
|
||||
|
||||
function testWindowResize_previousSize() {
|
||||
resize(100, 100);
|
||||
timer.tick(RESIZE_DELAY);
|
||||
assertEquals(1, resizeEventCount);
|
||||
assertTrue(goog.math.Size.equals(
|
||||
new goog.math.Size(100, 100), bufferedVsm.getSize()));
|
||||
|
||||
resize(200, 200);
|
||||
timer.tick(RESIZE_DELAY);
|
||||
assertEquals(2, resizeEventCount);
|
||||
assertTrue(goog.math.Size.equals(
|
||||
new goog.math.Size(200, 200), bufferedVsm.getSize()));
|
||||
}
|
||||
|
||||
function resize(width, height) {
|
||||
size = new goog.math.Size(width, height);
|
||||
goog.testing.events.fireBrowserEvent(
|
||||
new goog.testing.events.Event(
|
||||
goog.events.EventType.RESIZE, viewportSizeMonitor));
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for adding, removing and setting classes. Prefer
|
||||
* {@link goog.dom.classlist} over these utilities since goog.dom.classlist
|
||||
* conforms closer to the semantics of Element.classList, is faster (uses
|
||||
* native methods rather than parsing strings on every call) and compiles
|
||||
* to smaller code as a result.
|
||||
*
|
||||
* Note: these utilities are meant to operate on HTMLElements and
|
||||
* will not work on elements with differing interfaces (such as SVGElements).
|
||||
*
|
||||
* @author arv@google.com (Erik Arvidsson)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.classes');
|
||||
|
||||
goog.require('goog.array');
|
||||
|
||||
|
||||
/**
|
||||
* Sets the entire class name of an element.
|
||||
* @param {Node} element DOM node to set class of.
|
||||
* @param {string} className Class name(s) to apply to element.
|
||||
* @deprecated Use goog.dom.classlist.set instead.
|
||||
*/
|
||||
goog.dom.classes.set = function(element, className) {
|
||||
element.className = className;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets an array of class names on an element
|
||||
* @param {Node} element DOM node to get class of.
|
||||
* @return {!Array<?>} Class names on {@code element}. Some browsers add extra
|
||||
* properties to the array. Do not depend on any of these!
|
||||
* @deprecated Use goog.dom.classlist.get instead.
|
||||
*/
|
||||
goog.dom.classes.get = function(element) {
|
||||
var className = element.className;
|
||||
// Some types of elements don't have a className in IE (e.g. iframes).
|
||||
// Furthermore, in Firefox, className is not a string when the element is
|
||||
// an SVG element.
|
||||
return goog.isString(className) && className.match(/\S+/g) || [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a class or classes to an element. Does not add multiples of class names.
|
||||
* @param {Node} element DOM node to add class to.
|
||||
* @param {...string} var_args Class names to add.
|
||||
* @return {boolean} Whether class was added (or all classes were added).
|
||||
* @deprecated Use goog.dom.classlist.add or goog.dom.classlist.addAll instead.
|
||||
*/
|
||||
goog.dom.classes.add = function(element, var_args) {
|
||||
var classes = goog.dom.classes.get(element);
|
||||
var args = goog.array.slice(arguments, 1);
|
||||
var expectedCount = classes.length + args.length;
|
||||
goog.dom.classes.add_(classes, args);
|
||||
goog.dom.classes.set(element, classes.join(' '));
|
||||
return classes.length == expectedCount;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a class or classes from an element.
|
||||
* @param {Node} element DOM node to remove class from.
|
||||
* @param {...string} var_args Class name(s) to remove.
|
||||
* @return {boolean} Whether all classes in {@code var_args} were found and
|
||||
* removed.
|
||||
* @deprecated Use goog.dom.classlist.remove or goog.dom.classlist.removeAll
|
||||
* instead.
|
||||
*/
|
||||
goog.dom.classes.remove = function(element, var_args) {
|
||||
var classes = goog.dom.classes.get(element);
|
||||
var args = goog.array.slice(arguments, 1);
|
||||
var newClasses = goog.dom.classes.getDifference_(classes, args);
|
||||
goog.dom.classes.set(element, newClasses.join(' '));
|
||||
return newClasses.length == classes.length - args.length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for {@link goog.dom.classes.add} and
|
||||
* {@link goog.dom.classes.addRemove}. Adds one or more classes to the supplied
|
||||
* classes array.
|
||||
* @param {Array<string>} classes All class names for the element, will be
|
||||
* updated to have the classes supplied in {@code args} added.
|
||||
* @param {Array<string>} args Class names to add.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.classes.add_ = function(classes, args) {
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (!goog.array.contains(classes, args[i])) {
|
||||
classes.push(args[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for {@link goog.dom.classes.remove} and
|
||||
* {@link goog.dom.classes.addRemove}. Calculates the difference of two arrays.
|
||||
* @param {!Array<string>} arr1 First array.
|
||||
* @param {!Array<string>} arr2 Second array.
|
||||
* @return {!Array<string>} The first array without the elements of the second
|
||||
* array.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.classes.getDifference_ = function(arr1, arr2) {
|
||||
return goog.array.filter(arr1, function(item) {
|
||||
return !goog.array.contains(arr2, item);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Switches a class on an element from one to another without disturbing other
|
||||
* classes. If the fromClass isn't removed, the toClass won't be added.
|
||||
* @param {Node} element DOM node to swap classes on.
|
||||
* @param {string} fromClass Class to remove.
|
||||
* @param {string} toClass Class to add.
|
||||
* @return {boolean} Whether classes were switched.
|
||||
* @deprecated Use goog.dom.classlist.swap instead.
|
||||
*/
|
||||
goog.dom.classes.swap = function(element, fromClass, toClass) {
|
||||
var classes = goog.dom.classes.get(element);
|
||||
|
||||
var removed = false;
|
||||
for (var i = 0; i < classes.length; i++) {
|
||||
if (classes[i] == fromClass) {
|
||||
goog.array.splice(classes, i--, 1);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
classes.push(toClass);
|
||||
goog.dom.classes.set(element, classes.join(' '));
|
||||
}
|
||||
|
||||
return removed;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds zero or more classes to an element and removes zero or more as a single
|
||||
* operation. Unlike calling {@link goog.dom.classes.add} and
|
||||
* {@link goog.dom.classes.remove} separately, this is more efficient as it only
|
||||
* parses the class property once.
|
||||
*
|
||||
* If a class is in both the remove and add lists, it will be added. Thus,
|
||||
* you can use this instead of {@link goog.dom.classes.swap} when you have
|
||||
* more than two class names that you want to swap.
|
||||
*
|
||||
* @param {Node} element DOM node to swap classes on.
|
||||
* @param {?(string|Array<string>)} classesToRemove Class or classes to
|
||||
* remove, if null no classes are removed.
|
||||
* @param {?(string|Array<string>)} classesToAdd Class or classes to add, if
|
||||
* null no classes are added.
|
||||
* @deprecated Use goog.dom.classlist.addRemove instead.
|
||||
*/
|
||||
goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) {
|
||||
var classes = goog.dom.classes.get(element);
|
||||
if (goog.isString(classesToRemove)) {
|
||||
goog.array.remove(classes, classesToRemove);
|
||||
} else if (goog.isArray(classesToRemove)) {
|
||||
classes = goog.dom.classes.getDifference_(classes, classesToRemove);
|
||||
}
|
||||
|
||||
if (goog.isString(classesToAdd) &&
|
||||
!goog.array.contains(classes, classesToAdd)) {
|
||||
classes.push(classesToAdd);
|
||||
} else if (goog.isArray(classesToAdd)) {
|
||||
goog.dom.classes.add_(classes, classesToAdd);
|
||||
}
|
||||
|
||||
goog.dom.classes.set(element, classes.join(' '));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if an element has a class.
|
||||
* @param {Node} element DOM node to test.
|
||||
* @param {string} className Class name to test for.
|
||||
* @return {boolean} Whether element has the class.
|
||||
* @deprecated Use goog.dom.classlist.contains instead.
|
||||
*/
|
||||
goog.dom.classes.has = function(element, className) {
|
||||
return goog.array.contains(goog.dom.classes.get(element), className);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a class depending on the enabled argument.
|
||||
* @param {Node} element DOM node to add or remove the class on.
|
||||
* @param {string} className Class name to add or remove.
|
||||
* @param {boolean} enabled Whether to add or remove the class (true adds,
|
||||
* false removes).
|
||||
* @deprecated Use goog.dom.classlist.enable or goog.dom.classlist.enableAll
|
||||
* instead.
|
||||
*/
|
||||
goog.dom.classes.enable = function(element, className, enabled) {
|
||||
if (enabled) {
|
||||
goog.dom.classes.add(element, className);
|
||||
} else {
|
||||
goog.dom.classes.remove(element, className);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a class if an element has it, and adds it the element doesn't have
|
||||
* it. Won't affect other classes on the node.
|
||||
* @param {Node} element DOM node to toggle class on.
|
||||
* @param {string} className Class to toggle.
|
||||
* @return {boolean} True if class was added, false if it was removed
|
||||
* (in other words, whether element has the class after this function has
|
||||
* been called).
|
||||
* @deprecated Use goog.dom.classlist.toggle instead.
|
||||
*/
|
||||
goog.dom.classes.toggle = function(element, className) {
|
||||
var add = !goog.dom.classes.has(element, className);
|
||||
goog.dom.classes.enable(element, className, add);
|
||||
return add;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.
|
||||
-->
|
||||
<!--
|
||||
This is a copy of classes_test.html but without a doctype. Make sure these two
|
||||
are in sync.
|
||||
-->
|
||||
<head>
|
||||
<title>Closure Unit Tests - goog.dom.classes</title>=
|
||||
<script src="../base.js"></script>
|
||||
<style type="text/css">
|
||||
#styleTest1 {
|
||||
width:120px;font-weight:bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="testEl">
|
||||
<span>Test Element</span>
|
||||
</div>
|
||||
|
||||
<div><div><div id="testEl2"></div></div></div>
|
||||
|
||||
<span id="span1" class="TEST1"></span>
|
||||
<span id="span2" class="TEST1"></span>
|
||||
<span id="span3" class="TEST2"></span>
|
||||
<span id="span4" class="TEST3"></span>
|
||||
<span id="span5" class="TEST1"></span>
|
||||
<span id="span6"></span>
|
||||
|
||||
<p id="p1"></p>
|
||||
|
||||
<div id="styleTest1"></div>
|
||||
<div id="styleTest2" style="width:100px;font-weight:bold"></div>
|
||||
<div id="styleTest3"></div>
|
||||
|
||||
<!-- Paragraph to test element child and sibling -->
|
||||
<p id="p2">
|
||||
<!-- Comment -->
|
||||
a
|
||||
<b id="b1">c</b>
|
||||
d
|
||||
<!-- Comment -->
|
||||
e
|
||||
<b id="b2">f</b>
|
||||
g
|
||||
<!-- Comment -->
|
||||
</p>
|
||||
<p id="p3" class="SOMECLASS
|
||||
OTHERCLASS">
|
||||
h
|
||||
</p>
|
||||
<iframe name="frame" src="../math/math_test.html"></iframe>
|
||||
|
||||
<script>
|
||||
goog.require('goog.dom.classes_test');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.
|
||||
-->
|
||||
<!--
|
||||
When changing this, make sure that classes_quirks_test.html is kept in sync.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom.classes</title>
|
||||
<script src="../base.js"></script>
|
||||
<style type="text/css">
|
||||
#styleTest1 {
|
||||
width:120px;font-weight:bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="testEl">
|
||||
<span>Test Element</span>
|
||||
</div>
|
||||
|
||||
<div><div><div id="testEl2"></div></div></div>
|
||||
|
||||
<span id="span1" class="TEST1"></span>
|
||||
<span id="span2" class="TEST1"></span>
|
||||
<span id="span3" class="TEST2"></span>
|
||||
<span id="span4" class="TEST3"></span>
|
||||
<span id="span5" class="TEST1"></span>
|
||||
<span id="span6"></span>
|
||||
|
||||
<p id="p1"></p>
|
||||
|
||||
<div id="styleTest1"></div>
|
||||
<div id="styleTest2" style="width:100px;font-weight:bold"></div>
|
||||
<div id="styleTest3"></div>
|
||||
|
||||
<!-- Paragraph to test element child and sibling -->
|
||||
<p id="p2">
|
||||
<!-- Comment -->
|
||||
a
|
||||
<b id="b1">c</b>
|
||||
d
|
||||
<!-- Comment -->
|
||||
e
|
||||
<b id="b2">f</b>
|
||||
g
|
||||
<!-- Comment -->
|
||||
</p>
|
||||
<p id="p3" class="SOMECLASS
|
||||
OTHERCLASS">
|
||||
h
|
||||
</p>
|
||||
<iframe name="frame" src="../math/math_test.html"></iframe>
|
||||
|
||||
<script>
|
||||
goog.require('goog.dom.classes_test');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright 2008 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 Shared code for classes_test.html & classes_quirks_test.html.
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.classes_test');
|
||||
goog.setTestOnly('goog.dom.classes_test');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classes');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
|
||||
var classes = goog.dom.classes;
|
||||
|
||||
function testGet() {
|
||||
var el = document.createElement('div');
|
||||
assertArrayEquals([], goog.dom.classes.get(el));
|
||||
el.className = 'C';
|
||||
assertArrayEquals(['C'], goog.dom.classes.get(el));
|
||||
el.className = 'C D';
|
||||
assertArrayEquals(['C', 'D'], goog.dom.classes.get(el));
|
||||
el.className = 'C\nD';
|
||||
assertArrayEquals(['C', 'D'], goog.dom.classes.get(el));
|
||||
el.className = ' C ';
|
||||
assertArrayEquals(['C'], goog.dom.classes.get(el));
|
||||
}
|
||||
|
||||
function testSetAddHasRemove() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.set(el, 'SOMECLASS');
|
||||
assertTrue('Should have SOMECLASS', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.set(el, 'OTHERCLASS');
|
||||
assertTrue('Should have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertFalse('Should not have SOMECLASS', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.add(el, 'WOOCLASS');
|
||||
assertTrue('Should have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertTrue('Should have WOOCLASS', classes.has(el, 'WOOCLASS'));
|
||||
|
||||
classes.add(el, 'ACLASS', 'BCLASS', 'CCLASS');
|
||||
assertTrue('Should have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertTrue('Should have WOOCLASS', classes.has(el, 'WOOCLASS'));
|
||||
assertTrue('Should have ACLASS', classes.has(el, 'ACLASS'));
|
||||
assertTrue('Should have BCLASS', classes.has(el, 'BCLASS'));
|
||||
assertTrue('Should have CCLASS', classes.has(el, 'CCLASS'));
|
||||
|
||||
classes.remove(el, 'CCLASS');
|
||||
assertTrue('Should have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertTrue('Should have WOOCLASS', classes.has(el, 'WOOCLASS'));
|
||||
assertTrue('Should have ACLASS', classes.has(el, 'ACLASS'));
|
||||
assertTrue('Should have BCLASS', classes.has(el, 'BCLASS'));
|
||||
assertFalse('Should not have CCLASS', classes.has(el, 'CCLASS'));
|
||||
|
||||
classes.remove(el, 'ACLASS', 'BCLASS');
|
||||
assertTrue('Should have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertTrue('Should have WOOCLASS', classes.has(el, 'WOOCLASS'));
|
||||
assertFalse('Should not have ACLASS', classes.has(el, 'ACLASS'));
|
||||
assertFalse('Should not have BCLASS', classes.has(el, 'BCLASS'));
|
||||
}
|
||||
|
||||
// While support for this isn't implied in the method documentation,
|
||||
// this is a frequently used pattern.
|
||||
function testAddWithSpacesInClassName() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.add(el, 'CLASS1 CLASS2', 'CLASS3 CLASS4');
|
||||
assertTrue('Should have CLASS1', classes.has(el, 'CLASS1'));
|
||||
assertTrue('Should have CLASS2', classes.has(el, 'CLASS2'));
|
||||
assertTrue('Should have CLASS3', classes.has(el, 'CLASS3'));
|
||||
assertTrue('Should have CLASS4', classes.has(el, 'CLASS4'));
|
||||
}
|
||||
|
||||
function testSwap() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'SOMECLASS'));
|
||||
assertFalse('Should not have second class', classes.has(el, 'second'));
|
||||
|
||||
classes.swap(el, 'FIRST', 'second');
|
||||
|
||||
assertFalse('Should not have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'SOMECLASS'));
|
||||
assertTrue('Should have second class', classes.has(el, 'second'));
|
||||
|
||||
classes.swap(el, 'second', 'FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'SOMECLASS'));
|
||||
assertFalse('Should not have second class', classes.has(el, 'second'));
|
||||
}
|
||||
|
||||
function testEnable() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.enable(el, 'FIRST', false);
|
||||
|
||||
assertFalse('Should not have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.enable(el, 'FIRST', true);
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
}
|
||||
|
||||
function testToggle() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.toggle(el, 'FIRST');
|
||||
|
||||
assertFalse('Should not have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
|
||||
classes.toggle(el, 'FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classes.has(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class', classes.has(el, 'SOMECLASS'));
|
||||
}
|
||||
|
||||
function testAddNotAddingMultiples() {
|
||||
var el = goog.dom.getElement('span6');
|
||||
assertTrue(classes.add(el, 'A'));
|
||||
assertEquals('A', el.className);
|
||||
assertFalse(classes.add(el, 'A'));
|
||||
assertEquals('A', el.className);
|
||||
assertFalse(classes.add(el, 'B', 'B'));
|
||||
assertEquals('A B', el.className);
|
||||
}
|
||||
|
||||
function testAddRemoveString() {
|
||||
var el = goog.dom.getElement('span6');
|
||||
el.className = 'A';
|
||||
|
||||
goog.dom.classes.addRemove(el, 'A', 'B');
|
||||
assertEquals('B', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, null, 'C');
|
||||
assertEquals('B C', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, 'C', 'D');
|
||||
assertEquals('B D', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, 'D', null);
|
||||
assertEquals('B', el.className);
|
||||
}
|
||||
|
||||
function testAddRemoveArray() {
|
||||
var el = goog.dom.getElement('span6');
|
||||
el.className = 'A';
|
||||
|
||||
goog.dom.classes.addRemove(el, ['A'], ['B']);
|
||||
assertEquals('B', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, [], ['C']);
|
||||
assertEquals('B C', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, ['C'], ['D']);
|
||||
assertEquals('B D', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, ['D'], []);
|
||||
assertEquals('B', el.className);
|
||||
}
|
||||
|
||||
function testAddRemoveMultiple() {
|
||||
var el = goog.dom.getElement('span6');
|
||||
el.className = 'A';
|
||||
|
||||
goog.dom.classes.addRemove(el, ['A'], ['B', 'C', 'D']);
|
||||
assertEquals('B C D', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, [], ['E', 'F']);
|
||||
assertEquals('B C D E F', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, ['C', 'E'], []);
|
||||
assertEquals('B D F', el.className);
|
||||
|
||||
goog.dom.classes.addRemove(el, ['B'], ['G']);
|
||||
assertEquals('D F G', el.className);
|
||||
}
|
||||
|
||||
// While support for this isn't implied in the method documentation,
|
||||
// this is a frequently used pattern.
|
||||
function testAddRemoveWithSpacesInClassName() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classes.addRemove(el, '', 'CLASS1 CLASS2');
|
||||
assertTrue('Should have CLASS1', classes.has(el, 'CLASS1'));
|
||||
assertTrue('Should have CLASS2', classes.has(el, 'CLASS2'));
|
||||
}
|
||||
|
||||
function testHasWithNewlines() {
|
||||
var el = goog.dom.getElement('p3');
|
||||
assertTrue('Should have SOMECLASS', classes.has(el, 'SOMECLASS'));
|
||||
assertTrue('Should also have OTHERCLASS', classes.has(el, 'OTHERCLASS'));
|
||||
assertFalse('Should not have WEIRDCLASS', classes.has(el, 'WEIRDCLASS'));
|
||||
}
|
||||
|
||||
function testEmptyClassNames() {
|
||||
var el = goog.dom.getElement('span1');
|
||||
// At the very least, make sure these do not error out.
|
||||
assertFalse('Should not have an empty class', classes.has(el, ''));
|
||||
classes.add(el, '');
|
||||
classes.toggle(el, '');
|
||||
assertFalse('Should not remove an empty class', classes.remove(el, ''));
|
||||
classes.swap(el, '', 'OTHERCLASS');
|
||||
classes.swap(el, 'TEST1', '');
|
||||
classes.addRemove(el, '', '');
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for detecting, adding and removing classes. Prefer
|
||||
* this over goog.dom.classes for new code since it attempts to use classList
|
||||
* (DOMTokenList: http://dom.spec.whatwg.org/#domtokenlist) which is faster
|
||||
* and requires less code.
|
||||
*
|
||||
* Note: these utilities are meant to operate on HTMLElements
|
||||
* and may have unexpected behavior on elements with differing interfaces
|
||||
* (such as SVGElements).
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.classlist');
|
||||
|
||||
goog.require('goog.array');
|
||||
|
||||
|
||||
/**
|
||||
* Override this define at build-time if you know your target supports it.
|
||||
* @define {boolean} Whether to use the classList property (DOMTokenList).
|
||||
*/
|
||||
goog.define('goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST', false);
|
||||
|
||||
|
||||
/**
|
||||
* Gets an array-like object of class names on an element.
|
||||
* @param {Element} element DOM node to get the classes of.
|
||||
* @return {!goog.array.ArrayLike} Class names on {@code element}.
|
||||
*/
|
||||
goog.dom.classlist.get = function(element) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
return element.classList;
|
||||
}
|
||||
|
||||
var className = element.className;
|
||||
// Some types of elements don't have a className in IE (e.g. iframes).
|
||||
// Furthermore, in Firefox, className is not a string when the element is
|
||||
// an SVG element.
|
||||
return goog.isString(className) && className.match(/\S+/g) || [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the entire class name of an element.
|
||||
* @param {Element} element DOM node to set class of.
|
||||
* @param {string} className Class name(s) to apply to element.
|
||||
*/
|
||||
goog.dom.classlist.set = function(element, className) {
|
||||
element.className = className;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if an element has a class. This method may throw a DOM
|
||||
* exception for an invalid or empty class name if DOMTokenList is used.
|
||||
* @param {Element} element DOM node to test.
|
||||
* @param {string} className Class name to test for.
|
||||
* @return {boolean} Whether element has the class.
|
||||
*/
|
||||
goog.dom.classlist.contains = function(element, className) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
return element.classList.contains(className);
|
||||
}
|
||||
return goog.array.contains(goog.dom.classlist.get(element), className);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a class to an element. Does not add multiples of class names. This
|
||||
* method may throw a DOM exception for an invalid or empty class name if
|
||||
* DOMTokenList is used.
|
||||
* @param {Element} element DOM node to add class to.
|
||||
* @param {string} className Class name to add.
|
||||
*/
|
||||
goog.dom.classlist.add = function(element, className) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
element.classList.add(className);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!goog.dom.classlist.contains(element, className)) {
|
||||
// Ensure we add a space if this is not the first class name added.
|
||||
element.className += element.className.length > 0 ?
|
||||
(' ' + className) : className;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to add a number of class names at once.
|
||||
* @param {Element} element The element to which to add classes.
|
||||
* @param {goog.array.ArrayLike<string>} classesToAdd An array-like object
|
||||
* containing a collection of class names to add to the element.
|
||||
* This method may throw a DOM exception if classesToAdd contains invalid
|
||||
* or empty class names.
|
||||
*/
|
||||
goog.dom.classlist.addAll = function(element, classesToAdd) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
goog.array.forEach(classesToAdd, function(className) {
|
||||
goog.dom.classlist.add(element, className);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var classMap = {};
|
||||
|
||||
// Get all current class names into a map.
|
||||
goog.array.forEach(goog.dom.classlist.get(element),
|
||||
function(className) {
|
||||
classMap[className] = true;
|
||||
});
|
||||
|
||||
// Add new class names to the map.
|
||||
goog.array.forEach(classesToAdd,
|
||||
function(className) {
|
||||
classMap[className] = true;
|
||||
});
|
||||
|
||||
// Flatten the keys of the map into the className.
|
||||
element.className = '';
|
||||
for (var className in classMap) {
|
||||
element.className += element.className.length > 0 ?
|
||||
(' ' + className) : className;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a class from an element. This method may throw a DOM exception
|
||||
* for an invalid or empty class name if DOMTokenList is used.
|
||||
* @param {Element} element DOM node to remove class from.
|
||||
* @param {string} className Class name to remove.
|
||||
*/
|
||||
goog.dom.classlist.remove = function(element, className) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
element.classList.remove(className);
|
||||
return;
|
||||
}
|
||||
|
||||
if (goog.dom.classlist.contains(element, className)) {
|
||||
// Filter out the class name.
|
||||
element.className = goog.array.filter(
|
||||
goog.dom.classlist.get(element),
|
||||
function(c) {
|
||||
return c != className;
|
||||
}).join(' ');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a set of classes from an element. Prefer this call to
|
||||
* repeatedly calling {@code goog.dom.classlist.remove} if you want to remove
|
||||
* a large set of class names at once.
|
||||
* @param {Element} element The element from which to remove classes.
|
||||
* @param {goog.array.ArrayLike<string>} classesToRemove An array-like object
|
||||
* containing a collection of class names to remove from the element.
|
||||
* This method may throw a DOM exception if classesToRemove contains invalid
|
||||
* or empty class names.
|
||||
*/
|
||||
goog.dom.classlist.removeAll = function(element, classesToRemove) {
|
||||
if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
|
||||
goog.array.forEach(classesToRemove, function(className) {
|
||||
goog.dom.classlist.remove(element, className);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Filter out those classes in classesToRemove.
|
||||
element.className = goog.array.filter(
|
||||
goog.dom.classlist.get(element),
|
||||
function(className) {
|
||||
// If this class is not one we are trying to remove,
|
||||
// add it to the array of new class names.
|
||||
return !goog.array.contains(classesToRemove, className);
|
||||
}).join(' ');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a class depending on the enabled argument. This method
|
||||
* may throw a DOM exception for an invalid or empty class name if DOMTokenList
|
||||
* is used.
|
||||
* @param {Element} element DOM node to add or remove the class on.
|
||||
* @param {string} className Class name to add or remove.
|
||||
* @param {boolean} enabled Whether to add or remove the class (true adds,
|
||||
* false removes).
|
||||
*/
|
||||
goog.dom.classlist.enable = function(element, className, enabled) {
|
||||
if (enabled) {
|
||||
goog.dom.classlist.add(element, className);
|
||||
} else {
|
||||
goog.dom.classlist.remove(element, className);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a set of classes depending on the enabled argument. This
|
||||
* method may throw a DOM exception for an invalid or empty class name if
|
||||
* DOMTokenList is used.
|
||||
* @param {!Element} element DOM node to add or remove the class on.
|
||||
* @param {goog.array.ArrayLike<string>} classesToEnable An array-like object
|
||||
* containing a collection of class names to add or remove from the element.
|
||||
* @param {boolean} enabled Whether to add or remove the classes (true adds,
|
||||
* false removes).
|
||||
*/
|
||||
goog.dom.classlist.enableAll = function(element, classesToEnable, enabled) {
|
||||
var f = enabled ? goog.dom.classlist.addAll :
|
||||
goog.dom.classlist.removeAll;
|
||||
f(element, classesToEnable);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Switches a class on an element from one to another without disturbing other
|
||||
* classes. If the fromClass isn't removed, the toClass won't be added. This
|
||||
* method may throw a DOM exception if the class names are empty or invalid.
|
||||
* @param {Element} element DOM node to swap classes on.
|
||||
* @param {string} fromClass Class to remove.
|
||||
* @param {string} toClass Class to add.
|
||||
* @return {boolean} Whether classes were switched.
|
||||
*/
|
||||
goog.dom.classlist.swap = function(element, fromClass, toClass) {
|
||||
if (goog.dom.classlist.contains(element, fromClass)) {
|
||||
goog.dom.classlist.remove(element, fromClass);
|
||||
goog.dom.classlist.add(element, toClass);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a class if an element has it, and adds it the element doesn't have
|
||||
* it. Won't affect other classes on the node. This method may throw a DOM
|
||||
* exception if the class name is empty or invalid.
|
||||
* @param {Element} element DOM node to toggle class on.
|
||||
* @param {string} className Class to toggle.
|
||||
* @return {boolean} True if class was added, false if it was removed
|
||||
* (in other words, whether element has the class after this function has
|
||||
* been called).
|
||||
*/
|
||||
goog.dom.classlist.toggle = function(element, className) {
|
||||
var add = !goog.dom.classlist.contains(element, className);
|
||||
goog.dom.classlist.enable(element, className, add);
|
||||
return add;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds and removes a class of an element. Unlike
|
||||
* {@link goog.dom.classlist.swap}, this method adds the classToAdd regardless
|
||||
* of whether the classToRemove was present and had been removed. This method
|
||||
* may throw a DOM exception if the class names are empty or invalid.
|
||||
*
|
||||
* @param {Element} element DOM node to swap classes on.
|
||||
* @param {string} classToRemove Class to remove.
|
||||
* @param {string} classToAdd Class to add.
|
||||
*/
|
||||
goog.dom.classlist.addRemove = function(element, classToRemove, classToAdd) {
|
||||
goog.dom.classlist.remove(element, classToRemove);
|
||||
goog.dom.classlist.add(element, classToAdd);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2012 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>
|
||||
<title>Closure Unit Tests - goog.dom.classlist</title>
|
||||
<script src="../base.js"></script>
|
||||
<style type="text/css">
|
||||
#styleTest1 {
|
||||
width:120px;font-weight:bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p id="p1" class="SOMECLASS
|
||||
OTHERCLASS"></p>
|
||||
<p id="p2" class="camelCase"></p>
|
||||
<svg>
|
||||
<rect id="rect1" class="svgclass"/>
|
||||
</svg>
|
||||
<script>
|
||||
goog.require('goog.dom.classlist_test');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2012 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 Shared code for classlist_test.html.
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.classlist_test');
|
||||
goog.setTestOnly('goog.dom.classlist_test');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.testing.ExpectedFailures');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var expectedFailures = new goog.testing.ExpectedFailures();
|
||||
var classlist = goog.dom.classlist;
|
||||
|
||||
function tearDown() {
|
||||
expectedFailures.handleTearDown();
|
||||
}
|
||||
|
||||
function testGet() {
|
||||
var el = document.createElement('div');
|
||||
assertTrue(classlist.get(el).length == 0);
|
||||
el.className = 'C';
|
||||
assertElementsEquals(['C'], classlist.get(el));
|
||||
el.className = 'C D';
|
||||
assertElementsEquals(['C', 'D'], classlist.get(el));
|
||||
el.className = 'C\nD';
|
||||
assertElementsEquals(['C', 'D'], classlist.get(el));
|
||||
el.className = ' C ';
|
||||
assertElementsEquals(['C'], classlist.get(el));
|
||||
}
|
||||
|
||||
function testContainsWithNewlines() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
assertTrue('Should not have SOMECLASS', classlist.contains(el, 'SOMECLASS'));
|
||||
assertTrue('Should also have OTHERCLASS',
|
||||
classlist.contains(el, 'OTHERCLASS'));
|
||||
assertFalse('Should not have WEIRDCLASS',
|
||||
classlist.contains(el, 'WEIRDCLASS'));
|
||||
}
|
||||
|
||||
function testContainsCaseSensitive() {
|
||||
var el = goog.dom.getElement('p2');
|
||||
assertFalse('Should not have camelcase',
|
||||
classlist.contains(el, 'camelcase'));
|
||||
assertFalse('Should not have CAMELCASE',
|
||||
classlist.contains(el, 'CAMELCASE'));
|
||||
assertTrue('Should have camelCase',
|
||||
classlist.contains(el, 'camelCase'));
|
||||
}
|
||||
|
||||
function testAddNotAddingMultiples() {
|
||||
var el = document.createElement('div');
|
||||
classlist.add(el, 'A');
|
||||
assertEquals('A', el.className);
|
||||
classlist.add(el, 'A');
|
||||
assertEquals('A', el.className);
|
||||
classlist.add(el, 'B', 'B');
|
||||
assertEquals('A B', el.className);
|
||||
}
|
||||
|
||||
function testAddCaseSensitive() {
|
||||
var el = document.createElement('div');
|
||||
classlist.add(el, 'A');
|
||||
assertTrue(classlist.contains(el, 'A'));
|
||||
assertFalse(classlist.contains(el, 'a'));
|
||||
classlist.add(el, 'a');
|
||||
assertTrue(classlist.contains(el, 'A'));
|
||||
assertTrue(classlist.contains(el, 'a'));
|
||||
assertEquals('A a', el.className);
|
||||
}
|
||||
|
||||
function testAddAll() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo goog-bar';
|
||||
|
||||
goog.dom.classlist.addAll(elem, ['goog-baz', 'foo']);
|
||||
assertEquals(3, classlist.get(elem).length);
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'goog-bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'goog-baz'));
|
||||
}
|
||||
|
||||
function testAddAllEmpty() {
|
||||
var classes = 'foo bar';
|
||||
var elem = document.createElement('div');
|
||||
elem.className = classes;
|
||||
|
||||
goog.dom.classlist.addAll(elem, []);
|
||||
assertEquals(elem.className, classes);
|
||||
}
|
||||
|
||||
function testRemove() {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'A B C';
|
||||
classlist.remove(el, 'B');
|
||||
assertEquals('A C', el.className);
|
||||
}
|
||||
|
||||
function testRemoveCaseSensitive() {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'A B C';
|
||||
classlist.remove(el, 'b');
|
||||
assertEquals('A B C', el.className);
|
||||
}
|
||||
|
||||
function testRemoveAll() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar baz';
|
||||
|
||||
goog.dom.classlist.removeAll(elem, ['bar', 'foo']);
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
}
|
||||
|
||||
function testRemoveAllOne() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar baz';
|
||||
|
||||
goog.dom.classlist.removeAll(elem, ['bar']);
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
}
|
||||
|
||||
function testRemoveAllSomeNotPresent() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar baz';
|
||||
|
||||
goog.dom.classlist.removeAll(elem, ['a', 'bar']);
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
}
|
||||
|
||||
function testRemoveAllCaseSensitive() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar baz';
|
||||
|
||||
goog.dom.classlist.removeAll(elem, ['BAR', 'foo']);
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
}
|
||||
|
||||
function testEnable() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classlist.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
|
||||
classlist.enable(el, 'FIRST', false);
|
||||
|
||||
assertFalse('Should not have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
|
||||
classlist.enable(el, 'FIRST', true);
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
}
|
||||
|
||||
function testEnableNotAddingMultiples() {
|
||||
var el = document.createElement('div');
|
||||
classlist.enable(el, 'A', true);
|
||||
assertEquals('A', el.className);
|
||||
classlist.enable(el, 'A', true);
|
||||
assertEquals('A', el.className);
|
||||
classlist.enable(el, 'B', 'B', true);
|
||||
assertEquals('A B', el.className);
|
||||
}
|
||||
|
||||
function testEnableAllRemove() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar baz';
|
||||
|
||||
// Test removing some classes (some not present).
|
||||
goog.dom.classlist.enableAll(elem, ['a', 'bar'], false /* enable */);
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
assertFalse(goog.dom.classlist.contains(elem, 'a'));
|
||||
}
|
||||
|
||||
function testEnableAllAdd() {
|
||||
var elem = document.createElement('div');
|
||||
elem.className = 'foo bar';
|
||||
|
||||
// Test adding some classes (some duplicate).
|
||||
goog.dom.classlist.enableAll(elem, ['a', 'bar', 'baz'], true /* enable */);
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'foo'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'bar'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'baz'));
|
||||
assertTrue(goog.dom.classlist.contains(elem, 'a'));
|
||||
}
|
||||
|
||||
function testSwap() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classlist.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'SOMECLASS'));
|
||||
assertFalse('Should not have second class', classlist.contains(el, 'second'));
|
||||
|
||||
classlist.swap(el, 'FIRST', 'second');
|
||||
|
||||
assertFalse('Should not have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'SOMECLASS'));
|
||||
assertTrue('Should have second class', classlist.contains(el, 'second'));
|
||||
|
||||
classlist.swap(el, 'second', 'FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'SOMECLASS'));
|
||||
assertFalse('Should not have second class', classlist.contains(el, 'second'));
|
||||
}
|
||||
|
||||
function testToggle() {
|
||||
var el = goog.dom.getElement('p1');
|
||||
classlist.set(el, 'SOMECLASS FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
|
||||
var ret = classlist.toggle(el, 'FIRST');
|
||||
|
||||
assertFalse('Should not have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
assertFalse('Return value should have been false', ret);
|
||||
|
||||
ret = classlist.toggle(el, 'FIRST');
|
||||
|
||||
assertTrue('Should have FIRST class', classlist.contains(el, 'FIRST'));
|
||||
assertTrue('Should have SOMECLASS class',
|
||||
classlist.contains(el, 'SOMECLASS'));
|
||||
assertTrue('Return value should have been true', ret);
|
||||
}
|
||||
|
||||
function testAddRemoveString() {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'A';
|
||||
|
||||
classlist.addRemove(el, 'A', 'B');
|
||||
assertEquals('B', el.className);
|
||||
|
||||
classlist.addRemove(el, 'Z', 'C');
|
||||
assertEquals('B C', el.className);
|
||||
|
||||
classlist.addRemove(el, 'C', 'D');
|
||||
assertEquals('B D', el.className);
|
||||
|
||||
classlist.addRemove(el, 'D', 'B');
|
||||
assertEquals('B', el.className);
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for working with IE control ranges.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.ControlRange');
|
||||
goog.provide('goog.dom.ControlRangeIterator');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractMultiRange');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
goog.require('goog.dom.RangeIterator');
|
||||
goog.require('goog.dom.RangeType');
|
||||
goog.require('goog.dom.SavedRange');
|
||||
goog.require('goog.dom.TagWalkType');
|
||||
goog.require('goog.dom.TextRange');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new control selection with no properties. Do not use this
|
||||
* constructor: use one of the goog.dom.Range.createFrom* methods instead.
|
||||
* @constructor
|
||||
* @extends {goog.dom.AbstractMultiRange}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.ControlRange = function() {
|
||||
};
|
||||
goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange);
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper from the given browser range object. Do not use
|
||||
* this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {Object} controlRange The browser range object.
|
||||
* @return {!goog.dom.ControlRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.ControlRange.createFromBrowserRange = function(controlRange) {
|
||||
var range = new goog.dom.ControlRange();
|
||||
range.range_ = controlRange;
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that selects the given element. Do not use
|
||||
* this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {...Element} var_args The element(s) to select.
|
||||
* @return {!goog.dom.ControlRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.ControlRange.createFromElements = function(var_args) {
|
||||
var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange();
|
||||
for (var i = 0, len = arguments.length; i < len; i++) {
|
||||
range.addElement(arguments[i]);
|
||||
}
|
||||
return goog.dom.ControlRange.createFromBrowserRange(range);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The IE control range obejct.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.range_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Cached list of elements.
|
||||
* @type {Array<Element>?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.elements_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Cached sorted list of elements.
|
||||
* @type {Array<Element>?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.sortedElements_ = null;
|
||||
|
||||
|
||||
// Method implementations
|
||||
|
||||
|
||||
/**
|
||||
* Clear cached values.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.clearCachedValues_ = function() {
|
||||
this.elements_ = null;
|
||||
this.sortedElements_ = null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.clone = function() {
|
||||
return goog.dom.ControlRange.createFromElements.apply(this,
|
||||
this.getElements());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getType = function() {
|
||||
return goog.dom.RangeType.CONTROL;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getBrowserRangeObject = function() {
|
||||
return this.range_ || document.body.createControlRange();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) {
|
||||
if (!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
|
||||
return false;
|
||||
}
|
||||
this.range_ = nativeRange;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getTextRangeCount = function() {
|
||||
return this.range_ ? this.range_.length : 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getTextRange = function(i) {
|
||||
return goog.dom.TextRange.createFromNodeContents(this.range_.item(i));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getContainer = function() {
|
||||
return goog.dom.findCommonAncestor.apply(null, this.getElements());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getStartNode = function() {
|
||||
return this.getSortedElements()[0];
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getStartOffset = function() {
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getEndNode = function() {
|
||||
var sorted = this.getSortedElements();
|
||||
var startsLast = /** @type {Node} */ (goog.array.peek(sorted));
|
||||
return /** @type {Node} */ (goog.array.find(sorted, function(el) {
|
||||
return goog.dom.contains(el, startsLast);
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getEndOffset = function() {
|
||||
return this.getEndNode().childNodes.length;
|
||||
};
|
||||
|
||||
|
||||
// TODO(robbyw): Figure out how to unify getElements with TextRange API.
|
||||
/**
|
||||
* @return {!Array<Element>} Array of elements in the control range.
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.getElements = function() {
|
||||
if (!this.elements_) {
|
||||
this.elements_ = [];
|
||||
if (this.range_) {
|
||||
for (var i = 0; i < this.range_.length; i++) {
|
||||
this.elements_.push(this.range_.item(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.elements_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Array<Element>} Array of elements comprising the control range,
|
||||
* sorted by document order.
|
||||
*/
|
||||
goog.dom.ControlRange.prototype.getSortedElements = function() {
|
||||
if (!this.sortedElements_) {
|
||||
this.sortedElements_ = this.getElements().concat();
|
||||
this.sortedElements_.sort(function(a, b) {
|
||||
return a.sourceIndex - b.sourceIndex;
|
||||
});
|
||||
}
|
||||
|
||||
return this.sortedElements_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.isRangeInDocument = function() {
|
||||
var returnValue = false;
|
||||
|
||||
try {
|
||||
returnValue = goog.array.every(this.getElements(), function(element) {
|
||||
// On IE, this throws an exception when the range is detached.
|
||||
return goog.userAgent.IE ?
|
||||
!!element.parentNode :
|
||||
goog.dom.contains(element.ownerDocument.body, element);
|
||||
});
|
||||
} catch (e) {
|
||||
// IE sometimes throws Invalid Argument errors for detached elements.
|
||||
// Note: trying to return a value from the above try block can cause IE
|
||||
// to crash. It is necessary to use the local returnValue.
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.isCollapsed = function() {
|
||||
return !this.range_ || !this.range_.length;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getText = function() {
|
||||
// TODO(robbyw): What about for table selections? Should those have text?
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getHtmlFragment = function() {
|
||||
return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml).
|
||||
join('');
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getValidHtml = function() {
|
||||
return this.getHtmlFragment();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.getPastableHtml =
|
||||
goog.dom.ControlRange.prototype.getValidHtml;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) {
|
||||
return new goog.dom.ControlRangeIterator(this);
|
||||
};
|
||||
|
||||
|
||||
// RANGE ACTIONS
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.select = function() {
|
||||
if (this.range_) {
|
||||
this.range_.select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.removeContents = function() {
|
||||
// TODO(robbyw): Test implementing with execCommand('Delete')
|
||||
if (this.range_) {
|
||||
var nodes = [];
|
||||
for (var i = 0, len = this.range_.length; i < len; i++) {
|
||||
nodes.push(this.range_.item(i));
|
||||
}
|
||||
goog.array.forEach(nodes, goog.dom.removeNode);
|
||||
|
||||
this.collapse(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) {
|
||||
// Control selections have to have the node inserted before removing the
|
||||
// selection contents because a collapsed control range doesn't have start or
|
||||
// end nodes.
|
||||
var result = this.insertNode(node, true);
|
||||
|
||||
if (!this.isCollapsed()) {
|
||||
this.removeContents();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
// SAVE/RESTORE
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.saveUsingDom = function() {
|
||||
return new goog.dom.DomSavedControlRange_(this);
|
||||
};
|
||||
|
||||
|
||||
// RANGE MODIFICATION
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRange.prototype.collapse = function(toAnchor) {
|
||||
// TODO(robbyw): Should this return a text range? If so, API needs to change.
|
||||
this.range_ = null;
|
||||
this.clearCachedValues_();
|
||||
};
|
||||
|
||||
|
||||
// SAVED RANGE OBJECTS
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A SavedRange implementation using DOM endpoints.
|
||||
* @param {goog.dom.ControlRange} range The range to save.
|
||||
* @constructor
|
||||
* @extends {goog.dom.SavedRange}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.DomSavedControlRange_ = function(range) {
|
||||
/**
|
||||
* The element list.
|
||||
* @type {Array<Element>}
|
||||
* @private
|
||||
*/
|
||||
this.elements_ = range.getElements();
|
||||
};
|
||||
goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() {
|
||||
var doc = this.elements_.length ?
|
||||
goog.dom.getOwnerDocument(this.elements_[0]) : document;
|
||||
var controlRange = doc.body.createControlRange();
|
||||
for (var i = 0, len = this.elements_.length; i < len; i++) {
|
||||
controlRange.addElement(this.elements_[i]);
|
||||
}
|
||||
return goog.dom.ControlRange.createFromBrowserRange(controlRange);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() {
|
||||
goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this);
|
||||
delete this.elements_;
|
||||
};
|
||||
|
||||
|
||||
// RANGE ITERATION
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Subclass of goog.dom.TagIterator that iterates over a DOM range. It
|
||||
* adds functions to determine the portion of each text node that is selected.
|
||||
*
|
||||
* @param {goog.dom.ControlRange?} range The range to traverse.
|
||||
* @constructor
|
||||
* @extends {goog.dom.RangeIterator}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.ControlRangeIterator = function(range) {
|
||||
if (range) {
|
||||
this.elements_ = range.getSortedElements();
|
||||
this.startNode_ = this.elements_.shift();
|
||||
this.endNode_ = /** @type {Node} */ (goog.array.peek(this.elements_)) ||
|
||||
this.startNode_;
|
||||
}
|
||||
|
||||
goog.dom.RangeIterator.call(this, this.startNode_, false);
|
||||
};
|
||||
goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator);
|
||||
|
||||
|
||||
/**
|
||||
* The first node in the selection.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRangeIterator.prototype.startNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The last node in the selection.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRangeIterator.prototype.endNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The list of elements left to traverse.
|
||||
* @type {Array<Element>?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ControlRangeIterator.prototype.elements_ = null;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() {
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() {
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.getStartNode = function() {
|
||||
return this.startNode_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.getEndNode = function() {
|
||||
return this.endNode_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.isLast = function() {
|
||||
return !this.depth && !this.elements_.length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move to the next position in the selection.
|
||||
* Throws {@code goog.iter.StopIteration} when it passes the end of the range.
|
||||
* @return {Node} The node at the next position.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.ControlRangeIterator.prototype.next = function() {
|
||||
// Iterate over each element in the range, and all of its children.
|
||||
if (this.isLast()) {
|
||||
throw goog.iter.StopIteration;
|
||||
} else if (!this.depth) {
|
||||
var el = this.elements_.shift();
|
||||
this.setPosition(el,
|
||||
goog.dom.TagWalkType.START_TAG,
|
||||
goog.dom.TagWalkType.START_TAG);
|
||||
return el;
|
||||
}
|
||||
|
||||
// Call the super function.
|
||||
return goog.dom.ControlRangeIterator.superClass_.next.call(this);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) {
|
||||
this.elements_ = other.elements_;
|
||||
this.startNode_ = other.startNode_;
|
||||
this.endNode_ = other.endNode_;
|
||||
|
||||
goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.ControlRangeIterator} An identical iterator.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.ControlRangeIterator.prototype.clone = function() {
|
||||
var copy = new goog.dom.ControlRangeIterator(null);
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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.ControlRange</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.ControlRangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test1"></div>
|
||||
<div id="test2">
|
||||
<img id="logo" src="http://www.google.com/intl/en_ALL/images/logo.gif">
|
||||
</div>
|
||||
<!-- Omit whitespace here to ensure no extra text nodes are included. -->
|
||||
<table id="table"><tbody id="tbody"><tr id="tr1"><td id="td11">a</td
|
||||
><td id="td12">b</td></tr><tr id="tr2"><td id="td21">c</td><td id="td22"
|
||||
>d</td></tr></tbody></table>
|
||||
<table id="table2">
|
||||
<tr>
|
||||
<td>moof</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="table2td">
|
||||
foo
|
||||
<img id="logo2" src="http://www.google.com/intl/en_ALL/images/logo.gif">
|
||||
bar
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright 2008 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.ControlRangeTest');
|
||||
goog.setTestOnly('goog.dom.ControlRangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.ControlRange');
|
||||
goog.require('goog.dom.RangeType');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.dom.TextRange');
|
||||
goog.require('goog.testing.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var logo;
|
||||
var table;
|
||||
|
||||
function setUpPage() {
|
||||
logo = goog.dom.getElement('logo');
|
||||
table = goog.dom.getElement('table');
|
||||
}
|
||||
|
||||
function testCreateFromElement() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
assertNotNull('Control range object can be created for element',
|
||||
goog.dom.ControlRange.createFromElements(logo));
|
||||
}
|
||||
|
||||
function testCreateFromRange() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
var range = document.body.createControlRange();
|
||||
range.addElement(table);
|
||||
assertNotNull('Control range object can be created for element',
|
||||
goog.dom.ControlRange.createFromBrowserRange(range));
|
||||
}
|
||||
|
||||
function testSelect() {
|
||||
if (!goog.userAgent.IE || goog.userAgent.isVersionOrHigher('11')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(table);
|
||||
range.select();
|
||||
|
||||
assertEquals('Control range should be selected', 'Control',
|
||||
document.selection.type);
|
||||
assertEquals('Control range should have length 1', 1,
|
||||
document.selection.createRange().length);
|
||||
assertEquals('Control range should select table', table,
|
||||
document.selection.createRange().item(0));
|
||||
}
|
||||
|
||||
function testControlRangeIterator() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
var range = goog.dom.ControlRange.createFromElements(logo, table);
|
||||
// Each node is included twice - once as a start tag, once as an end.
|
||||
goog.testing.dom.assertNodesMatch(range, ['#logo', '#logo', '#table',
|
||||
'#tbody', '#tr1', '#td11', 'a', '#td11', '#td12', 'b', '#td12', '#tr1',
|
||||
'#tr2', '#td21', 'c', '#td21', '#td22', 'd', '#td22', '#tr2', '#tbody',
|
||||
'#table']);
|
||||
}
|
||||
|
||||
function testBounds() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize in both orders.
|
||||
helpTestBounds(goog.dom.ControlRange.createFromElements(logo, table));
|
||||
helpTestBounds(goog.dom.ControlRange.createFromElements(table, logo));
|
||||
}
|
||||
|
||||
function helpTestBounds(range) {
|
||||
assertEquals('Start node is logo', logo, range.getStartNode());
|
||||
assertEquals('Start offset is 0', 0, range.getStartOffset());
|
||||
assertEquals('End node is table', table, range.getEndNode());
|
||||
assertEquals('End offset is 1', 1, range.getEndOffset());
|
||||
}
|
||||
|
||||
function testCollapse() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(logo, table);
|
||||
assertFalse('Not initially collapsed', range.isCollapsed());
|
||||
range.collapse();
|
||||
assertTrue('Successfully collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
function testGetContainer() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(logo);
|
||||
assertEquals('Single element range is contained by itself', logo,
|
||||
range.getContainer());
|
||||
|
||||
range = goog.dom.ControlRange.createFromElements(logo, table);
|
||||
assertEquals('Two element range is contained by body', document.body,
|
||||
range.getContainer());
|
||||
}
|
||||
|
||||
function testSave() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(logo, table);
|
||||
var savedRange = range.saveUsingDom();
|
||||
|
||||
range.collapse();
|
||||
assertTrue('Successfully collapsed', range.isCollapsed());
|
||||
|
||||
range = savedRange.restore();
|
||||
assertEquals('Restored a control range', goog.dom.RangeType.CONTROL,
|
||||
range.getType());
|
||||
assertFalse('Not collapsed after restore', range.isCollapsed());
|
||||
helpTestBounds(range);
|
||||
}
|
||||
|
||||
function testRemoveContents() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var img = goog.dom.createDom('IMG');
|
||||
img.src = logo.src;
|
||||
|
||||
var div = goog.dom.getElement('test1');
|
||||
div.innerHTML = '';
|
||||
div.appendChild(img);
|
||||
assertEquals('Div has 1 child', 1, div.childNodes.length);
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(img);
|
||||
range.removeContents();
|
||||
assertEquals('Div has 0 children', 0, div.childNodes.length);
|
||||
assertTrue('Range is collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
function testReplaceContents() {
|
||||
// Test a control range.
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var outer = goog.dom.getElement('test1');
|
||||
outer.innerHTML =
|
||||
'<div contentEditable="true">' +
|
||||
'Hello <input type="text" value="World">' +
|
||||
'</div>';
|
||||
range = goog.dom.ControlRange.createFromElements(
|
||||
outer.getElementsByTagName(goog.dom.TagName.INPUT)[0]);
|
||||
goog.dom.ControlRange.createFromElements(table);
|
||||
range.replaceContentsWithNode(goog.dom.createTextNode('World'));
|
||||
assertEquals('Hello World', outer.firstChild.innerHTML);
|
||||
}
|
||||
|
||||
function testContainsRange() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var table2 = goog.dom.getElement('table2');
|
||||
var table2td = goog.dom.getElement('table2td');
|
||||
var logo2 = goog.dom.getElement('logo2');
|
||||
|
||||
var range = goog.dom.ControlRange.createFromElements(logo, table);
|
||||
var range2 = goog.dom.ControlRange.createFromElements(logo);
|
||||
assertTrue('Control range contains the other control range',
|
||||
range.containsRange(range2));
|
||||
assertTrue('Control range partially contains the other control range',
|
||||
range2.containsRange(range, true));
|
||||
|
||||
range2 = goog.dom.ControlRange.createFromElements(table2);
|
||||
assertFalse('Control range does not contain the other control range',
|
||||
range.containsRange(range2));
|
||||
|
||||
range = goog.dom.ControlRange.createFromElements(table2);
|
||||
range2 = goog.dom.TextRange.createFromNodeContents(table2td);
|
||||
assertTrue('Control range contains text range',
|
||||
range.containsRange(range2));
|
||||
|
||||
range2 = goog.dom.TextRange.createFromNodeContents(table);
|
||||
assertFalse('Control range does not contain text range',
|
||||
range.containsRange(range2));
|
||||
|
||||
range = goog.dom.ControlRange.createFromElements(logo2);
|
||||
range2 = goog.dom.TextRange.createFromNodeContents(table2);
|
||||
assertFalse('Control range does not fully contain text range',
|
||||
range.containsRange(range2, false));
|
||||
|
||||
range2 = goog.dom.ControlRange.createFromElements(table2);
|
||||
assertTrue('Control range contains the other control range (2)',
|
||||
range2.containsRange(range));
|
||||
}
|
||||
|
||||
function testCloneRange() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
var range = goog.dom.ControlRange.createFromElements(logo);
|
||||
assertNotNull('Control range object created for element', range);
|
||||
|
||||
var cloneRange = range.clone();
|
||||
assertNotNull('Cloned control range object', cloneRange);
|
||||
assertArrayEquals('Control range and clone have same elements',
|
||||
range.getElements(), cloneRange.getElements());
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// 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 Utilities for adding, removing and setting values in
|
||||
* an Element's dataset.
|
||||
* See {@link http://www.w3.org/TR/html5/Overview.html#dom-dataset}.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.dataset');
|
||||
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
/**
|
||||
* The DOM attribute name prefix that must be present for it to be considered
|
||||
* for a dataset.
|
||||
* @type {string}
|
||||
* @const
|
||||
* @private
|
||||
*/
|
||||
goog.dom.dataset.PREFIX_ = 'data-';
|
||||
|
||||
|
||||
/**
|
||||
* Sets a custom data attribute on an element. The key should be
|
||||
* in camelCase format (e.g "keyName" for the "data-key-name" attribute).
|
||||
* @param {Element} element DOM node to set the custom data attribute on.
|
||||
* @param {string} key Key for the custom data attribute.
|
||||
* @param {string} value Value for the custom data attribute.
|
||||
*/
|
||||
goog.dom.dataset.set = function(element, key, value) {
|
||||
if (element.dataset) {
|
||||
element.dataset[key] = value;
|
||||
} else {
|
||||
element.setAttribute(
|
||||
goog.dom.dataset.PREFIX_ + goog.string.toSelectorCase(key),
|
||||
value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets a custom data attribute from an element. The key should be
|
||||
* in camelCase format (e.g "keyName" for the "data-key-name" attribute).
|
||||
* @param {Element} element DOM node to get the custom data attribute from.
|
||||
* @param {string} key Key for the custom data attribute.
|
||||
* @return {?string} The attribute value, if it exists.
|
||||
*/
|
||||
goog.dom.dataset.get = function(element, key) {
|
||||
if (element.dataset) {
|
||||
// Android browser (non-chrome) returns the empty string for
|
||||
// element.dataset['doesNotExist'].
|
||||
if (!(key in element.dataset)) {
|
||||
return null;
|
||||
}
|
||||
return element.dataset[key];
|
||||
} else {
|
||||
return element.getAttribute(goog.dom.dataset.PREFIX_ +
|
||||
goog.string.toSelectorCase(key));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a custom data attribute from an element. The key should be
|
||||
* in camelCase format (e.g "keyName" for the "data-key-name" attribute).
|
||||
* @param {Element} element DOM node to get the custom data attribute from.
|
||||
* @param {string} key Key for the custom data attribute.
|
||||
*/
|
||||
goog.dom.dataset.remove = function(element, key) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset[key];
|
||||
} else {
|
||||
element.removeAttribute(goog.dom.dataset.PREFIX_ +
|
||||
goog.string.toSelectorCase(key));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether custom data attribute exists on an element. The key should be
|
||||
* in camelCase format (e.g "keyName" for the "data-key-name" attribute).
|
||||
*
|
||||
* @param {Element} element DOM node to get the custom data attribute from.
|
||||
* @param {string} key Key for the custom data attribute.
|
||||
* @return {boolean} Whether the attribute exists.
|
||||
*/
|
||||
goog.dom.dataset.has = function(element, key) {
|
||||
if (element.dataset) {
|
||||
return key in element.dataset;
|
||||
} else if (element.hasAttribute) {
|
||||
return element.hasAttribute(goog.dom.dataset.PREFIX_ +
|
||||
goog.string.toSelectorCase(key));
|
||||
} else {
|
||||
return !!(element.getAttribute(goog.dom.dataset.PREFIX_ +
|
||||
goog.string.toSelectorCase(key)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets all custom data attributes as a string map. The attribute names will be
|
||||
* camel cased (e.g., data-foo-bar -> dataset['fooBar']). This operation is not
|
||||
* safe for attributes having camel-cased names clashing with already existing
|
||||
* properties (e.g., data-to-string -> dataset['toString']).
|
||||
* @param {!Element} element DOM node to get the data attributes from.
|
||||
* @return {!Object} The string map containing data attributes and their
|
||||
* respective values.
|
||||
*/
|
||||
goog.dom.dataset.getAll = function(element) {
|
||||
if (element.dataset) {
|
||||
return element.dataset;
|
||||
} else {
|
||||
var dataset = {};
|
||||
var attributes = element.attributes;
|
||||
for (var i = 0; i < attributes.length; ++i) {
|
||||
var attribute = attributes[i];
|
||||
if (goog.string.startsWith(attribute.name,
|
||||
goog.dom.dataset.PREFIX_)) {
|
||||
// We use substr(5), since it's faster than replacing 'data-' with ''.
|
||||
var key = goog.string.toCamelCase(attribute.name.substr(5));
|
||||
dataset[key] = attribute.value;
|
||||
}
|
||||
}
|
||||
return dataset;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2011 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.dataset</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.datasetTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<span id="el1" data-basic-key="basic"
|
||||
data--unusual-key1="unusual1"
|
||||
data-unusual--key2="unusual2"
|
||||
data---bizarre---key="bizarre"></span>
|
||||
<span id="el2"></span>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2011 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.datasetTest');
|
||||
goog.setTestOnly('goog.dom.datasetTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.dataset');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var $ = goog.dom.getElement;
|
||||
var dataset = goog.dom.dataset;
|
||||
|
||||
|
||||
function setUp() {
|
||||
var el = $('el2');
|
||||
el.setAttribute('data-dynamic-key', 'dynamic');
|
||||
}
|
||||
|
||||
|
||||
function testHas() {
|
||||
var el = $('el1');
|
||||
|
||||
assertTrue('Dataset should have an existing key',
|
||||
dataset.has(el, 'basicKey'));
|
||||
assertTrue('Dataset should have an existing (unusual) key',
|
||||
dataset.has(el, 'UnusualKey1'));
|
||||
assertTrue('Dataset should have an existing (unusual) key',
|
||||
dataset.has(el, 'unusual-Key2'));
|
||||
assertTrue('Dataset should have an existing (bizarre) key',
|
||||
dataset.has(el, '-Bizarre--Key'));
|
||||
assertFalse('Dataset should not have a non-existent key',
|
||||
dataset.has(el, 'bogusKey'));
|
||||
}
|
||||
|
||||
|
||||
function testGet() {
|
||||
var el = $('el1');
|
||||
|
||||
assertEquals('Dataset should return the proper value for an existing key',
|
||||
dataset.get(el, 'basicKey'), 'basic');
|
||||
assertEquals('Dataset should have an existing (unusual) key',
|
||||
dataset.get(el, 'UnusualKey1'), 'unusual1');
|
||||
assertEquals('Dataset should have an existing (unusual) key',
|
||||
dataset.get(el, 'unusual-Key2'), 'unusual2');
|
||||
assertEquals('Dataset should have an existing (bizarre) key',
|
||||
dataset.get(el, '-Bizarre--Key'), 'bizarre');
|
||||
assertFalse(
|
||||
'Dataset should return null or an empty string for a non-existent key',
|
||||
!!dataset.get(el, 'bogusKey'));
|
||||
|
||||
el = $('el2');
|
||||
assertEquals('Dataset should return the proper value for an existing key',
|
||||
dataset.get(el, 'dynamicKey'), 'dynamic');
|
||||
}
|
||||
|
||||
|
||||
function testSet() {
|
||||
var el = $('el2');
|
||||
|
||||
dataset.set(el, 'newKey', 'newValue');
|
||||
assertTrue('Dataset should have a newly created key',
|
||||
dataset.has(el, 'newKey'));
|
||||
assertEquals('Dataset should return the proper value for a newly created key',
|
||||
dataset.get(el, 'newKey'), 'newValue');
|
||||
|
||||
dataset.set(el, 'dynamicKey', 'customValue');
|
||||
assertTrue('Dataset should have a modified, existing key',
|
||||
dataset.has(el, 'dynamicKey'));
|
||||
assertEquals('Dataset should return the proper value for a modified key',
|
||||
dataset.get(el, 'dynamicKey'), 'customValue');
|
||||
}
|
||||
|
||||
|
||||
function testRemove() {
|
||||
var el = $('el2');
|
||||
|
||||
dataset.remove(el, 'dynamicKey');
|
||||
assertFalse('Dataset should not have a removed key',
|
||||
dataset.has(el, 'dynamicKey'));
|
||||
assertFalse('Dataset should return null or an empty string for removed key',
|
||||
!!dataset.get(el, 'dynamicKey'));
|
||||
}
|
||||
|
||||
|
||||
function testGetAll() {
|
||||
var el = $('el1');
|
||||
var expectedDataset = {
|
||||
'basicKey': 'basic',
|
||||
'UnusualKey1': 'unusual1',
|
||||
'unusual-Key2': 'unusual2',
|
||||
'-Bizarre--Key': 'bizarre'
|
||||
};
|
||||
assertHashEquals('Dataset should have basicKey, UnusualKey1, ' +
|
||||
'unusual-Key2, and -Bizarre--Key',
|
||||
expectedDataset, dataset.getAll(el));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.
|
||||
-->
|
||||
<!--
|
||||
|
||||
This is a copy of dom_test.html but without a doctype. Make sure these two
|
||||
are in sync.
|
||||
-->
|
||||
<head>
|
||||
<title>Closure Unit Tests - goog.dom</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
</script>
|
||||
<style type="text/css">
|
||||
#styleTest1 {
|
||||
width:120px;font-weight:bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
abc <i>def</i> <s id="offsetParent1">g <b>h <i id="offsetTest1">ij</i> kl</b> mn</s> opq
|
||||
</div>
|
||||
|
||||
|
||||
<div id="testEl">
|
||||
<span>Test Element</span>
|
||||
</div>
|
||||
|
||||
<div><div><div id="testEl2"></div></div></div>
|
||||
|
||||
<!-- classbefore and classafter are for making sure that getElementsByClass
|
||||
works when multiple classes are specified. -->
|
||||
<div id="span-container">
|
||||
<span id="span1" class="classbefore test1"></span>
|
||||
<span id="span2" class="test1"></span>
|
||||
<span id="span3" class="test2"></span>
|
||||
<span id="span4" class="test3"></span>
|
||||
<span id="span5" class="test1 classafter"></span>
|
||||
</div>
|
||||
|
||||
<div class="mixedCaseClass"></div>
|
||||
|
||||
<p id="p1"></p>
|
||||
|
||||
<div id="styleTest1"></div>
|
||||
<div id="styleTest2" style="width:100px;font-weight:bold"></div>
|
||||
<div id="styleTest3"></div>
|
||||
|
||||
<!-- Paragraph to test element child and sibling -->
|
||||
<p id="p2">
|
||||
<!-- Comment -->
|
||||
a
|
||||
<b id="b1">c</b>
|
||||
d
|
||||
<!-- Comment -->
|
||||
e
|
||||
<b id="b2">f</b>
|
||||
g
|
||||
<!-- Comment -->
|
||||
</p>
|
||||
|
||||
<table id="testTable1">
|
||||
<tr>
|
||||
<td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<iframe name="frame" src="tagname_test.html"></iframe>
|
||||
|
||||
<p id="order-test"></p>
|
||||
|
||||
<div id="testAncestorDiv" class="ancestorClassA testAncestor">
|
||||
<p id="testAncestorP" class="ancestorClassB testAncestor">
|
||||
<b id="nestedElement">ancestorTest</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="noTabIndex">Test</div>
|
||||
<div id="tabIndexNegative2" tabindex="-2">Test</div>
|
||||
<div id="tabIndexNegative1" tabindex="-1">Test</div>
|
||||
<div id="tabIndex0" tabindex="0">Test</div>
|
||||
<div id="tabIndex1" tabindex="1">Test</div>
|
||||
<div id="tabIndex2" tabindex="2">Test</div>
|
||||
|
||||
<form>
|
||||
<a href="testUrl" id="noTabIndexAnchor">Test</a>
|
||||
<input id="noTabIndexInput">
|
||||
<textarea id="noTabIndexTextArea">Test</textarea>
|
||||
<select id="noTabIndexSelect"><option>Test</option></select>
|
||||
<button id="noTabIndexButton">Test</button>
|
||||
<button id="negTabIndexButton" tabindex="-1">Test</button>
|
||||
<button id="zeroTabIndexButton" tabindex="0">Test</button>
|
||||
<button id="posTabIndexButton" tabindex="1">Test</button>
|
||||
<button id="disabledNoTabIndexButton" disabled>Test</button>
|
||||
<button id="disabledNegTabIndexButton" disabled tabindex="-1">Test</button>
|
||||
<button id="disabledZeroTabIndexButton" disabled tabindex="0">Test</button>
|
||||
<button id="disabledPosTabIndexButton" disabled tabindex="1">Test</button>
|
||||
</form>
|
||||
|
||||
<div id="toReplace">Replace Test</div>
|
||||
|
||||
<iframe id="iframe"></iframe>
|
||||
|
||||
<div id="myIframeDiv1" style="height:42px;font-size:1px;line-height:0;">hello world</div>
|
||||
<div id="myIframeDiv2" style="height:23px;font-size:1px;line-height:0;">hello world</div>
|
||||
|
||||
<iframe id="myIframe" style="width:400px;height:200px;"></iframe>
|
||||
|
||||
<a id='link' href='foo.html'>Foo</a>
|
||||
|
||||
<svg id="testSvg" width="100" height="100" viewPort="0 0 100 100" version="1.0">
|
||||
<g id="testG">
|
||||
<rect id="testRect" x="10" y="10" width="50" height="50"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<script src="dom_test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.
|
||||
-->
|
||||
<!--
|
||||
|
||||
When changing this, make sure that dom_quirks_test.html is kept in sync.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
</script>
|
||||
<style type="text/css">
|
||||
#styleTest1 {
|
||||
width:120px;font-weight:bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
abc <i>def</i> <s id="offsetParent1">g <b>h <i id="offsetTest1">ij</i> kl</b> mn</s> opq
|
||||
</div>
|
||||
|
||||
|
||||
<div id="testEl">
|
||||
<span>Test Element</span>
|
||||
</div>
|
||||
|
||||
<div><div><div id="testEl2"></div></div></div>
|
||||
|
||||
<!-- classbefore and classafter are for making sure that getElementsByClass
|
||||
works when multiple classes are specified. -->
|
||||
<div id="span-container">
|
||||
<span id="span1" class="classbefore test1"></span>
|
||||
<span id="span2" class="test1"></span>
|
||||
<span id="span3" class="test2"></span>
|
||||
<span id="span4" class="test3"></span>
|
||||
<span id="span5" class="test1 classafter"></span>
|
||||
</div>
|
||||
|
||||
<div class="mixedCaseClass"></div>
|
||||
|
||||
<p id="p1"></p>
|
||||
|
||||
<div id="styleTest1"></div>
|
||||
<div id="styleTest2" style="width:100px;font-weight:bold"></div>
|
||||
<div id="styleTest3"></div>
|
||||
|
||||
<!-- Paragraph to test element child and sibling -->
|
||||
<p id="p2">
|
||||
<!-- Comment -->
|
||||
a
|
||||
<b id="b1">c</b>
|
||||
d
|
||||
<!-- Comment -->
|
||||
e
|
||||
<b id="b2">f</b>
|
||||
g
|
||||
<!-- Comment -->
|
||||
</p>
|
||||
|
||||
<table id="testTable1">
|
||||
<tr>
|
||||
<td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<iframe name="frame" src="tagname_test.html"></iframe>
|
||||
|
||||
<p id="order-test"></p>
|
||||
|
||||
<div id="testAncestorDiv" class="ancestorClassA testAncestor">
|
||||
<p id="testAncestorP" class="ancestorClassB testAncestor">
|
||||
<b id="nestedElement">ancestorTest</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="noTabIndex">Test</div>
|
||||
<div id="tabIndexNegative2" tabindex="-2">Test</div>
|
||||
<div id="tabIndexNegative1" tabindex="-1">Test</div>
|
||||
<div id="tabIndex0" tabindex="0">Test</div>
|
||||
<div id="tabIndex1" tabindex="1">Test</div>
|
||||
<div id="tabIndex2" tabindex="2">Test</div>
|
||||
|
||||
<form>
|
||||
<a href="testUrl" id="noTabIndexAnchor">Test</a>
|
||||
<input id="noTabIndexInput">
|
||||
<textarea id="noTabIndexTextArea">Test</textarea>
|
||||
<select id="noTabIndexSelect"><option>Test</option></select>
|
||||
<button id="noTabIndexButton">Test</button>
|
||||
<button id="negTabIndexButton" tabindex="-1">Test</button>
|
||||
<button id="zeroTabIndexButton" tabindex="0">Test</button>
|
||||
<button id="posTabIndexButton" tabindex="1">Test</button>
|
||||
<button id="disabledNoTabIndexButton" disabled>Test</button>
|
||||
<button id="disabledNegTabIndexButton" disabled tabindex="-1">Test</button>
|
||||
<button id="disabledZeroTabIndexButton" disabled tabindex="0">Test</button>
|
||||
<button id="disabledPosTabIndexButton" disabled tabindex="1">Test</button>
|
||||
</form>
|
||||
|
||||
<div id="toReplace">Replace Test</div>
|
||||
|
||||
<iframe id="iframe"></iframe>
|
||||
|
||||
<div id="myIframeDiv1" style="height:42px;font-size:1px;line-height:0;">hello world</div>
|
||||
<div id="myIframeDiv2" style="height:23px;font-size:1px;line-height:0;">hello world</div>
|
||||
|
||||
<iframe id="myIframe" style="width:400px;height:200px;"></iframe>
|
||||
|
||||
<a id='link' href='foo.html'>Foo</a>
|
||||
|
||||
<svg id="testSvg" width="100" height="100" viewPort="0 0 100 100" version="1.0">
|
||||
<g id="testG">
|
||||
<rect id="testRect" x="10" y="10" width="50" height="50"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<script src="dom_test.js"></script></body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
// Copyright 2005 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 A class that can be used to listen to font size changes.
|
||||
* @author arv@google.com (Erik Arvidsson)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.FontSizeMonitor');
|
||||
goog.provide('goog.dom.FontSizeMonitor.EventType');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
// TODO(arv): Move this to goog.events instead.
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class can be used to monitor changes in font size. Instances will
|
||||
* dispatch a {@code goog.dom.FontSizeMonitor.EventType.CHANGE} event.
|
||||
* Example usage:
|
||||
* <pre>
|
||||
* var fms = new goog.dom.FontSizeMonitor();
|
||||
* goog.events.listen(fms, goog.dom.FontSizeMonitor.EventType.CHANGE,
|
||||
* function(e) {
|
||||
* alert('Font size was changed');
|
||||
* });
|
||||
* </pre>
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper object that is used to
|
||||
* determine where to insert the DOM nodes used to determine when the font
|
||||
* size changes.
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.FontSizeMonitor = function(opt_domHelper) {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
var dom = opt_domHelper || goog.dom.getDomHelper();
|
||||
|
||||
/**
|
||||
* Offscreen iframe which we use to detect resize events.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this.sizeElement_ = dom.createDom(
|
||||
// The size of the iframe is expressed in em, which are font size relative
|
||||
// which will cause the iframe to be resized when the font size changes.
|
||||
// The actual values are not relevant as long as we can ensure that the
|
||||
// iframe has a non zero size and is completely off screen.
|
||||
goog.userAgent.IE ? 'div' : 'iframe', {
|
||||
'style': 'position:absolute;width:9em;height:9em;top:-99em',
|
||||
'tabIndex': -1,
|
||||
'aria-hidden': 'true'
|
||||
});
|
||||
var p = dom.getDocument().body;
|
||||
p.insertBefore(this.sizeElement_, p.firstChild);
|
||||
|
||||
/**
|
||||
* The object that we listen to resize events on.
|
||||
* @type {Element|Window}
|
||||
* @private
|
||||
*/
|
||||
var resizeTarget = this.resizeTarget_ =
|
||||
goog.userAgent.IE ? this.sizeElement_ :
|
||||
goog.dom.getFrameContentWindow(
|
||||
/** @type {HTMLIFrameElement} */ (this.sizeElement_));
|
||||
|
||||
// We need to open and close the document to get Firefox 2 to work. We must
|
||||
// not do this for IE in case we are using HTTPS since accessing the document
|
||||
// on an about:blank iframe in IE using HTTPS raises a Permission Denied
|
||||
// error.
|
||||
if (goog.userAgent.GECKO) {
|
||||
var doc = resizeTarget.document;
|
||||
doc.open();
|
||||
doc.close();
|
||||
}
|
||||
|
||||
// Listen to resize event on the window inside the iframe.
|
||||
goog.events.listen(resizeTarget, goog.events.EventType.RESIZE,
|
||||
this.handleResize_, false, this);
|
||||
|
||||
/**
|
||||
* Last measured width of the iframe element.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.lastWidth_ = this.sizeElement_.offsetWidth;
|
||||
};
|
||||
goog.inherits(goog.dom.FontSizeMonitor, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* The event types that the FontSizeMonitor fires.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.FontSizeMonitor.EventType = {
|
||||
// TODO(arv): Change value to 'change' after updating the callers.
|
||||
CHANGE: 'fontsizechange'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Constant for the change event.
|
||||
* @type {string}
|
||||
* @deprecated Use {@code goog.dom.FontSizeMonitor.EventType.CHANGE} instead.
|
||||
*/
|
||||
goog.dom.FontSizeMonitor.CHANGE_EVENT =
|
||||
goog.dom.FontSizeMonitor.EventType.CHANGE;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.FontSizeMonitor.prototype.disposeInternal = function() {
|
||||
goog.dom.FontSizeMonitor.superClass_.disposeInternal.call(this);
|
||||
|
||||
goog.events.unlisten(this.resizeTarget_, goog.events.EventType.RESIZE,
|
||||
this.handleResize_, false, this);
|
||||
this.resizeTarget_ = null;
|
||||
|
||||
// Firefox 2 crashes if the iframe is removed during the unload phase.
|
||||
if (!goog.userAgent.GECKO ||
|
||||
goog.userAgent.isVersionOrHigher('1.9')) {
|
||||
goog.dom.removeNode(this.sizeElement_);
|
||||
}
|
||||
delete this.sizeElement_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the onresize event of the iframe and dispatches a change event in
|
||||
* case its size really changed.
|
||||
* @param {goog.events.BrowserEvent} e The event object.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.FontSizeMonitor.prototype.handleResize_ = function(e) {
|
||||
// Only dispatch the event if the size really changed. Some newer browsers do
|
||||
// not really change the font-size, instead they zoom the whole page. This
|
||||
// does trigger window resize events on the iframe but the logical pixel size
|
||||
// remains the same (the device pixel size changes but that is irrelevant).
|
||||
var currentWidth = this.sizeElement_.offsetWidth;
|
||||
if (this.lastWidth_ != currentWidth) {
|
||||
this.lastWidth_ = currentWidth;
|
||||
this.dispatchEvent(goog.dom.FontSizeMonitor.EventType.CHANGE);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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.
|
||||
-->
|
||||
<!--
|
||||
Author: arv@google.com (Erik Arvidsson)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom.FontSizeMonitor</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.FontSizeMonitorTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- iframe to be used to test DomHelper support -->
|
||||
<iframe></iframe>
|
||||
|
||||
<!--
|
||||
This div has a script in it that creates a FontSizeMonitor. This ensures that
|
||||
we do not get an "Operation abort" error in IE.
|
||||
-->
|
||||
<div>
|
||||
<script>
|
||||
var operationAbortTester = new goog.dom.FontSizeMonitor();
|
||||
// Close script tag before disposing.
|
||||
</script>
|
||||
<script>
|
||||
operationAbortTester.dispose();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2008 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.FontSizeMonitorTest');
|
||||
goog.setTestOnly('goog.dom.FontSizeMonitorTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.FontSizeMonitor');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.testing.PropertyReplacer');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
function isBuggyGecko() {
|
||||
return goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('1.9');
|
||||
}
|
||||
|
||||
var monitor;
|
||||
|
||||
function setUp() {
|
||||
monitor = new goog.dom.FontSizeMonitor();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
monitor.dispose();
|
||||
}
|
||||
|
||||
function getResizeTarget() {
|
||||
return goog.userAgent.IE ? monitor.sizeElement_ :
|
||||
goog.dom.getFrameContentWindow(monitor.sizeElement_);
|
||||
}
|
||||
|
||||
function testFontSizeNoChange() {
|
||||
// This tests that firing the resize event without changing the font-size
|
||||
// does not trigger the event.
|
||||
|
||||
var fired = false;
|
||||
goog.events.listen(monitor, goog.dom.FontSizeMonitor.EventType.CHANGE,
|
||||
function(e) {
|
||||
fired = true;
|
||||
});
|
||||
|
||||
var resizeEvent = new goog.events.Event('resize', getResizeTarget());
|
||||
goog.testing.events.fireBrowserEvent(resizeEvent);
|
||||
|
||||
assertFalse('The font size should not have changed', fired);
|
||||
}
|
||||
|
||||
function testFontSizeChanged() {
|
||||
// One can trigger the iframe resize by changing the
|
||||
// document.body.style.fontSize but the event is fired asynchronously in
|
||||
// Firefox. Instead, we just override the lastWidth_ to simulate that the
|
||||
// size changed.
|
||||
|
||||
var fired = false;
|
||||
goog.events.listen(monitor, goog.dom.FontSizeMonitor.EventType.CHANGE,
|
||||
function(e) {
|
||||
fired = true;
|
||||
});
|
||||
|
||||
monitor.lastWidth_--;
|
||||
|
||||
var resizeEvent = new goog.events.Event('resize', getResizeTarget());
|
||||
goog.testing.events.fireBrowserEvent(resizeEvent);
|
||||
|
||||
assertTrue('The font size should have changed', fired);
|
||||
}
|
||||
|
||||
function testCreateAndDispose() {
|
||||
var frameCount = window.frames.length;
|
||||
var iframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var divElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
var monitor = new goog.dom.FontSizeMonitor();
|
||||
monitor.dispose();
|
||||
|
||||
var newFrameCount = window.frames.length;
|
||||
var newIframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var newDivElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
assertEquals('There should be no trailing frames',
|
||||
frameCount + isBuggyGecko(), newFrameCount);
|
||||
assertEquals('There should be no trailing iframe elements',
|
||||
iframeElementCount + isBuggyGecko(),
|
||||
newIframeElementCount);
|
||||
assertEquals('There should be no trailing div elements',
|
||||
divElementCount, newDivElementCount);
|
||||
}
|
||||
|
||||
function testWithDomHelper() {
|
||||
var frameCount = window.frames.length;
|
||||
var iframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var divElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
var monitor = new goog.dom.FontSizeMonitor(goog.dom.getDomHelper());
|
||||
|
||||
var newFrameCount = window.frames.length;
|
||||
var newIframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var newDivElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
if (goog.userAgent.IE) {
|
||||
assertEquals('There should be one new div element',
|
||||
divElementCount + 1, newDivElementCount);
|
||||
} else {
|
||||
assertEquals('There should be one new frame',
|
||||
frameCount + 1, newFrameCount);
|
||||
assertEquals('There should be one new iframe element',
|
||||
iframeElementCount + 1, newIframeElementCount);
|
||||
}
|
||||
|
||||
// Use the first iframe in the doc. This is added in the HTML markup.
|
||||
var win = window.frames[0];
|
||||
var doc = win.document;
|
||||
doc.open();
|
||||
doc.write('<html><body></body></html>');
|
||||
doc.close();
|
||||
var domHelper = goog.dom.getDomHelper(doc);
|
||||
|
||||
var frameCount2 = win.frames.length;
|
||||
var iframeElementCount2 = doc.getElementsByTagName('iframe').length;
|
||||
var divElementCount2 = doc.getElementsByTagName('div').length;
|
||||
|
||||
var monitor2 = new goog.dom.FontSizeMonitor(domHelper);
|
||||
|
||||
var newFrameCount2 = win.frames.length;
|
||||
var newIframeElementCount2 = doc.getElementsByTagName('iframe').length;
|
||||
var newDivElementCount2 = doc.getElementsByTagName('div').length;
|
||||
|
||||
if (goog.userAgent.IE) {
|
||||
assertEquals('There should be one new div element',
|
||||
divElementCount2 + 1, newDivElementCount2);
|
||||
} else {
|
||||
assertEquals('There should be one new frame', frameCount2 + 1,
|
||||
newFrameCount2);
|
||||
assertEquals('There should be one new iframe element',
|
||||
iframeElementCount2 + 1, newIframeElementCount2);
|
||||
}
|
||||
|
||||
monitor.dispose();
|
||||
monitor2.dispose();
|
||||
}
|
||||
|
||||
function testEnsureThatDocIsOpenedForGecko() {
|
||||
|
||||
var pr = new goog.testing.PropertyReplacer();
|
||||
pr.set(goog.userAgent, 'GECKO', true);
|
||||
pr.set(goog.userAgent, 'IE', false);
|
||||
|
||||
var openCalled = false;
|
||||
var closeCalled = false;
|
||||
var instance = {
|
||||
document: {
|
||||
open: function() {
|
||||
openCalled = true;
|
||||
},
|
||||
close: function() {
|
||||
closeCalled = true;
|
||||
}
|
||||
},
|
||||
attachEvent: function() {}
|
||||
};
|
||||
|
||||
pr.set(goog.dom, 'getFrameContentWindow', function() {
|
||||
return instance;
|
||||
});
|
||||
|
||||
try {
|
||||
var monitor = new goog.dom.FontSizeMonitor();
|
||||
|
||||
assertTrue('doc.open should have been called', openCalled);
|
||||
assertTrue('doc.close should have been called', closeCalled);
|
||||
|
||||
monitor.dispose();
|
||||
} finally {
|
||||
pr.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function testFirefox2WorkAroundFirefox3() {
|
||||
var pr = new goog.testing.PropertyReplacer();
|
||||
pr.set(goog.userAgent, 'GECKO', true);
|
||||
pr.set(goog.userAgent, 'IE', false);
|
||||
|
||||
try {
|
||||
// 1.9 should clear iframes
|
||||
pr.set(goog.userAgent, 'VERSION', '1.9');
|
||||
goog.userAgent.isVersionOrHigherCache_ = {};
|
||||
|
||||
var frameCount = window.frames.length;
|
||||
var iframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var divElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
var monitor = new goog.dom.FontSizeMonitor();
|
||||
monitor.dispose();
|
||||
|
||||
var newFrameCount = window.frames.length;
|
||||
var newIframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var newDivElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
assertEquals('There should be no trailing frames',
|
||||
frameCount, newFrameCount);
|
||||
assertEquals('There should be no trailing iframe elements',
|
||||
iframeElementCount,
|
||||
newIframeElementCount);
|
||||
assertEquals('There should be no trailing div elements',
|
||||
divElementCount, newDivElementCount);
|
||||
} finally {
|
||||
pr.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function testFirefox2WorkAroundFirefox2() {
|
||||
var pr = new goog.testing.PropertyReplacer();
|
||||
pr.set(goog.userAgent, 'GECKO', true);
|
||||
pr.set(goog.userAgent, 'IE', false);
|
||||
|
||||
try {
|
||||
// 1.8 should NOT clear iframes
|
||||
pr.set(goog.userAgent, 'VERSION', '1.8');
|
||||
goog.userAgent.isVersionOrHigherCache_ = {};
|
||||
|
||||
var frameCount = window.frames.length;
|
||||
var iframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var divElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
var monitor = new goog.dom.FontSizeMonitor();
|
||||
monitor.dispose();
|
||||
|
||||
var newFrameCount = window.frames.length;
|
||||
var newIframeElementCount = document.getElementsByTagName('iframe').length;
|
||||
var newDivElementCount = document.getElementsByTagName('div').length;
|
||||
|
||||
assertEquals('There should be no trailing frames',
|
||||
frameCount + 1, newFrameCount);
|
||||
assertEquals('There should be no trailing iframe elements',
|
||||
iframeElementCount + 1,
|
||||
newIframeElementCount);
|
||||
assertEquals('There should be no trailing div elements',
|
||||
divElementCount, newDivElementCount);
|
||||
} finally {
|
||||
pr.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for manipulating a form and elements.
|
||||
*
|
||||
* @author arv@google.com (Erik Arvidsson)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.forms');
|
||||
|
||||
goog.require('goog.structs.Map');
|
||||
|
||||
|
||||
/**
|
||||
* Returns form data as a map of name to value arrays. This doesn't
|
||||
* support file inputs.
|
||||
* @param {HTMLFormElement} form The form.
|
||||
* @return {!goog.structs.Map.<string, !Array.<string>>} A map of the form data
|
||||
* as field name to arrays of values.
|
||||
*/
|
||||
goog.dom.forms.getFormDataMap = function(form) {
|
||||
var map = new goog.structs.Map();
|
||||
goog.dom.forms.getFormDataHelper_(form, map,
|
||||
goog.dom.forms.addFormDataToMap_);
|
||||
return map;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the form data as an application/x-www-url-encoded string. This
|
||||
* doesn't support file inputs.
|
||||
* @param {HTMLFormElement} form The form.
|
||||
* @return {string} An application/x-www-url-encoded string.
|
||||
*/
|
||||
goog.dom.forms.getFormDataString = function(form) {
|
||||
var sb = [];
|
||||
goog.dom.forms.getFormDataHelper_(form, sb,
|
||||
goog.dom.forms.addFormDataToStringBuffer_);
|
||||
return sb.join('&');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the form data as a map or an application/x-www-url-encoded
|
||||
* string. This doesn't support file inputs.
|
||||
* @param {HTMLFormElement} form The form.
|
||||
* @param {Object} result The object form data is being put in.
|
||||
* @param {Function} fnAppend Function that takes {@code result}, an element
|
||||
* name, and an element value, and adds the name/value pair to the result
|
||||
* object.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.getFormDataHelper_ = function(form, result, fnAppend) {
|
||||
var els = form.elements;
|
||||
for (var el, i = 0; el = els[i]; i++) {
|
||||
if (// Make sure we don't include elements that are not part of the form.
|
||||
// Some browsers include non-form elements. Check for 'form' property.
|
||||
// See http://code.google.com/p/closure-library/issues/detail?id=227
|
||||
// and
|
||||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#the-input-element
|
||||
(el.form != form) ||
|
||||
el.disabled ||
|
||||
// HTMLFieldSetElement has a form property but no value.
|
||||
el.tagName.toLowerCase() == 'fieldset') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = el.name;
|
||||
switch (el.type.toLowerCase()) {
|
||||
case 'file':
|
||||
// file inputs are not supported
|
||||
case 'submit':
|
||||
case 'reset':
|
||||
case 'button':
|
||||
// don't submit these
|
||||
break;
|
||||
case 'select-multiple':
|
||||
var values = goog.dom.forms.getValue(el);
|
||||
if (values != null) {
|
||||
for (var value, j = 0; value = values[j]; j++) {
|
||||
fnAppend(result, name, value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var value = goog.dom.forms.getValue(el);
|
||||
if (value != null) {
|
||||
fnAppend(result, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// input[type=image] are not included in the elements collection
|
||||
var inputs = form.getElementsByTagName('input');
|
||||
for (var input, i = 0; input = inputs[i]; i++) {
|
||||
if (input.form == form && input.type.toLowerCase() == 'image') {
|
||||
name = input.name;
|
||||
fnAppend(result, name, input.value);
|
||||
fnAppend(result, name + '.x', '0');
|
||||
fnAppend(result, name + '.y', '0');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds the name/value pair to the map.
|
||||
* @param {!goog.structs.Map.<string, !Array.<string>>} map The map to add to.
|
||||
* @param {string} name The name.
|
||||
* @param {string} value The value.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.addFormDataToMap_ = function(map, name, value) {
|
||||
var array = map.get(name);
|
||||
if (!array) {
|
||||
array = [];
|
||||
map.set(name, array);
|
||||
}
|
||||
array.push(value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a name/value pair to an string buffer array in the form 'name=value'.
|
||||
* @param {Array<string>} sb The string buffer array for storing data.
|
||||
* @param {string} name The name.
|
||||
* @param {string} value The value.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.addFormDataToStringBuffer_ = function(sb, name, value) {
|
||||
sb.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether the form has a file input.
|
||||
* @param {HTMLFormElement} form The form.
|
||||
* @return {boolean} Whether the form has a file input.
|
||||
*/
|
||||
goog.dom.forms.hasFileInput = function(form) {
|
||||
var els = form.elements;
|
||||
for (var el, i = 0; el = els[i]; i++) {
|
||||
if (!el.disabled && el.type && el.type.toLowerCase() == 'file') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Enables or disables either all elements in a form or a single form element.
|
||||
* @param {Element} el The element, either a form or an element within a form.
|
||||
* @param {boolean} disabled Whether the element should be disabled.
|
||||
*/
|
||||
goog.dom.forms.setDisabled = function(el, disabled) {
|
||||
// disable all elements in a form
|
||||
if (el.tagName == 'FORM') {
|
||||
var els = el.elements;
|
||||
for (var i = 0; el = els[i]; i++) {
|
||||
goog.dom.forms.setDisabled(el, disabled);
|
||||
}
|
||||
} else {
|
||||
// makes sure to blur buttons, multi-selects, and any elements which
|
||||
// maintain keyboard/accessibility focus when disabled
|
||||
if (disabled == true) {
|
||||
el.blur();
|
||||
}
|
||||
el.disabled = disabled;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Focuses, and optionally selects the content of, a form element.
|
||||
* @param {Element} el The form element.
|
||||
*/
|
||||
goog.dom.forms.focusAndSelect = function(el) {
|
||||
el.focus();
|
||||
if (el.select) {
|
||||
el.select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether a form element has a value.
|
||||
* @param {Element} el The element.
|
||||
* @return {boolean} Whether the form has a value.
|
||||
*/
|
||||
goog.dom.forms.hasValue = function(el) {
|
||||
var value = goog.dom.forms.getValue(el);
|
||||
return !!value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether a named form field has a value.
|
||||
* @param {HTMLFormElement} form The form element.
|
||||
* @param {string} name Name of an input to the form.
|
||||
* @return {boolean} Whether the form has a value.
|
||||
*/
|
||||
goog.dom.forms.hasValueByName = function(form, name) {
|
||||
var value = goog.dom.forms.getValueByName(form, name);
|
||||
return !!value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current value of any element with a type.
|
||||
* @param {Element} el The element.
|
||||
* @return {string|Array<string>|null} The current value of the element
|
||||
* (or null).
|
||||
*/
|
||||
goog.dom.forms.getValue = function(el) {
|
||||
var type = el.type;
|
||||
if (!goog.isDef(type)) {
|
||||
return null;
|
||||
}
|
||||
switch (type.toLowerCase()) {
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
return goog.dom.forms.getInputChecked_(el);
|
||||
case 'select-one':
|
||||
return goog.dom.forms.getSelectSingle_(el);
|
||||
case 'select-multiple':
|
||||
return goog.dom.forms.getSelectMultiple_(el);
|
||||
default:
|
||||
return goog.isDef(el.value) ? el.value : null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Alias for goog.dom.form.element.getValue
|
||||
* @type {Function}
|
||||
* @deprecated Use {@link goog.dom.forms.getValue} instead.
|
||||
* @suppress {missingProvide}
|
||||
*/
|
||||
goog.dom.$F = goog.dom.forms.getValue;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value of the named form field. In the case of radio buttons,
|
||||
* returns the value of the checked button with the given name.
|
||||
*
|
||||
* @param {HTMLFormElement} form The form element.
|
||||
* @param {string} name Name of an input to the form.
|
||||
*
|
||||
* @return {Array<string>|string|null} The value of the form element, or
|
||||
* null if the form element does not exist or has no value.
|
||||
*/
|
||||
goog.dom.forms.getValueByName = function(form, name) {
|
||||
var els = form.elements[name];
|
||||
|
||||
if (els) {
|
||||
if (els.type) {
|
||||
return goog.dom.forms.getValue(els);
|
||||
} else {
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var val = goog.dom.forms.getValue(els[i]);
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current value of a checkable input element.
|
||||
* @param {Element} el The element.
|
||||
* @return {?string} The value of the form element (or null).
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.getInputChecked_ = function(el) {
|
||||
return el.checked ? el.value : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current value of a select-one element.
|
||||
* @param {Element} el The element.
|
||||
* @return {?string} The value of the form element (or null).
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.getSelectSingle_ = function(el) {
|
||||
var selectedIndex = el.selectedIndex;
|
||||
return selectedIndex >= 0 ? el.options[selectedIndex].value : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current value of a select-multiple element.
|
||||
* @param {Element} el The element.
|
||||
* @return {Array<string>?} The value of the form element (or null).
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.getSelectMultiple_ = function(el) {
|
||||
var values = [];
|
||||
for (var option, i = 0; option = el.options[i]; i++) {
|
||||
if (option.selected) {
|
||||
values.push(option.value);
|
||||
}
|
||||
}
|
||||
return values.length ? values : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the current value of any element with a type.
|
||||
* @param {Element} el The element.
|
||||
* @param {*=} opt_value The value to give to the element, which will be coerced
|
||||
* by the browser in the default case using toString. This value should be
|
||||
* an array for setting the value of select multiple elements.
|
||||
*/
|
||||
goog.dom.forms.setValue = function(el, opt_value) {
|
||||
var type = el.type;
|
||||
if (goog.isDef(type)) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
goog.dom.forms.setInputChecked_(el,
|
||||
/** @type {string} */ (opt_value));
|
||||
break;
|
||||
case 'select-one':
|
||||
goog.dom.forms.setSelectSingle_(el,
|
||||
/** @type {string} */ (opt_value));
|
||||
break;
|
||||
case 'select-multiple':
|
||||
goog.dom.forms.setSelectMultiple_(el,
|
||||
/** @type {Array<string>} */ (opt_value));
|
||||
break;
|
||||
default:
|
||||
el.value = goog.isDefAndNotNull(opt_value) ? opt_value : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets a checkable input element's checked property.
|
||||
* #TODO(user): This seems potentially unintuitive since it doesn't set
|
||||
* the value property but my hunch is that the primary use case is to check a
|
||||
* checkbox, not to reset its value property.
|
||||
* @param {Element} el The element.
|
||||
* @param {string|boolean=} opt_value The value, sets the element checked if
|
||||
* val is set.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.setInputChecked_ = function(el, opt_value) {
|
||||
el.checked = opt_value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value of a select-one element.
|
||||
* @param {Element} el The element.
|
||||
* @param {string=} opt_value The value of the selected option element.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.setSelectSingle_ = function(el, opt_value) {
|
||||
// unset any prior selections
|
||||
el.selectedIndex = -1;
|
||||
if (goog.isString(opt_value)) {
|
||||
for (var option, i = 0; option = el.options[i]; i++) {
|
||||
if (option.value == opt_value) {
|
||||
option.selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value of a select-multiple element.
|
||||
* @param {Element} el The element.
|
||||
* @param {Array<string>|string=} opt_value The value of the selected option
|
||||
* element(s).
|
||||
* @private
|
||||
*/
|
||||
goog.dom.forms.setSelectMultiple_ = function(el, opt_value) {
|
||||
// reset string opt_values as an array
|
||||
if (goog.isString(opt_value)) {
|
||||
opt_value = [opt_value];
|
||||
}
|
||||
for (var option, i = 0; option = el.options[i]; i++) {
|
||||
// we have to reset the other options to false for select-multiple
|
||||
option.selected = false;
|
||||
if (opt_value) {
|
||||
for (var value, j = 0; value = opt_value[j]; j++) {
|
||||
if (option.value == value) {
|
||||
option.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 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.forms</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.formsTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- empty div to test against -->
|
||||
<div id="testdiv1"></div>
|
||||
|
||||
<form id="testform1" onsubmit="return false">
|
||||
|
||||
<!-- text input with one value -->
|
||||
<input id="in1" name="in1" value="foo">
|
||||
|
||||
<!-- text inputs with two values -->
|
||||
<input id="in2" name="in2" value="bar">
|
||||
<input id="in2" name="in2" value="baaz">
|
||||
|
||||
<!-- empty text input -->
|
||||
<input id="in3" name="in3" value="">
|
||||
|
||||
<!-- password -->
|
||||
<input id="pass" name="pass" type="password" value="bar">
|
||||
|
||||
<!-- textarea -->
|
||||
<textarea id="textarea1" name="textarea">foo bar baz</textarea>
|
||||
|
||||
<!-- select single -->
|
||||
<select id="select1" name="select1">
|
||||
<option value="1" selected>one</option>
|
||||
<option value="2">two</option>
|
||||
</select>
|
||||
|
||||
<!-- select multiple -->
|
||||
<select id="select2" name="select2" multiple=true>
|
||||
<option value="a" selected>A</option>
|
||||
<option value="b">B</option>
|
||||
<option value="c" selected>C</option>
|
||||
</select>
|
||||
|
||||
<!-- select no value -->
|
||||
<select id="select3" name="select3">
|
||||
<option></option>
|
||||
<option value="1">one</option>
|
||||
<option value="2">two</option>
|
||||
</select>
|
||||
|
||||
<!-- checkboxes -->
|
||||
<fieldset id="testfieldset1">
|
||||
<legend id="testlegend1">Checkboxes</legend>
|
||||
<input id="checkbox1" type="checkbox" name="checkbox1" checked>
|
||||
<input id="checkbox2" type="checkbox" name="checkbox2">
|
||||
</fieldset>
|
||||
|
||||
<!-- radio buttons -->
|
||||
<fieldset>
|
||||
<legend>Radio Buttons</legend>
|
||||
<input id="radio1" type="radio" name="radio" value="X" checked>
|
||||
<input id="radio2" type="radio" name="radio" value="Y">
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Radio Buttons</legend>
|
||||
<input id="radio3" type="radio" name="radio2" value="X">
|
||||
<input id="radio4" type="radio" name="radio2" value="Y" checked>
|
||||
</fieldset>
|
||||
|
||||
<!-- button -->
|
||||
<button id="button" name="button" type="button" value="button" onclick="testSetValueSelectMultiple()">button</button>
|
||||
|
||||
<!-- submit -->
|
||||
<input id="submit" type="submit" name="submit" value="submit">
|
||||
|
||||
<!-- reset -->
|
||||
<input id="reset" type="reset" name="reset" value="reset">
|
||||
|
||||
</form>
|
||||
|
||||
<form id="testform2">
|
||||
<input type="file" name="file">
|
||||
</form>
|
||||
|
||||
<form id="testform3">
|
||||
<!-- text input -->
|
||||
<input id="in4" name="in4">
|
||||
|
||||
<!-- textarea -->
|
||||
<textarea id="textarea2" name="textarea"></textarea>
|
||||
|
||||
<!-- select single -->
|
||||
<select id="select4" name="select1">
|
||||
<option value="1">one</option>
|
||||
<option value="2">two</option>
|
||||
</select>
|
||||
|
||||
<!-- select multiple -->
|
||||
<select id="select5" name="select5" multiple=true>
|
||||
<option value="a">A</option>
|
||||
<option value="b">B</option>
|
||||
<option value="c">C</option>
|
||||
</select>
|
||||
|
||||
<!-- radio -->
|
||||
<input id="radio3" type="radio" name="radio3" value="Z">
|
||||
|
||||
<!-- checkbox -->
|
||||
<input id="checkbox2" type="checkbox" name="checkbox2">
|
||||
|
||||
|
||||
<!-- select multiple no value -->
|
||||
<select id="select6" name="select6" multiple=true>
|
||||
<option value="a">A</option>
|
||||
<option value="b">B</option>
|
||||
</select>
|
||||
|
||||
<!-- select with empty value -->
|
||||
<select id="select7" name="select7">
|
||||
<option value="">Empty</option>
|
||||
<option value="a">A</option>
|
||||
<option value="b">B</option>
|
||||
</select>
|
||||
|
||||
</form>
|
||||
|
||||
<form id="testform4">
|
||||
<embed ></embed>
|
||||
<embed type="foo/bar"></embed>
|
||||
<object></object>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,377 @@
|
||||
// Copyright 2006 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.formsTest');
|
||||
goog.setTestOnly('goog.dom.formsTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.forms');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
function testGetFormDataString() {
|
||||
var el = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getFormDataString(el);
|
||||
assertEquals(
|
||||
'in1=foo&in2=bar&in2=baaz&in3=&pass=bar&textarea=foo%20bar%20baz&' +
|
||||
'select1=1&select2=a&select2=c&select3=&checkbox1=on&radio=X&radio2=Y',
|
||||
result);
|
||||
}
|
||||
|
||||
function testGetFormDataMap() {
|
||||
var el = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getFormDataMap(el);
|
||||
|
||||
assertArrayEquals(['foo'], result.get('in1'));
|
||||
assertArrayEquals(['bar', 'baaz'], result.get('in2'));
|
||||
assertArrayEquals(['1'], result.get('select1'));
|
||||
assertArrayEquals(['a', 'c'], result.get('select2'));
|
||||
assertArrayEquals(['on'], result.get('checkbox1'));
|
||||
assertUndefined(result.get('select6'));
|
||||
assertUndefined(result.get('checkbox2'));
|
||||
assertArrayEquals(['X'], result.get('radio'));
|
||||
assertArrayEquals(['Y'], result.get('radio2'));
|
||||
}
|
||||
|
||||
function testHasFileInput() {
|
||||
var el = goog.dom.getElement('testform1');
|
||||
assertFalse(goog.dom.forms.hasFileInput(el));
|
||||
el = goog.dom.getElement('testform2');
|
||||
assertTrue(goog.dom.forms.hasFileInput(el));
|
||||
}
|
||||
|
||||
|
||||
function testGetValueOnAtypicalValueElements() {
|
||||
var el = goog.dom.getElement('testdiv1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
var el = goog.dom.getElement('testfieldset1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
var el = goog.dom.getElement('testlegend1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testHasValueInput() {
|
||||
var el = goog.dom.getElement('in1');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testGetValueByNameForNonExistentElement() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'non_existent');
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameInput() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'in1');
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueInputEmpty() {
|
||||
var el = goog.dom.getElement('in3');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameEmpty() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'in3');
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueRadio() {
|
||||
var el = goog.dom.getElement('radio1');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameRadio() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'radio');
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueRadioNotChecked() {
|
||||
var el = goog.dom.getElement('radio2');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameRadioNotChecked() {
|
||||
var form = goog.dom.getElement('testform3');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'radio3');
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueSelectSingle() {
|
||||
var el = goog.dom.getElement('select1');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameSelectSingle() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'select1');
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueSelectMultiple() {
|
||||
var el = goog.dom.getElement('select2');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameSelectMultiple() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'select2');
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
function testHasValueSelectNotSelected() {
|
||||
// select without value
|
||||
var el = goog.dom.getElement('select3');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameSelectNotSelected() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'select3');
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueSelectMultipleNotSelected() {
|
||||
var el = goog.dom.getElement('select6');
|
||||
var result = goog.dom.forms.hasValue(el);
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
function testHasValueByNameSelectMultipleNotSelected() {
|
||||
var form = goog.dom.getElement('testform3');
|
||||
var result = goog.dom.forms.hasValueByName(form, 'select6');
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
// TODO(user): make this a meaningful selenium test
|
||||
function testSetDisabledFalse() {
|
||||
}
|
||||
function testSetDisabledTrue() {
|
||||
}
|
||||
|
||||
// TODO(user): make this a meaningful selenium test
|
||||
function testFocusAndSelect() {
|
||||
var el = goog.dom.getElement('in1');
|
||||
goog.dom.forms.focusAndSelect(el);
|
||||
}
|
||||
|
||||
function testGetValueInput() {
|
||||
var el = goog.dom.getElement('in1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('foo', result);
|
||||
}
|
||||
|
||||
function testSetValueInput() {
|
||||
var el = goog.dom.getElement('in3');
|
||||
goog.dom.forms.setValue(el, 'foo');
|
||||
assertEquals('foo', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, 3500);
|
||||
assertEquals('3500', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, 0);
|
||||
assertEquals('0', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, null);
|
||||
assertEquals('', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, undefined);
|
||||
assertEquals('', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, false);
|
||||
assertEquals('false', goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, {});
|
||||
assertEquals({}.toString(), goog.dom.forms.getValue(el));
|
||||
|
||||
goog.dom.forms.setValue(el, {
|
||||
toString: function() {
|
||||
return 'test';
|
||||
}
|
||||
});
|
||||
assertEquals('test', goog.dom.forms.getValue(el));
|
||||
|
||||
// unset
|
||||
goog.dom.forms.setValue(el);
|
||||
assertEquals('', goog.dom.forms.getValue(el));
|
||||
}
|
||||
|
||||
function testGetValuePassword() {
|
||||
var el = goog.dom.getElement('pass');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('bar', result);
|
||||
}
|
||||
|
||||
function testGetValueByNamePassword() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'pass');
|
||||
assertEquals('bar', result);
|
||||
}
|
||||
|
||||
function testGetValueTextarea() {
|
||||
var el = goog.dom.getElement('textarea1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('foo bar baz', result);
|
||||
}
|
||||
|
||||
function testGetValueByNameTextarea() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'textarea1');
|
||||
assertEquals('foo bar baz', result);
|
||||
}
|
||||
|
||||
function testSetValueTextarea() {
|
||||
var el = goog.dom.getElement('textarea2');
|
||||
goog.dom.forms.setValue(el, 'foo bar baz');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('foo bar baz', result);
|
||||
}
|
||||
|
||||
function testGetValueSelectSingle() {
|
||||
var el = goog.dom.getElement('select1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('1', result);
|
||||
}
|
||||
|
||||
function testGetValueByNameSelectSingle() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'select1');
|
||||
assertEquals('1', result);
|
||||
}
|
||||
|
||||
function testSetValueSelectSingle() {
|
||||
var el = goog.dom.getElement('select4');
|
||||
goog.dom.forms.setValue(el, '2');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('2', result);
|
||||
// unset
|
||||
goog.dom.forms.setValue(el);
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testSetValueSelectSingleEmptyString() {
|
||||
var el = goog.dom.getElement('select7');
|
||||
// unset
|
||||
goog.dom.forms.setValue(el);
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
goog.dom.forms.setValue(el, '');
|
||||
result = goog.dom.forms.getValue(el);
|
||||
assertEquals('', result);
|
||||
}
|
||||
|
||||
function testGetValueSelectMultiple() {
|
||||
var el = goog.dom.getElement('select2');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertArrayEquals(['a', 'c'], result);
|
||||
}
|
||||
|
||||
function testGetValueSelectMultipleNotSelected() {
|
||||
var el = goog.dom.getElement('select6');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testGetValueByNameSelectMultiple() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'select2');
|
||||
assertArrayEquals(['a', 'c'], result);
|
||||
}
|
||||
|
||||
function testSetValueSelectMultiple() {
|
||||
var el = goog.dom.getElement('select5');
|
||||
goog.dom.forms.setValue(el, ['a', 'c']);
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertArrayEquals(['a', 'c'], result);
|
||||
|
||||
goog.dom.forms.setValue(el, 'a');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertArrayEquals(['a'], result);
|
||||
|
||||
// unset
|
||||
goog.dom.forms.setValue(el);
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testGetValueCheckbox() {
|
||||
var el = goog.dom.getElement('checkbox1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('on', result);
|
||||
var el = goog.dom.getElement('checkbox2');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testGetValueByNameCheckbox() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'checkbox1');
|
||||
assertEquals('on', result);
|
||||
result = goog.dom.forms.getValueByName(form, 'checkbox2');
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testGetValueRadio() {
|
||||
var el = goog.dom.getElement('radio1');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('X', result);
|
||||
var el = goog.dom.getElement('radio2');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
function testGetValueByNameRadio() {
|
||||
var form = goog.dom.getElement('testform1');
|
||||
var result = goog.dom.forms.getValueByName(form, 'radio');
|
||||
assertEquals('X', result);
|
||||
|
||||
result = goog.dom.forms.getValueByName(form, 'radio2');
|
||||
assertEquals('Y', result);
|
||||
}
|
||||
|
||||
function testGetValueButton() {
|
||||
var el = goog.dom.getElement('button');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('button', result);
|
||||
}
|
||||
|
||||
function testGetValueSubmit() {
|
||||
var el = goog.dom.getElement('submit');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('submit', result);
|
||||
}
|
||||
|
||||
function testGetValueReset() {
|
||||
var el = goog.dom.getElement('reset');
|
||||
var result = goog.dom.forms.getValue(el);
|
||||
assertEquals('reset', result);
|
||||
}
|
||||
|
||||
function testGetFormDataHelperAndNonInputElements() {
|
||||
var el = goog.dom.getElement('testform4');
|
||||
goog.dom.forms.getFormDataHelper_(el, {}, goog.nullFunction);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2012 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 Functions for managing full screen status of the DOM.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.fullscreen');
|
||||
goog.provide('goog.dom.fullscreen.EventType');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Event types for full screen.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.fullscreen.EventType = {
|
||||
/** Dispatched by the Document when the fullscreen status changes. */
|
||||
CHANGE: (function() {
|
||||
if (goog.userAgent.WEBKIT) {
|
||||
return 'webkitfullscreenchange';
|
||||
}
|
||||
if (goog.userAgent.GECKO) {
|
||||
return 'mozfullscreenchange';
|
||||
}
|
||||
if (goog.userAgent.IE) {
|
||||
return 'MSFullscreenChange';
|
||||
}
|
||||
// Opera 12-14, and W3C standard (Draft):
|
||||
// https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html
|
||||
return 'fullscreenchange';
|
||||
})()
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if full screen is supported.
|
||||
* @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
|
||||
* queried. If not provided, use the current DOM.
|
||||
* @return {boolean} True iff full screen is supported.
|
||||
*/
|
||||
goog.dom.fullscreen.isSupported = function(opt_domHelper) {
|
||||
var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
|
||||
var body = doc.body;
|
||||
return !!(body.webkitRequestFullscreen ||
|
||||
(body.mozRequestFullScreen && doc.mozFullScreenEnabled) ||
|
||||
(body.msRequestFullscreen && doc.msFullscreenEnabled) ||
|
||||
(body.requestFullscreen && doc.fullscreenEnabled));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Requests putting the element in full screen.
|
||||
* @param {!Element} element The element to put full screen.
|
||||
*/
|
||||
goog.dom.fullscreen.requestFullScreen = function(element) {
|
||||
if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen();
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen();
|
||||
} else if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Requests putting the element in full screen with full keyboard access.
|
||||
* @param {!Element} element The element to put full screen.
|
||||
*/
|
||||
goog.dom.fullscreen.requestFullScreenWithKeys = function(
|
||||
element) {
|
||||
if (element.mozRequestFullScreenWithKeys) {
|
||||
element.mozRequestFullScreenWithKeys();
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else {
|
||||
goog.dom.fullscreen.requestFullScreen(element);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Exits full screen.
|
||||
* @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
|
||||
* queried. If not provided, use the current DOM.
|
||||
*/
|
||||
goog.dom.fullscreen.exitFullScreen = function(opt_domHelper) {
|
||||
var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
|
||||
if (doc.webkitCancelFullScreen) {
|
||||
doc.webkitCancelFullScreen();
|
||||
} else if (doc.mozCancelFullScreen) {
|
||||
doc.mozCancelFullScreen();
|
||||
} else if (doc.msExitFullscreen) {
|
||||
doc.msExitFullscreen();
|
||||
} else if (doc.exitFullscreen) {
|
||||
doc.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the document is full screen.
|
||||
* @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
|
||||
* queried. If not provided, use the current DOM.
|
||||
* @return {boolean} Whether the document is full screen.
|
||||
*/
|
||||
goog.dom.fullscreen.isFullScreen = function(opt_domHelper) {
|
||||
var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
|
||||
// IE 11 doesn't have similar boolean property, so check whether
|
||||
// document.msFullscreenElement is null instead.
|
||||
return !!(doc.webkitIsFullScreen || doc.mozFullScreen ||
|
||||
doc.msFullscreenElement || doc.fullscreenElement);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the document object of the dom.
|
||||
* @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
|
||||
* queried. If not provided, use the current DOM.
|
||||
* @return {!Document} The dom document.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.fullscreen.getDocument_ = function(opt_domHelper) {
|
||||
return opt_domHelper ?
|
||||
opt_domHelper.getDocument() :
|
||||
goog.dom.getDomHelper().getDocument();
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for creating and working with iframes
|
||||
* cross-browser.
|
||||
* @author gboyer@google.com (Garry Boyer)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.iframe');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Safe source for a blank iframe.
|
||||
*
|
||||
* Intentionally not about:blank, which gives mixed content warnings in IE6
|
||||
* over HTTPS.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
goog.dom.iframe.BLANK_SOURCE = 'javascript:""';
|
||||
|
||||
|
||||
/**
|
||||
* Safe source for a new blank iframe that may not cause a new load of the
|
||||
* iframe. This is different from {@code goog.dom.iframe.BLANK_SOURCE} in that
|
||||
* it will allow an iframe to be loaded synchronously in more browsers, notably
|
||||
* Gecko, following the javascript protocol spec.
|
||||
*
|
||||
* NOTE: This should not be used to replace the source of an existing iframe.
|
||||
* The new src value will be ignored, per the spec.
|
||||
*
|
||||
* Due to cross-browser differences, the load is not guaranteed to be
|
||||
* synchronous. If code depends on the load of the iframe,
|
||||
* then {@code goog.net.IframeLoadMonitor} or a similar technique should be
|
||||
* used.
|
||||
*
|
||||
* According to
|
||||
* http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#javascript-protocol
|
||||
* the 'javascript:""' URL should trigger a new load of the iframe, which may be
|
||||
* asynchronous. A void src, such as 'javascript:undefined', does not change
|
||||
* the browsing context document's, and thus should not trigger another load.
|
||||
*
|
||||
* Intentionally not about:blank, which also triggers a load.
|
||||
*
|
||||
* NOTE: 'javascript:' URL handling spec compliance varies per browser. IE
|
||||
* throws an error with 'javascript:undefined'. Webkit browsers will reload the
|
||||
* iframe when setting this source on an existing iframe.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
goog.dom.iframe.BLANK_SOURCE_NEW_FRAME = goog.userAgent.IE ?
|
||||
'javascript:""' :
|
||||
'javascript:undefined';
|
||||
|
||||
|
||||
/**
|
||||
* Styles to help ensure an undecorated iframe.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.iframe.STYLES_ = 'border:0;vertical-align:bottom;';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a completely blank iframe element.
|
||||
*
|
||||
* The iframe will not caused mixed-content warnings for IE6 under HTTPS.
|
||||
* The iframe will also have no borders or padding, so that the styled width
|
||||
* and height will be the actual width and height of the iframe.
|
||||
*
|
||||
* This function currently only attempts to create a blank iframe. There
|
||||
* are no guarantees to the contents of the iframe or whether it is rendered
|
||||
* in quirks mode.
|
||||
*
|
||||
* @param {goog.dom.DomHelper} domHelper The dom helper to use.
|
||||
* @param {string=} opt_styles CSS styles for the iframe.
|
||||
* @return {!HTMLIFrameElement} A completely blank iframe.
|
||||
*/
|
||||
goog.dom.iframe.createBlank = function(domHelper, opt_styles) {
|
||||
return /** @type {!HTMLIFrameElement} */ (domHelper.createDom('iframe', {
|
||||
'frameborder': 0,
|
||||
// Since iframes are inline elements, we must align to bottom to
|
||||
// compensate for the line descent.
|
||||
'style': goog.dom.iframe.STYLES_ + (opt_styles || ''),
|
||||
'src': goog.dom.iframe.BLANK_SOURCE
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Writes the contents of a blank iframe that has already been inserted
|
||||
* into the document.
|
||||
* @param {!HTMLIFrameElement} iframe An iframe with no contents, such as
|
||||
* one created by goog.dom.iframe.createBlank, but already appended to
|
||||
* a parent document.
|
||||
* @param {string} content Content to write to the iframe, from doctype to
|
||||
* the HTML close tag.
|
||||
*/
|
||||
goog.dom.iframe.writeContent = function(iframe, content) {
|
||||
var doc = goog.dom.getFrameContentDocument(iframe);
|
||||
doc.open();
|
||||
doc.write(content);
|
||||
doc.close();
|
||||
};
|
||||
|
||||
|
||||
// TODO(gboyer): Provide a higher-level API for the most common use case, so
|
||||
// that you can just provide a list of stylesheets and some content HTML.
|
||||
/**
|
||||
* Creates a same-domain iframe containing preloaded content.
|
||||
*
|
||||
* This is primarily useful for DOM sandboxing. One use case is to embed
|
||||
* a trusted Javascript app with potentially conflicting CSS styles. The
|
||||
* second case is to reduce the cost of layout passes by the browser -- for
|
||||
* example, you can perform sandbox sizing of characters in an iframe while
|
||||
* manipulating a heavy DOM in the main window. The iframe and parent frame
|
||||
* can access each others' properties and functions without restriction.
|
||||
*
|
||||
* @param {!Element} parentElement The parent element in which to append the
|
||||
* iframe.
|
||||
* @param {string=} opt_headContents Contents to go into the iframe's head.
|
||||
* @param {string=} opt_bodyContents Contents to go into the iframe's body.
|
||||
* @param {string=} opt_styles CSS styles for the iframe itself, before adding
|
||||
* to the parent element.
|
||||
* @param {boolean=} opt_quirks Whether to use quirks mode (false by default).
|
||||
* @return {!HTMLIFrameElement} An iframe that has the specified contents.
|
||||
*/
|
||||
goog.dom.iframe.createWithContent = function(
|
||||
parentElement, opt_headContents, opt_bodyContents, opt_styles, opt_quirks) {
|
||||
var domHelper = goog.dom.getDomHelper(parentElement);
|
||||
// Generate the HTML content.
|
||||
var contentBuf = [];
|
||||
|
||||
if (!opt_quirks) {
|
||||
contentBuf.push('<!DOCTYPE html>');
|
||||
}
|
||||
contentBuf.push('<html><head>', opt_headContents, '</head><body>',
|
||||
opt_bodyContents, '</body></html>');
|
||||
|
||||
var iframe = goog.dom.iframe.createBlank(domHelper, opt_styles);
|
||||
|
||||
// Cannot manipulate iframe content until it is in a document.
|
||||
parentElement.appendChild(iframe);
|
||||
goog.dom.iframe.writeContent(iframe, contentBuf.join(''));
|
||||
|
||||
return iframe;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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.
|
||||
-->
|
||||
<!--
|
||||
All Rights Reserved.
|
||||
|
||||
Author: gboyer@google.com (Garrett Boyer)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom.iframe</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.iframeTest');
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="border: 1px solid black; padding: 4px">
|
||||
<div>
|
||||
Blank Iframe - The below area should be completely white.
|
||||
</div>
|
||||
<!--
|
||||
- Simple table to measure the exterior size of the iframe. A table is
|
||||
- used because it is sensitive to problems with iframe margins and
|
||||
- vertical alignment.
|
||||
-->
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tr><td>
|
||||
<div id="blank">
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="sandbox"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2008 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.iframeTest');
|
||||
goog.setTestOnly('goog.dom.iframeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.iframe');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var domHelper;
|
||||
var sandbox;
|
||||
|
||||
function setUpPage() {
|
||||
domHelper = goog.dom.getDomHelper();
|
||||
sandbox = domHelper.getElement('sandbox');
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
goog.dom.removeChildren(sandbox);
|
||||
}
|
||||
|
||||
function testCreateWithContent() {
|
||||
var iframe = goog.dom.iframe.createWithContent(sandbox,
|
||||
'<title>Foo Title</title>', '<div id="blah">Test</div>',
|
||||
'position: absolute',
|
||||
false /* opt_quirks */);
|
||||
|
||||
var doc = goog.dom.getFrameContentDocument(iframe);
|
||||
assertNotNull(doc.getElementById('blah'));
|
||||
assertEquals('Foo Title', doc.title);
|
||||
assertEquals('absolute', iframe.style.position);
|
||||
}
|
||||
|
||||
function testCreateBlankYieldsIframeWithNoBorderOrPadding() {
|
||||
var iframe = goog.dom.iframe.createBlank(domHelper);
|
||||
iframe.style.width = '350px';
|
||||
iframe.style.height = '250px';
|
||||
var blankElement = domHelper.getElement('blank');
|
||||
blankElement.appendChild(iframe);
|
||||
assertEquals(
|
||||
'Width should be as styled: no extra borders, padding, etc.',
|
||||
350, blankElement.offsetWidth);
|
||||
assertEquals(
|
||||
'Height should be as styled: no extra borders, padding, etc.',
|
||||
250, blankElement.offsetHeight);
|
||||
}
|
||||
|
||||
function testCreateBlankWithStyles() {
|
||||
var iframe = goog.dom.iframe.createBlank(domHelper, 'position:absolute');
|
||||
assertEquals('absolute', iframe.style.position);
|
||||
assertEquals('bottom', iframe.style.verticalAlign);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2008 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 Iterators over DOM nodes.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.iter.AncestorIterator');
|
||||
goog.provide('goog.dom.iter.ChildIterator');
|
||||
goog.provide('goog.dom.iter.SiblingIterator');
|
||||
|
||||
goog.require('goog.iter.Iterator');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Iterator over a Node's siblings.
|
||||
* @param {Node} node The node to start with.
|
||||
* @param {boolean=} opt_includeNode Whether to return the given node as the
|
||||
* first return value from next.
|
||||
* @param {boolean=} opt_reverse Whether to traverse siblings in reverse
|
||||
* document order.
|
||||
* @constructor
|
||||
* @extends {goog.iter.Iterator}
|
||||
*/
|
||||
goog.dom.iter.SiblingIterator = function(node, opt_includeNode, opt_reverse) {
|
||||
/**
|
||||
* The current node, or null if iteration is finished.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
this.node_ = node;
|
||||
|
||||
/**
|
||||
* Whether to iterate in reverse.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.reverse_ = !!opt_reverse;
|
||||
|
||||
if (node && !opt_includeNode) {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
goog.inherits(goog.dom.iter.SiblingIterator, goog.iter.Iterator);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.iter.SiblingIterator.prototype.next = function() {
|
||||
var node = this.node_;
|
||||
if (!node) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
this.node_ = this.reverse_ ? node.previousSibling : node.nextSibling;
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Iterator over an Element's children.
|
||||
* @param {Element} element The element to iterate over.
|
||||
* @param {boolean=} opt_reverse Optionally traverse children from last to
|
||||
* first.
|
||||
* @param {number=} opt_startIndex Optional starting index.
|
||||
* @constructor
|
||||
* @extends {goog.dom.iter.SiblingIterator}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.iter.ChildIterator = function(element, opt_reverse, opt_startIndex) {
|
||||
if (!goog.isDef(opt_startIndex)) {
|
||||
opt_startIndex = opt_reverse && element.childNodes.length ?
|
||||
element.childNodes.length - 1 : 0;
|
||||
}
|
||||
goog.dom.iter.SiblingIterator.call(this, element.childNodes[opt_startIndex],
|
||||
true, opt_reverse);
|
||||
};
|
||||
goog.inherits(goog.dom.iter.ChildIterator, goog.dom.iter.SiblingIterator);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Iterator over a Node's ancestors, stopping after the document body.
|
||||
* @param {Node} node The node to start with.
|
||||
* @param {boolean=} opt_includeNode Whether to return the given node as the
|
||||
* first return value from next.
|
||||
* @constructor
|
||||
* @extends {goog.iter.Iterator}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.iter.AncestorIterator = function(node, opt_includeNode) {
|
||||
/**
|
||||
* The current node, or null if iteration is finished.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
this.node_ = node;
|
||||
|
||||
if (node && !opt_includeNode) {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
goog.inherits(goog.dom.iter.AncestorIterator, goog.iter.Iterator);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.iter.AncestorIterator.prototype.next = function() {
|
||||
var node = this.node_;
|
||||
if (!node) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
this.node_ = node.parentNode;
|
||||
return node;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html id="html">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom.iter</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.iterTest');
|
||||
</script>
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="test">abc<br id="br">def</div>
|
||||
</body>
|
||||
</html>
|
||||
<!--
|
||||
Copyright 2008 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.
|
||||
-->
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2008 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.iterTest');
|
||||
goog.setTestOnly('goog.dom.iterTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.iter.AncestorIterator');
|
||||
goog.require('goog.dom.iter.ChildIterator');
|
||||
goog.require('goog.dom.iter.SiblingIterator');
|
||||
goog.require('goog.testing.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var test;
|
||||
var br;
|
||||
|
||||
function setUpPage() {
|
||||
test = goog.dom.getElement('test');
|
||||
br = goog.dom.getElement('br');
|
||||
}
|
||||
|
||||
function testNextSibling() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.SiblingIterator(test.firstChild),
|
||||
['#br', 'def']);
|
||||
}
|
||||
|
||||
function testNextSiblingInclusive() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.SiblingIterator(test.firstChild, true),
|
||||
['abc', '#br', 'def']);
|
||||
}
|
||||
|
||||
function testPreviousSibling() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.SiblingIterator(test.lastChild, false, true),
|
||||
['#br', 'abc']);
|
||||
}
|
||||
|
||||
function testPreviousSiblingInclusive() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.SiblingIterator(test.lastChild, true, true),
|
||||
['def', '#br', 'abc']);
|
||||
}
|
||||
|
||||
function testChildIterator() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.ChildIterator(test),
|
||||
['abc', '#br', 'def']);
|
||||
}
|
||||
|
||||
function testChildIteratorIndex() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.ChildIterator(test, false, 1),
|
||||
['#br', 'def']);
|
||||
}
|
||||
|
||||
function testChildIteratorReverse() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.ChildIterator(test, true),
|
||||
['def', '#br', 'abc']);
|
||||
}
|
||||
|
||||
function testEmptyChildIteratorReverse() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.ChildIterator(br, true), []);
|
||||
}
|
||||
|
||||
function testChildIteratorIndexReverse() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.ChildIterator(test, true, 1),
|
||||
['#br', 'abc']);
|
||||
}
|
||||
|
||||
function testAncestorIterator() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.AncestorIterator(br),
|
||||
['#test', '#body', '#html', goog.dom.NodeType.DOCUMENT]);
|
||||
}
|
||||
|
||||
function testAncestorIteratorInclusive() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.iter.AncestorIterator(br, true),
|
||||
['#br', '#test', '#body', '#html', goog.dom.NodeType.DOCUMENT]);
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for working with W3C multi-part ranges.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.MultiRange');
|
||||
goog.provide('goog.dom.MultiRangeIterator');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom.AbstractMultiRange');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
goog.require('goog.dom.RangeIterator');
|
||||
goog.require('goog.dom.RangeType');
|
||||
goog.require('goog.dom.SavedRange');
|
||||
goog.require('goog.dom.TextRange');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
goog.require('goog.log');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new multi part range with no properties. Do not use this
|
||||
* constructor: use one of the goog.dom.Range.createFrom* methods instead.
|
||||
* @constructor
|
||||
* @extends {goog.dom.AbstractMultiRange}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.MultiRange = function() {
|
||||
/**
|
||||
* Array of browser sub-ranges comprising this multi-range.
|
||||
* @type {Array<Range>}
|
||||
* @private
|
||||
*/
|
||||
this.browserRanges_ = [];
|
||||
|
||||
/**
|
||||
* Lazily initialized array of range objects comprising this multi-range.
|
||||
* @type {Array<goog.dom.TextRange>}
|
||||
* @private
|
||||
*/
|
||||
this.ranges_ = [];
|
||||
|
||||
/**
|
||||
* Lazily computed sorted version of ranges_, sorted by start point.
|
||||
* @type {Array<goog.dom.TextRange>?}
|
||||
* @private
|
||||
*/
|
||||
this.sortedRanges_ = null;
|
||||
|
||||
/**
|
||||
* Lazily computed container node.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
this.container_ = null;
|
||||
};
|
||||
goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange);
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new range wrapper from the given browser selection object. Do not
|
||||
* use this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {Selection} selection The browser selection object.
|
||||
* @return {!goog.dom.MultiRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.MultiRange.createFromBrowserSelection = function(selection) {
|
||||
var range = new goog.dom.MultiRange();
|
||||
for (var i = 0, len = selection.rangeCount; i < len; i++) {
|
||||
range.browserRanges_.push(selection.getRangeAt(i));
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new range wrapper from the given browser ranges. Do not
|
||||
* use this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {Array<Range>} browserRanges The browser ranges.
|
||||
* @return {!goog.dom.MultiRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) {
|
||||
var range = new goog.dom.MultiRange();
|
||||
range.browserRanges_ = goog.array.clone(browserRanges);
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new range wrapper from the given goog.dom.TextRange objects. Do
|
||||
* not use this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {Array<goog.dom.TextRange>} textRanges The text range objects.
|
||||
* @return {!goog.dom.MultiRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.MultiRange.createFromTextRanges = function(textRanges) {
|
||||
var range = new goog.dom.MultiRange();
|
||||
range.ranges_ = textRanges;
|
||||
range.browserRanges_ = goog.array.map(textRanges, function(range) {
|
||||
return range.getBrowserRangeObject();
|
||||
});
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Logging object.
|
||||
* @type {goog.log.Logger}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.MultiRange.prototype.logger_ =
|
||||
goog.log.getLogger('goog.dom.MultiRange');
|
||||
|
||||
|
||||
// Method implementations
|
||||
|
||||
|
||||
/**
|
||||
* Clears cached values. Should be called whenever this.browserRanges_ is
|
||||
* modified.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.MultiRange.prototype.clearCachedValues_ = function() {
|
||||
this.ranges_ = [];
|
||||
this.sortedRanges_ = null;
|
||||
this.container_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.MultiRange} A clone of this range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.MultiRange.prototype.clone = function() {
|
||||
return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getType = function() {
|
||||
return goog.dom.RangeType.MULTI;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getBrowserRangeObject = function() {
|
||||
// NOTE(robbyw): This method does not make sense for multi-ranges.
|
||||
if (this.browserRanges_.length > 1) {
|
||||
goog.log.warning(this.logger_,
|
||||
'getBrowserRangeObject called on MultiRange with more than 1 range');
|
||||
}
|
||||
return this.browserRanges_[0];
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) {
|
||||
// TODO(robbyw): Look in to adding setBrowserSelectionObject.
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getTextRangeCount = function() {
|
||||
return this.browserRanges_.length;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getTextRange = function(i) {
|
||||
if (!this.ranges_[i]) {
|
||||
this.ranges_[i] = goog.dom.TextRange.createFromBrowserRange(
|
||||
this.browserRanges_[i]);
|
||||
}
|
||||
return this.ranges_[i];
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getContainer = function() {
|
||||
if (!this.container_) {
|
||||
var nodes = [];
|
||||
for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {
|
||||
nodes.push(this.getTextRange(i).getContainer());
|
||||
}
|
||||
this.container_ = goog.dom.findCommonAncestor.apply(null, nodes);
|
||||
}
|
||||
return this.container_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Array<goog.dom.TextRange>} An array of sub-ranges, sorted by start
|
||||
* point.
|
||||
*/
|
||||
goog.dom.MultiRange.prototype.getSortedRanges = function() {
|
||||
if (!this.sortedRanges_) {
|
||||
this.sortedRanges_ = this.getTextRanges();
|
||||
this.sortedRanges_.sort(function(a, b) {
|
||||
var aStartNode = a.getStartNode();
|
||||
var aStartOffset = a.getStartOffset();
|
||||
var bStartNode = b.getStartNode();
|
||||
var bStartOffset = b.getStartOffset();
|
||||
|
||||
if (aStartNode == bStartNode && aStartOffset == bStartOffset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return goog.dom.Range.isReversed(aStartNode, aStartOffset, bStartNode,
|
||||
bStartOffset) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
return this.sortedRanges_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getStartNode = function() {
|
||||
return this.getSortedRanges()[0].getStartNode();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getStartOffset = function() {
|
||||
return this.getSortedRanges()[0].getStartOffset();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getEndNode = function() {
|
||||
// NOTE(robbyw): This may return the wrong node if any subranges overlap.
|
||||
return goog.array.peek(this.getSortedRanges()).getEndNode();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getEndOffset = function() {
|
||||
// NOTE(robbyw): This may return the wrong value if any subranges overlap.
|
||||
return goog.array.peek(this.getSortedRanges()).getEndOffset();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.isRangeInDocument = function() {
|
||||
return goog.array.every(this.getTextRanges(), function(range) {
|
||||
return range.isRangeInDocument();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.isCollapsed = function() {
|
||||
return this.browserRanges_.length == 0 ||
|
||||
this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getText = function() {
|
||||
return goog.array.map(this.getTextRanges(), function(range) {
|
||||
return range.getText();
|
||||
}).join('');
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getHtmlFragment = function() {
|
||||
return this.getValidHtml();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getValidHtml = function() {
|
||||
// NOTE(robbyw): This does not behave well if the sub-ranges overlap.
|
||||
return goog.array.map(this.getTextRanges(), function(range) {
|
||||
return range.getValidHtml();
|
||||
}).join('');
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.getPastableHtml = function() {
|
||||
// TODO(robbyw): This should probably do something smart like group TR and TD
|
||||
// selections in to the same table.
|
||||
return this.getValidHtml();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) {
|
||||
return new goog.dom.MultiRangeIterator(this);
|
||||
};
|
||||
|
||||
|
||||
// RANGE ACTIONS
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.select = function() {
|
||||
var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(
|
||||
this.getWindow());
|
||||
selection.removeAllRanges();
|
||||
for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {
|
||||
selection.addRange(this.getTextRange(i).getBrowserRangeObject());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.removeContents = function() {
|
||||
goog.array.forEach(this.getTextRanges(), function(range) {
|
||||
range.removeContents();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// SAVE/RESTORE
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRange.prototype.saveUsingDom = function() {
|
||||
return new goog.dom.DomSavedMultiRange_(this);
|
||||
};
|
||||
|
||||
|
||||
// RANGE MODIFICATION
|
||||
|
||||
|
||||
/**
|
||||
* Collapses this range to a single point, either the first or last point
|
||||
* depending on the parameter. This will result in the number of ranges in this
|
||||
* multi range becoming 1.
|
||||
* @param {boolean} toAnchor Whether to collapse to the anchor.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.MultiRange.prototype.collapse = function(toAnchor) {
|
||||
if (!this.isCollapsed()) {
|
||||
var range = toAnchor ? this.getTextRange(0) : this.getTextRange(
|
||||
this.getTextRangeCount() - 1);
|
||||
|
||||
this.clearCachedValues_();
|
||||
range.collapse(toAnchor);
|
||||
this.ranges_ = [range];
|
||||
this.sortedRanges_ = [range];
|
||||
this.browserRanges_ = [range.getBrowserRangeObject()];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// SAVED RANGE OBJECTS
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A SavedRange implementation using DOM endpoints.
|
||||
* @param {goog.dom.MultiRange} range The range to save.
|
||||
* @constructor
|
||||
* @extends {goog.dom.SavedRange}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.DomSavedMultiRange_ = function(range) {
|
||||
/**
|
||||
* Array of saved ranges.
|
||||
* @type {Array<goog.dom.SavedRange>}
|
||||
* @private
|
||||
*/
|
||||
this.savedRanges_ = goog.array.map(range.getTextRanges(), function(range) {
|
||||
return range.saveUsingDom();
|
||||
});
|
||||
};
|
||||
goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange);
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.MultiRange} The restored range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() {
|
||||
var ranges = goog.array.map(this.savedRanges_, function(savedRange) {
|
||||
return savedRange.restore();
|
||||
});
|
||||
return goog.dom.MultiRange.createFromTextRanges(ranges);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() {
|
||||
goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this);
|
||||
|
||||
goog.array.forEach(this.savedRanges_, function(savedRange) {
|
||||
savedRange.dispose();
|
||||
});
|
||||
delete this.savedRanges_;
|
||||
};
|
||||
|
||||
|
||||
// RANGE ITERATION
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Subclass of goog.dom.TagIterator that iterates over a DOM range. It
|
||||
* adds functions to determine the portion of each text node that is selected.
|
||||
*
|
||||
* @param {goog.dom.MultiRange} range The range to traverse.
|
||||
* @constructor
|
||||
* @extends {goog.dom.RangeIterator}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.MultiRangeIterator = function(range) {
|
||||
if (range) {
|
||||
this.iterators_ = goog.array.map(
|
||||
range.getSortedRanges(),
|
||||
function(r) {
|
||||
return goog.iter.toIterator(r);
|
||||
});
|
||||
}
|
||||
|
||||
goog.dom.RangeIterator.call(
|
||||
this, range ? this.getStartNode() : null, false);
|
||||
};
|
||||
goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator);
|
||||
|
||||
|
||||
/**
|
||||
* The list of range iterators left to traverse.
|
||||
* @type {Array<goog.dom.RangeIterator>?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.MultiRangeIterator.prototype.iterators_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The index of the current sub-iterator being traversed.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.MultiRangeIterator.prototype.currentIdx_ = 0;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() {
|
||||
return this.iterators_[this.currentIdx_].getStartTextOffset();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() {
|
||||
return this.iterators_[this.currentIdx_].getEndTextOffset();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.getStartNode = function() {
|
||||
return this.iterators_[0].getStartNode();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.getEndNode = function() {
|
||||
return goog.array.peek(this.iterators_).getEndNode();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.isLast = function() {
|
||||
return this.iterators_[this.currentIdx_].isLast();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.next = function() {
|
||||
/** @preserveTry */
|
||||
try {
|
||||
var it = this.iterators_[this.currentIdx_];
|
||||
var next = it.next();
|
||||
this.setPosition(it.node, it.tagType, it.depth);
|
||||
return next;
|
||||
} catch (ex) {
|
||||
if (ex !== goog.iter.StopIteration ||
|
||||
this.iterators_.length - 1 == this.currentIdx_) {
|
||||
throw ex;
|
||||
} else {
|
||||
// In case we got a StopIteration, increment counter and try again.
|
||||
this.currentIdx_++;
|
||||
return this.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) {
|
||||
this.iterators_ = goog.array.clone(other.iterators_);
|
||||
goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.MultiRangeIterator} An identical iterator.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.MultiRangeIterator.prototype.clone = function() {
|
||||
var copy = new goog.dom.MultiRangeIterator(null);
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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.
|
||||
-->
|
||||
<!--
|
||||
|
||||
Author: robbyw@google.com (Robby Walker)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Unit Tests - goog.dom.MultiRange</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.MultiRangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test">
|
||||
<div id="test1">abc</div>
|
||||
<div id="test2">defghi</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2008 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.MultiRangeTest');
|
||||
goog.setTestOnly('goog.dom.MultiRangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.MultiRange');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.iter');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var range;
|
||||
function setUp() {
|
||||
range = new goog.dom.MultiRange.createFromTextRanges([
|
||||
goog.dom.Range.createFromNodeContents(goog.dom.getElement('test2')),
|
||||
goog.dom.Range.createFromNodeContents(goog.dom.getElement('test1'))
|
||||
]);
|
||||
}
|
||||
|
||||
function testStartAndEnd() {
|
||||
assertEquals(goog.dom.getElement('test1').firstChild, range.getStartNode());
|
||||
assertEquals(0, range.getStartOffset());
|
||||
assertEquals(goog.dom.getElement('test2').firstChild, range.getEndNode());
|
||||
assertEquals(6, range.getEndOffset());
|
||||
}
|
||||
|
||||
function testStartAndEndIterator() {
|
||||
var it = goog.iter.toIterator(range);
|
||||
assertEquals(goog.dom.getElement('test1').firstChild, it.getStartNode());
|
||||
assertEquals(0, it.getStartTextOffset());
|
||||
assertEquals(goog.dom.getElement('test2').firstChild, it.getEndNode());
|
||||
assertEquals(3, it.getEndTextOffset());
|
||||
|
||||
it.next();
|
||||
it.next();
|
||||
assertEquals(6, it.getEndTextOffset());
|
||||
}
|
||||
|
||||
function testIteration() {
|
||||
var tags = goog.iter.toArray(range);
|
||||
assertEquals(2, tags.length);
|
||||
|
||||
assertEquals(goog.dom.getElement('test1').firstChild, tags[0]);
|
||||
assertEquals(goog.dom.getElement('test2').firstChild, tags[1]);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2008 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 Iterator subclass for DOM tree traversal.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.NodeIterator');
|
||||
|
||||
goog.require('goog.dom.TagIterator');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A DOM tree traversal iterator.
|
||||
*
|
||||
* Starting with the given node, the iterator walks the DOM in order, reporting
|
||||
* events for each node. The iterator acts as a prefix iterator:
|
||||
*
|
||||
* <pre>
|
||||
* <div>1<span>2</span>3</div>
|
||||
* </pre>
|
||||
*
|
||||
* Will return the following nodes:
|
||||
*
|
||||
* <code>[div, 1, span, 2, 3]</code>
|
||||
*
|
||||
* With the following depths
|
||||
*
|
||||
* <code>[1, 1, 2, 2, 1]</code>
|
||||
*
|
||||
* Imagining <code>|</code> represents iterator position, the traversal stops at
|
||||
* each of the following locations:
|
||||
*
|
||||
* <pre><div>|1|<span>|2|</span>3|</div></pre>
|
||||
*
|
||||
* The iterator can also be used in reverse mode, which will return the nodes
|
||||
* and states in the opposite order. The depths will be slightly different
|
||||
* since, like in normal mode, the depth is computed *after* the last move.
|
||||
*
|
||||
* Lastly, it is possible to create an iterator that is unconstrained, meaning
|
||||
* that it will continue iterating until the end of the document instead of
|
||||
* until exiting the start node.
|
||||
*
|
||||
* @param {Node=} opt_node The start node. Defaults to an empty iterator.
|
||||
* @param {boolean=} opt_reversed Whether to traverse the tree in reverse.
|
||||
* @param {boolean=} opt_unconstrained Whether the iterator is not constrained
|
||||
* to the starting node and its children.
|
||||
* @param {number=} opt_depth The starting tree depth.
|
||||
* @constructor
|
||||
* @extends {goog.dom.TagIterator}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.NodeIterator = function(opt_node, opt_reversed,
|
||||
opt_unconstrained, opt_depth) {
|
||||
goog.dom.TagIterator.call(this, opt_node, opt_reversed, opt_unconstrained,
|
||||
null, opt_depth);
|
||||
};
|
||||
goog.inherits(goog.dom.NodeIterator, goog.dom.TagIterator);
|
||||
|
||||
|
||||
/**
|
||||
* Moves to the next position in the DOM tree.
|
||||
* @return {Node} Returns the next node, or throws a goog.iter.StopIteration
|
||||
* exception if the end of the iterator's range has been reached.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.NodeIterator.prototype.next = function() {
|
||||
do {
|
||||
goog.dom.NodeIterator.superClass_.next.call(this);
|
||||
} while (this.isEndTag());
|
||||
|
||||
return this.node;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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>goog.dom.NodeIterator Tests</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.NodeIteratorTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--
|
||||
The next line goes past 80 characters to avoid ambiguity with
|
||||
newlines as text nodes
|
||||
-->
|
||||
<div id="test"><a href="#" id="a1">T<b id="b1">e</b>xt</a><span id="span1"></span><p id="p1">Text</p></div>
|
||||
<ul id="test2"><li id="li1">Not<li id="li2">Closed</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2008 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.NodeIteratorTest');
|
||||
goog.setTestOnly('goog.dom.NodeIteratorTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeIterator');
|
||||
goog.require('goog.testing.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
function testBasic() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.NodeIterator(goog.dom.getElement('test')),
|
||||
['#test', '#a1', 'T', '#b1', 'e', 'xt', '#span1', '#p1', 'Text']);
|
||||
}
|
||||
|
||||
function testUnclosed() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.NodeIterator(goog.dom.getElement('test2')),
|
||||
['#test2', '#li1', 'Not', '#li2', 'Closed']);
|
||||
}
|
||||
|
||||
function testReverse() {
|
||||
goog.testing.dom.assertNodesMatch(
|
||||
new goog.dom.NodeIterator(goog.dom.getElement('test'), true),
|
||||
['Text', '#p1', '#span1', 'xt', 'e', '#b1', 'T', '#a1', '#test']);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2005 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 Object to store the offset from one node to another in a way
|
||||
* that works on any similar DOM structure regardless of whether it is the same
|
||||
* actual nodes.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.NodeOffset');
|
||||
|
||||
goog.require('goog.Disposable');
|
||||
goog.require('goog.dom.TagName');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Object to store the offset from one node to another in a way that works on
|
||||
* any similar DOM structure regardless of whether it is the same actual nodes.
|
||||
* @param {Node} node The node to get the offset for.
|
||||
* @param {Node} baseNode The node to calculate the offset from.
|
||||
* @extends {goog.Disposable}
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.dom.NodeOffset = function(node, baseNode) {
|
||||
goog.Disposable.call(this);
|
||||
|
||||
/**
|
||||
* A stack of childNode offsets.
|
||||
* @type {Array<number>}
|
||||
* @private
|
||||
*/
|
||||
this.offsetStack_ = [];
|
||||
|
||||
/**
|
||||
* A stack of childNode names.
|
||||
* @type {Array<string>}
|
||||
* @private
|
||||
*/
|
||||
this.nameStack_ = [];
|
||||
|
||||
while (node && node.nodeName != goog.dom.TagName.BODY && node != baseNode) {
|
||||
// Compute the sibling offset.
|
||||
var siblingOffset = 0;
|
||||
var sib = node.previousSibling;
|
||||
while (sib) {
|
||||
sib = sib.previousSibling;
|
||||
++siblingOffset;
|
||||
}
|
||||
this.offsetStack_.unshift(siblingOffset);
|
||||
this.nameStack_.unshift(node.nodeName);
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
};
|
||||
goog.inherits(goog.dom.NodeOffset, goog.Disposable);
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} A string representation of this object.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.NodeOffset.prototype.toString = function() {
|
||||
var strs = [];
|
||||
var name;
|
||||
for (var i = 0; name = this.nameStack_[i]; i++) {
|
||||
strs.push(this.offsetStack_[i] + ',' + name);
|
||||
}
|
||||
return strs.join('\n');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Walk the dom and find the node relative to baseNode. Returns null on
|
||||
* failure.
|
||||
* @param {Node} baseNode The node to start walking from. Should be equivalent
|
||||
* to the node passed in to the constructor, in that it should have the
|
||||
* same contents.
|
||||
* @return {Node} The node relative to baseNode, or null on failure.
|
||||
*/
|
||||
goog.dom.NodeOffset.prototype.findTargetNode = function(baseNode) {
|
||||
var name;
|
||||
var curNode = baseNode;
|
||||
for (var i = 0; name = this.nameStack_[i]; ++i) {
|
||||
curNode = curNode.childNodes[this.offsetStack_[i]];
|
||||
|
||||
// Sanity check and make sure the element names match.
|
||||
if (!curNode || curNode.nodeName != name) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return curNode;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.NodeOffset.prototype.disposeInternal = function() {
|
||||
delete this.offsetStack_;
|
||||
delete this.nameStack_;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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>goog.dom.NodeOffset Tests</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.NodeOffsetTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="test1">Text<br> and <b>more <i id="i">text.</i></b></div>
|
||||
<div id="test2"></div>
|
||||
<div id="empty"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2008 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.NodeOffsetTest');
|
||||
goog.setTestOnly('goog.dom.NodeOffsetTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeOffset');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
var test1;
|
||||
var test2;
|
||||
var i;
|
||||
var empty;
|
||||
|
||||
function setUpPage() {
|
||||
test1 = goog.dom.getElement('test1');
|
||||
i = goog.dom.getElement('i');
|
||||
test2 = goog.dom.getElement('test2');
|
||||
test2.innerHTML = test1.innerHTML;
|
||||
empty = goog.dom.getElement('empty');
|
||||
}
|
||||
|
||||
function testElementOffset() {
|
||||
var nodeOffset = new goog.dom.NodeOffset(i, test1);
|
||||
|
||||
var recovered = nodeOffset.findTargetNode(test2);
|
||||
assertNotNull('Should recover a node.', recovered);
|
||||
assertEquals('Should recover an I node.', goog.dom.TagName.I,
|
||||
recovered.tagName);
|
||||
assertTrue('Should recover a child of test2',
|
||||
goog.dom.contains(test2, recovered));
|
||||
assertFalse('Should not recover a child of test1',
|
||||
goog.dom.contains(test1, recovered));
|
||||
|
||||
nodeOffset.dispose();
|
||||
}
|
||||
|
||||
function testNodeOffset() {
|
||||
var nodeOffset = new goog.dom.NodeOffset(i.firstChild, test1);
|
||||
|
||||
var recovered = nodeOffset.findTargetNode(test2);
|
||||
assertNotNull('Should recover a node.', recovered);
|
||||
assertEquals('Should recover a text node.', goog.dom.NodeType.TEXT,
|
||||
recovered.nodeType);
|
||||
assertEquals('Should have correct contents.', 'text.',
|
||||
recovered.nodeValue);
|
||||
assertTrue('Should recover a child of test2',
|
||||
goog.dom.contains(test2, recovered));
|
||||
assertFalse('Should not recover a child of test1',
|
||||
goog.dom.contains(test1, recovered));
|
||||
|
||||
nodeOffset.dispose();
|
||||
}
|
||||
|
||||
function testToString() {
|
||||
var nodeOffset = new goog.dom.NodeOffset(i.firstChild, test1);
|
||||
|
||||
assertEquals('Should have correct string representation',
|
||||
'3,B\n1,I\n0,#text', nodeOffset.toString());
|
||||
|
||||
nodeOffset.dispose();
|
||||
}
|
||||
|
||||
function testBadRecovery() {
|
||||
var nodeOffset = new goog.dom.NodeOffset(i.firstChild, test1);
|
||||
|
||||
var recovered = nodeOffset.findTargetNode(empty);
|
||||
assertNull('Should recover nothing.', recovered);
|
||||
|
||||
nodeOffset.dispose();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2006 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 goog.dom.NodeType.
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.NodeType');
|
||||
|
||||
|
||||
/**
|
||||
* Constants for the nodeType attribute in the Node interface.
|
||||
*
|
||||
* These constants match those specified in the Node interface. These are
|
||||
* usually present on the Node object in recent browsers, but not in older
|
||||
* browsers (specifically, early IEs) and thus are given here.
|
||||
*
|
||||
* In some browsers (early IEs), these are not defined on the Node object,
|
||||
* so they are provided here.
|
||||
*
|
||||
* See http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1950641247
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.dom.NodeType = {
|
||||
ELEMENT: 1,
|
||||
ATTRIBUTE: 2,
|
||||
TEXT: 3,
|
||||
CDATA_SECTION: 4,
|
||||
ENTITY_REFERENCE: 5,
|
||||
ENTITY: 6,
|
||||
PROCESSING_INSTRUCTION: 7,
|
||||
COMMENT: 8,
|
||||
DOCUMENT: 9,
|
||||
DOCUMENT_TYPE: 10,
|
||||
DOCUMENT_FRAGMENT: 11,
|
||||
NOTATION: 12
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 DOM pattern base class.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.AbstractPattern');
|
||||
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Base pattern class for DOM matching.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
goog.dom.pattern.AbstractPattern = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The first node matched by this pattern.
|
||||
* @type {Node}
|
||||
*/
|
||||
goog.dom.pattern.AbstractPattern.prototype.matchedNode = null;
|
||||
|
||||
|
||||
/**
|
||||
* Reset any internal state this pattern keeps.
|
||||
*/
|
||||
goog.dom.pattern.AbstractPattern.prototype.reset = function() {
|
||||
// The base implementation does nothing.
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test whether this pattern matches the given token.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} {@code MATCH} if the pattern matches.
|
||||
*/
|
||||
goog.dom.pattern.AbstractPattern.prototype.matchToken = function(token, type) {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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 DOM pattern to match any children of a tag.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.AllChildren');
|
||||
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches any nodes at or below the current tree depth.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
*/
|
||||
goog.dom.pattern.AllChildren = function() {
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.AllChildren, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* Tracks the matcher's depth to detect the end of the tag.
|
||||
*
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.AllChildren.prototype.depth_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is on the same level.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} {@code MATCHING} if the token is on the
|
||||
* same level or deeper and {@code BACKTRACK_MATCH} if not.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.AllChildren.prototype.matchToken = function(token, type) {
|
||||
this.depth_ += type;
|
||||
|
||||
if (this.depth_ >= 0) {
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
} else {
|
||||
this.depth_ = 0;
|
||||
return goog.dom.pattern.MatchType.BACKTRACK_MATCH;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset any internal state this pattern keeps.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.AllChildren.prototype.reset = function() {
|
||||
this.depth_ = 0;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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 Useful callback functions for the DOM matcher.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.callback');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagWalkType');
|
||||
goog.require('goog.iter');
|
||||
|
||||
|
||||
/**
|
||||
* Callback function for use in {@link goog.dom.pattern.Matcher.addPattern}
|
||||
* that removes the matched node from the tree. Should be used in conjunciton
|
||||
* with a {@link goog.dom.pattern.StartTag} pattern.
|
||||
*
|
||||
* @param {Node} node The node matched by the pattern.
|
||||
* @param {goog.dom.TagIterator} position The position where the match
|
||||
* finished.
|
||||
* @return {boolean} Returns true to indicate tree changes were made.
|
||||
*/
|
||||
goog.dom.pattern.callback.removeNode = function(node, position) {
|
||||
// Find out which position would be next.
|
||||
position.setPosition(node, goog.dom.TagWalkType.END_TAG);
|
||||
|
||||
goog.iter.nextOrValue(position, null);
|
||||
|
||||
// Remove the node.
|
||||
goog.dom.removeNode(node);
|
||||
|
||||
// Correct for the depth change.
|
||||
position.depth -= 1;
|
||||
|
||||
// Indicate that we made position/tree changes.
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Callback function for use in {@link goog.dom.pattern.Matcher.addPattern}
|
||||
* that removes the matched node from the tree and replaces it with its
|
||||
* children. Should be used in conjunction with a
|
||||
* {@link goog.dom.pattern.StartTag} pattern.
|
||||
*
|
||||
* @param {Element} node The node matched by the pattern.
|
||||
* @param {goog.dom.TagIterator} position The position where the match
|
||||
* finished.
|
||||
* @return {boolean} Returns true to indicate tree changes were made.
|
||||
*/
|
||||
goog.dom.pattern.callback.flattenElement = function(node, position) {
|
||||
// Find out which position would be next.
|
||||
position.setPosition(node, node.firstChild ?
|
||||
goog.dom.TagWalkType.START_TAG :
|
||||
goog.dom.TagWalkType.END_TAG);
|
||||
|
||||
goog.iter.nextOrValue(position, null);
|
||||
|
||||
// Flatten the node.
|
||||
goog.dom.flattenElement(node);
|
||||
|
||||
// Correct for the depth change.
|
||||
position.depth -= 1;
|
||||
|
||||
// Indicate that we made position/tree changes.
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 Callback object that counts matches.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.callback.Counter');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Callback class for counting matches.
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.callback.Counter = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The count of objects matched so far.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
goog.dom.pattern.callback.Counter.prototype.count = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The callback function. Suitable as a callback for
|
||||
* {@link goog.dom.pattern.Matcher}.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.callback.Counter.prototype.callback_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get a bound callback function that is suitable as a callback for
|
||||
* {@link goog.dom.pattern.Matcher}.
|
||||
*
|
||||
* @return {!Function} A callback function.
|
||||
*/
|
||||
goog.dom.pattern.callback.Counter.prototype.getCallback = function() {
|
||||
if (!this.callback_) {
|
||||
this.callback_ = goog.bind(function() {
|
||||
this.count++;
|
||||
return false;
|
||||
}, this);
|
||||
}
|
||||
return this.callback_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset the counter.
|
||||
*/
|
||||
goog.dom.pattern.callback.Counter.prototype.reset = function() {
|
||||
this.count = 0;
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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 Callback object that tests if a pattern matches at least once.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.callback.Test');
|
||||
|
||||
goog.require('goog.iter.StopIteration');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Callback class for testing for at least one match.
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.callback.Test = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether or not the pattern matched.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
goog.dom.pattern.callback.Test.prototype.matched = false;
|
||||
|
||||
|
||||
/**
|
||||
* The callback function. Suitable as a callback for
|
||||
* {@link goog.dom.pattern.Matcher}.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.callback.Test.prototype.callback_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get a bound callback function that is suitable as a callback for
|
||||
* {@link goog.dom.pattern.Matcher}.
|
||||
*
|
||||
* @return {!Function} A callback function.
|
||||
*/
|
||||
goog.dom.pattern.callback.Test.prototype.getCallback = function() {
|
||||
if (!this.callback_) {
|
||||
this.callback_ = goog.bind(function(node, position) {
|
||||
// Mark our match.
|
||||
this.matched = true;
|
||||
|
||||
// Stop searching.
|
||||
throw goog.iter.StopIteration;
|
||||
}, this);
|
||||
}
|
||||
return this.callback_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset the counter.
|
||||
*/
|
||||
goog.dom.pattern.callback.Test.prototype.reset = function() {
|
||||
this.matched = false;
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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 DOM pattern to match any children of a tag, and
|
||||
* specifically collect those that match a child pattern.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.ChildMatches');
|
||||
|
||||
goog.require('goog.dom.pattern.AllChildren');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches any nodes at or below the current tree depth.
|
||||
*
|
||||
* @param {goog.dom.pattern.AbstractPattern} childPattern Pattern to collect
|
||||
* child matches of.
|
||||
* @param {number=} opt_minimumMatches Enforce a minimum nuber of matches.
|
||||
* Defaults to 0.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AllChildren}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches = function(childPattern, opt_minimumMatches) {
|
||||
this.childPattern_ = childPattern;
|
||||
this.matches = [];
|
||||
this.minimumMatches_ = opt_minimumMatches || 0;
|
||||
goog.dom.pattern.AllChildren.call(this);
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.ChildMatches, goog.dom.pattern.AllChildren);
|
||||
|
||||
|
||||
/**
|
||||
* Array of matched child nodes.
|
||||
*
|
||||
* @type {Array<Node>}
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.matches;
|
||||
|
||||
|
||||
/**
|
||||
* Minimum number of matches.
|
||||
*
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.minimumMatches_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The child pattern to collect matches from.
|
||||
*
|
||||
* @type {goog.dom.pattern.AbstractPattern}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.childPattern_;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the pattern has recently matched or failed to match and will need to
|
||||
* be reset when starting a new round of matches.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.needsReset_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is on the same level.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} {@code MATCHING} if the token is on the
|
||||
* same level or deeper and {@code BACKTRACK_MATCH} if not.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.matchToken = function(token, type) {
|
||||
// Defer resets so we maintain our matches array until the last possible time.
|
||||
if (this.needsReset_) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// Call the super-method to ensure we stay in the child tree.
|
||||
var status =
|
||||
goog.dom.pattern.AllChildren.prototype.matchToken.apply(this, arguments);
|
||||
|
||||
switch (status) {
|
||||
case goog.dom.pattern.MatchType.MATCHING:
|
||||
var backtrack = false;
|
||||
|
||||
switch (this.childPattern_.matchToken(token, type)) {
|
||||
case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
|
||||
backtrack = true;
|
||||
case goog.dom.pattern.MatchType.MATCH:
|
||||
// Collect the match.
|
||||
this.matches.push(this.childPattern_.matchedNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Keep trying if we haven't hit a terminal state.
|
||||
break;
|
||||
}
|
||||
|
||||
if (backtrack) {
|
||||
// The only interesting result is a MATCH, since BACKTRACK_MATCH means
|
||||
// we are hitting an infinite loop on something like a Repeat(0).
|
||||
if (this.childPattern_.matchToken(token, type) ==
|
||||
goog.dom.pattern.MatchType.MATCH) {
|
||||
this.matches.push(this.childPattern_.matchedNode);
|
||||
}
|
||||
}
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
|
||||
case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
|
||||
// TODO(robbyw): this should return something like BACKTRACK_NO_MATCH
|
||||
// when we don't meet our minimum.
|
||||
this.needsReset_ = true;
|
||||
return (this.matches.length >= this.minimumMatches_) ?
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH :
|
||||
goog.dom.pattern.MatchType.NO_MATCH;
|
||||
|
||||
default:
|
||||
this.needsReset_ = true;
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset any internal state this pattern keeps.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.ChildMatches.prototype.reset = function() {
|
||||
this.needsReset_ = false;
|
||||
this.matches.length = 0;
|
||||
this.childPattern_.reset();
|
||||
goog.dom.pattern.AllChildren.prototype.reset.call(this);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 DOM pattern to match the end of a tag.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.EndTag');
|
||||
|
||||
goog.require('goog.dom.TagWalkType');
|
||||
goog.require('goog.dom.pattern.Tag');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches a closing tag.
|
||||
*
|
||||
* @param {string|RegExp} tag Name of the tag. Also will accept a regular
|
||||
* expression to match against the tag name.
|
||||
* @param {Object=} opt_attrs Optional map of attribute names to desired values.
|
||||
* This pattern will only match when all attributes are present and match
|
||||
* the string or regular expression value provided here.
|
||||
* @param {Object=} opt_styles Optional map of CSS style names to desired
|
||||
* values. This pattern will only match when all styles are present and
|
||||
* match the string or regular expression value provided here.
|
||||
* @param {Function=} opt_test Optional function that takes the element as a
|
||||
* parameter and returns true if this pattern should match it.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.Tag}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.EndTag = function(tag, opt_attrs, opt_styles, opt_test) {
|
||||
goog.dom.pattern.Tag.call(
|
||||
this,
|
||||
tag,
|
||||
goog.dom.TagWalkType.END_TAG,
|
||||
opt_attrs,
|
||||
opt_styles,
|
||||
opt_test);
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.EndTag, goog.dom.pattern.Tag);
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 DOM pattern to match a tag and all of its children.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.FullTag');
|
||||
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
goog.require('goog.dom.pattern.StartTag');
|
||||
goog.require('goog.dom.pattern.Tag');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches a full tag including all its children.
|
||||
*
|
||||
* @param {string|RegExp} tag Name of the tag. Also will accept a regular
|
||||
* expression to match against the tag name.
|
||||
* @param {Object=} opt_attrs Optional map of attribute names to desired values.
|
||||
* This pattern will only match when all attributes are present and match
|
||||
* the string or regular expression value provided here.
|
||||
* @param {Object=} opt_styles Optional map of CSS style names to desired
|
||||
* values. This pattern will only match when all styles are present and
|
||||
* match the string or regular expression value provided here.
|
||||
* @param {Function=} opt_test Optional function that takes the element as a
|
||||
* parameter and returns true if this pattern should match it.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.StartTag}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.FullTag = function(tag, opt_attrs, opt_styles, opt_test) {
|
||||
goog.dom.pattern.StartTag.call(
|
||||
this,
|
||||
tag,
|
||||
opt_attrs,
|
||||
opt_styles,
|
||||
opt_test);
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.FullTag, goog.dom.pattern.StartTag);
|
||||
|
||||
|
||||
/**
|
||||
* Tracks the matcher's depth to detect the end of the tag.
|
||||
*
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.FullTag.prototype.depth_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is a start tag token which matches the tag name,
|
||||
* style, and attributes provided in the constructor.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> at the end of our
|
||||
* tag, <code>MATCHING</code> if we are within the tag, and
|
||||
* <code>NO_MATCH</code> if the starting tag does not match.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.FullTag.prototype.matchToken = function(token, type) {
|
||||
if (!this.depth_) {
|
||||
// If we have not yet started, make sure we match as a StartTag.
|
||||
if (goog.dom.pattern.Tag.prototype.matchToken.call(this, token, type)) {
|
||||
this.depth_ = type;
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
|
||||
} else {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
} else {
|
||||
this.depth_ += type;
|
||||
|
||||
return this.depth_ ?
|
||||
goog.dom.pattern.MatchType.MATCHING :
|
||||
goog.dom.pattern.MatchType.MATCH;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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 DOM pattern matcher. Allows for simple searching of DOM
|
||||
* using patterns descended from {@link goog.dom.pattern.AbstractPattern}.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.Matcher');
|
||||
|
||||
goog.require('goog.dom.TagIterator');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
goog.require('goog.iter');
|
||||
|
||||
|
||||
// TODO(robbyw): Allow for backtracks of size > 1.
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Given a set of patterns and a root node, this class tests the patterns in
|
||||
* parallel.
|
||||
*
|
||||
* It is not (yet) a smart matcher - it doesn't do any advanced backtracking.
|
||||
* Given the pattern <code>DIV, SPAN</code> the matcher will not match
|
||||
* <code>DIV, DIV, SPAN</code> because it starts matching at the first
|
||||
* <code>DIV</code>, fails to match <code>SPAN</code> at the second, and never
|
||||
* backtracks to try again.
|
||||
*
|
||||
* It is also possible to have a set of complex patterns that when matched in
|
||||
* parallel will miss some possible matches. Running multiple times will catch
|
||||
* all matches eventually.
|
||||
*
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.Matcher = function() {
|
||||
this.patterns_ = [];
|
||||
this.callbacks_ = [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Array of patterns to attempt to match in parallel.
|
||||
*
|
||||
* @type {Array<goog.dom.pattern.AbstractPattern>}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.patterns_;
|
||||
|
||||
|
||||
/**
|
||||
* Array of callbacks to call when a pattern is matched. The indexing is the
|
||||
* same as the {@link #patterns_} array.
|
||||
*
|
||||
* @type {Array<Function>}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.callbacks_;
|
||||
|
||||
|
||||
/**
|
||||
* Adds a pattern to be matched. The callback can return an object whose keys
|
||||
* are processing instructions.
|
||||
*
|
||||
* @param {goog.dom.pattern.AbstractPattern} pattern The pattern to add.
|
||||
* @param {Function} callback Function to call when a match is found. Uses
|
||||
* the above semantics.
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.addPattern = function(pattern, callback) {
|
||||
this.patterns_.push(pattern);
|
||||
this.callbacks_.push(callback);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Resets all the patterns.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.reset_ = function() {
|
||||
for (var i = 0, len = this.patterns_.length; i < len; i++) {
|
||||
this.patterns_[i].reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test the given node against all patterns.
|
||||
*
|
||||
* @param {goog.dom.TagIterator} position A position in a node walk that is
|
||||
* located at the token to process.
|
||||
* @return {boolean} Whether a pattern modified the position or tree
|
||||
* and its callback resulted in DOM structure or position modification.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.matchToken_ = function(position) {
|
||||
for (var i = 0, len = this.patterns_.length; i < len; i++) {
|
||||
var pattern = this.patterns_[i];
|
||||
switch (pattern.matchToken(position.node, position.tagType)) {
|
||||
case goog.dom.pattern.MatchType.MATCH:
|
||||
case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
|
||||
var callback = this.callbacks_[i];
|
||||
|
||||
// Callbacks are allowed to modify the current position, but must
|
||||
// return true if the do.
|
||||
if (callback(pattern.matchedNode, position, pattern)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Match the set of patterns against a match tree.
|
||||
*
|
||||
* @param {Node} node The root node of the tree to match.
|
||||
*/
|
||||
goog.dom.pattern.Matcher.prototype.match = function(node) {
|
||||
var position = new goog.dom.TagIterator(node);
|
||||
|
||||
this.reset_();
|
||||
|
||||
goog.iter.forEach(position, function() {
|
||||
while (this.matchToken_(position)) {
|
||||
// Since we've moved, our old pattern statuses don't make sense any more.
|
||||
// Reset them.
|
||||
this.reset_();
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<!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>goog.dom.pattern.Matcher Tests</title>
|
||||
<script src="../../base.js"></script>
|
||||
<script type="text/javascript">
|
||||
goog.require('goog.dom.pattern.matcherTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p id="p1">
|
||||
<span id="span1" style="color: red"></span>
|
||||
</p>
|
||||
<p id="p2">
|
||||
<span id="span2" style="color: blue">x</span>
|
||||
</p>
|
||||
<p id="p3">Text</p>
|
||||
<p id="p4">Other Text</p>
|
||||
<span></span>
|
||||
|
||||
<div id="div1"><b>x</b><b>y</b><i>z</i></div>
|
||||
|
||||
<p id="p5"><b id="b1">x</b><b id="b2">y</b><i id="i1">z</i></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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.pattern.matcherTest');
|
||||
goog.setTestOnly('goog.dom.pattern.matcherTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.pattern.EndTag');
|
||||
goog.require('goog.dom.pattern.FullTag');
|
||||
goog.require('goog.dom.pattern.Matcher');
|
||||
goog.require('goog.dom.pattern.Repeat');
|
||||
goog.require('goog.dom.pattern.Sequence');
|
||||
goog.require('goog.dom.pattern.StartTag');
|
||||
goog.require('goog.dom.pattern.callback.Counter');
|
||||
goog.require('goog.dom.pattern.callback.Test');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
function testMatcherAndStartTag() {
|
||||
var pattern = new goog.dom.pattern.StartTag('P');
|
||||
|
||||
var counter = new goog.dom.pattern.callback.Counter();
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, counter.getCallback());
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('StartTag(p) should match 5 times in body', 5,
|
||||
counter.count);
|
||||
}
|
||||
|
||||
function testMatcherAndStartTagTwice() {
|
||||
var pattern = new goog.dom.pattern.StartTag('P');
|
||||
|
||||
var counter = new goog.dom.pattern.callback.Counter();
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, counter.getCallback());
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('StartTag(p) should match 5 times in body', 5,
|
||||
counter.count);
|
||||
|
||||
// Make sure no state got mangled.
|
||||
counter.reset();
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('StartTag(p) should match 5 times in body again', 5,
|
||||
counter.count);
|
||||
}
|
||||
|
||||
function testMatcherAndStartTagAttributes() {
|
||||
var pattern = new goog.dom.pattern.StartTag('SPAN', {id: /./});
|
||||
|
||||
var counter = new goog.dom.pattern.callback.Counter();
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, counter.getCallback());
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('StartTag(span,id) should match 2 times in body', 2,
|
||||
counter.count);
|
||||
}
|
||||
|
||||
function testMatcherWithTwoPatterns() {
|
||||
var pattern1 = new goog.dom.pattern.StartTag('SPAN');
|
||||
var pattern2 = new goog.dom.pattern.StartTag('P');
|
||||
|
||||
var counter = new goog.dom.pattern.callback.Counter();
|
||||
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern1, counter.getCallback());
|
||||
matcher.addPattern(pattern2, counter.getCallback());
|
||||
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('StartTag(span|p) should match 8 times in body', 8,
|
||||
counter.count);
|
||||
}
|
||||
|
||||
function testMatcherWithQuit() {
|
||||
var pattern1 = new goog.dom.pattern.StartTag('SPAN');
|
||||
var pattern2 = new goog.dom.pattern.StartTag('P');
|
||||
|
||||
var count = 0;
|
||||
var callback = function(node, position) {
|
||||
if (node.nodeName == 'SPAN') {
|
||||
throw goog.iter.StopIteration;
|
||||
return true;
|
||||
}
|
||||
count++;
|
||||
};
|
||||
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern1, callback);
|
||||
matcher.addPattern(pattern2, callback);
|
||||
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('Stopped span|p should match 1 time in body', 1, count);
|
||||
}
|
||||
|
||||
function testMatcherWithReplace() {
|
||||
var pattern1 = new goog.dom.pattern.StartTag('B');
|
||||
var pattern2 = new goog.dom.pattern.StartTag('I');
|
||||
|
||||
var count = 0;
|
||||
var callback = function(node, position) {
|
||||
count++;
|
||||
if (node.nodeName == 'B') {
|
||||
var i = goog.dom.createDom('I');
|
||||
node.parentNode.insertBefore(i, node);
|
||||
goog.dom.removeNode(node);
|
||||
|
||||
position.setPosition(i);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern1, callback);
|
||||
matcher.addPattern(pattern2, callback);
|
||||
|
||||
matcher.match(goog.dom.getElement('div1'));
|
||||
|
||||
assertEquals('i|b->i should match 5 times in div1', 5, count);
|
||||
}
|
||||
|
||||
function testMatcherAndFullTag() {
|
||||
var pattern = new goog.dom.pattern.FullTag('P');
|
||||
|
||||
var test = new goog.dom.pattern.callback.Test();
|
||||
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, test.getCallback());
|
||||
|
||||
matcher.match(goog.dom.getElement('p1'));
|
||||
|
||||
assert('FullTag(p) should match on p1', test.matched);
|
||||
|
||||
test.reset();
|
||||
matcher.match(goog.dom.getElement('div1'));
|
||||
|
||||
assert('FullTag(p) should not match on div1', !test.matched);
|
||||
}
|
||||
|
||||
function testMatcherAndSequence() {
|
||||
var pattern = new goog.dom.pattern.Sequence([
|
||||
new goog.dom.pattern.StartTag('P'),
|
||||
new goog.dom.pattern.StartTag('SPAN'),
|
||||
new goog.dom.pattern.EndTag('SPAN'),
|
||||
new goog.dom.pattern.EndTag('P')
|
||||
], true);
|
||||
|
||||
var counter = new goog.dom.pattern.callback.Counter();
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, counter.getCallback());
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('Sequence should match 1 times in body', 1, counter.count);
|
||||
}
|
||||
|
||||
function testMatcherAndRepeatFullTag() {
|
||||
var pattern = new goog.dom.pattern.Repeat(
|
||||
new goog.dom.pattern.FullTag('P'), 1);
|
||||
|
||||
var count = 0;
|
||||
var tcount = 0;
|
||||
var matcher = new goog.dom.pattern.Matcher();
|
||||
matcher.addPattern(pattern, function() {
|
||||
count++;
|
||||
tcount += pattern.count;
|
||||
});
|
||||
matcher.match(document.body);
|
||||
|
||||
assertEquals('Repeated p should match 2 times in body', 2, count);
|
||||
assertEquals('Repeated p should match 5 total times in body', 5, tcount);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2008 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 DOM pattern to match a node of the given type.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.NodeType');
|
||||
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches any node of the given type.
|
||||
* @param {goog.dom.NodeType} nodeType The node type to match.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.NodeType = function(nodeType) {
|
||||
/**
|
||||
* The node type to match.
|
||||
* @type {goog.dom.NodeType}
|
||||
* @private
|
||||
*/
|
||||
this.nodeType_ = nodeType;
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.NodeType, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is a text token which matches the string or
|
||||
* regular expression provided in the constructor.
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> if the pattern
|
||||
* matches, <code>NO_MATCH</code> otherwise.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.NodeType.prototype.matchToken = function(token, type) {
|
||||
return token.nodeType == this.nodeType_ ?
|
||||
goog.dom.pattern.MatchType.MATCH :
|
||||
goog.dom.pattern.MatchType.NO_MATCH;
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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 DOM patterns. Allows for description of complex DOM patterns
|
||||
* using regular expression like constructs.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern');
|
||||
goog.provide('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
/**
|
||||
* Regular expression for breaking text nodes.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
goog.dom.pattern.BREAKING_TEXTNODE_RE = /^\s*$/;
|
||||
|
||||
|
||||
/**
|
||||
* Utility function to match a string against either a string or a regular
|
||||
* expression.
|
||||
*
|
||||
* @param {string|RegExp} obj Either a string or a regular expression.
|
||||
* @param {string} str The string to match.
|
||||
* @return {boolean} Whether the strings are equal, or if the string matches
|
||||
* the regular expression.
|
||||
*/
|
||||
goog.dom.pattern.matchStringOrRegex = function(obj, str) {
|
||||
if (goog.isString(obj)) {
|
||||
// Match a string
|
||||
return str == obj;
|
||||
} else {
|
||||
// Match a regular expression
|
||||
return !!(str && str.match(obj));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Utility function to match a DOM attribute against either a string or a
|
||||
* regular expression. Conforms to the interface spec for
|
||||
* {@link goog.object#every}.
|
||||
*
|
||||
* @param {string|RegExp} elem Either a string or a regular expression.
|
||||
* @param {string} index The attribute name to match.
|
||||
* @param {Object} orig The original map of matches to test.
|
||||
* @return {boolean} Whether the strings are equal, or if the attribute matches
|
||||
* the regular expression.
|
||||
* @this {Element} Called using goog.object every on an Element.
|
||||
*/
|
||||
goog.dom.pattern.matchStringOrRegexMap = function(elem, index, orig) {
|
||||
return goog.dom.pattern.matchStringOrRegex(elem,
|
||||
index in this ? this[index] :
|
||||
(this.getAttribute ? this.getAttribute(index) : null));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* When matched to a token, a pattern may return any of the following statuses:
|
||||
* <ol>
|
||||
* <li><code>NO_MATCH</code> - The pattern does not match. This is the only
|
||||
* value that evaluates to <code>false</code> in a boolean context.
|
||||
* <li><code>MATCHING</code> - The token is part of an incomplete match.
|
||||
* <li><code>MATCH</code> - The token completes a match.
|
||||
* <li><code>BACKTRACK_MATCH</code> - The token does not match, but indicates
|
||||
* the end of a repetitive match. For instance, in regular expressions,
|
||||
* the pattern <code>/a+/</code> would match <code>'aaaaaaaab'</code>.
|
||||
* Every <code>'a'</code> token would give a status of
|
||||
* <code>MATCHING</code> while the <code>'b'</code> token would give a
|
||||
* status of <code>BACKTRACK_MATCH</code>.
|
||||
* </ol>
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.dom.pattern.MatchType = {
|
||||
NO_MATCH: 0,
|
||||
MATCHING: 1,
|
||||
MATCH: 2,
|
||||
BACKTRACK_MATCH: 3
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<!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>goog.dom.pattern Tests</title>
|
||||
<script src="../../base.js"></script>
|
||||
<script type="text/javascript">
|
||||
goog.require('goog.dom.patternTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="div1">
|
||||
<span id="span1" style="color: red"></span>
|
||||
</div>
|
||||
<div id="div2">
|
||||
<span id="span2" style="color: blue">x</span>
|
||||
</div>
|
||||
<div id="div3">Text</div>
|
||||
<div id="div4">Other Text</div>
|
||||
<span></span>
|
||||
|
||||
<!-- This chunk gets deleted! -->
|
||||
<p id="p1"><b>x</b><b>y</b><i>z</i></p>
|
||||
|
||||
<div id="div5"><b id="b1">x</b><b id="b2">y</b><i id="i1">z</i></div>
|
||||
|
||||
<span id="span3"><span><span>X</span></span></span>
|
||||
|
||||
<div id="nodeTypes"><!-- Comment -->Text</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,592 @@
|
||||
// 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.patternTest');
|
||||
goog.setTestOnly('goog.dom.patternTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TagWalkType');
|
||||
goog.require('goog.dom.pattern.AllChildren');
|
||||
goog.require('goog.dom.pattern.ChildMatches');
|
||||
goog.require('goog.dom.pattern.EndTag');
|
||||
goog.require('goog.dom.pattern.FullTag');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
goog.require('goog.dom.pattern.NodeType');
|
||||
goog.require('goog.dom.pattern.Repeat');
|
||||
goog.require('goog.dom.pattern.Sequence');
|
||||
goog.require('goog.dom.pattern.StartTag');
|
||||
goog.require('goog.dom.pattern.Text');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
// TODO(robbyw): write a test that checks if backtracking works in Sequence
|
||||
|
||||
function testStartTag() {
|
||||
var pattern = new goog.dom.pattern.StartTag('DIV');
|
||||
assertEquals(
|
||||
'StartTag(div) should match div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(div) should not match span',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(div) should not match /div',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testStartTagCase() {
|
||||
var pattern = new goog.dom.pattern.StartTag('diV');
|
||||
assertEquals(
|
||||
'StartTag(diV) should match div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(diV) should not match span',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testStartTagRegex() {
|
||||
var pattern = new goog.dom.pattern.StartTag(/D/);
|
||||
assertEquals(
|
||||
'StartTag(/D/) should match div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(/D/) should not match span',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(/D/) should not match /div',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testStartTagAttributes() {
|
||||
var pattern = new goog.dom.pattern.StartTag('DIV', {id: 'div1'});
|
||||
assertEquals(
|
||||
'StartTag(div,id:div1) should match div1',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals('StartTag(div,id:div2) should not match div1',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testStartTagStyle() {
|
||||
var pattern = new goog.dom.pattern.StartTag('SPAN', null, {color: 'red'});
|
||||
assertEquals(
|
||||
'StartTag(span,null,color:red) should match span1',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(span,null,color:blue) should not match span1',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testStartTagAttributeRegex() {
|
||||
var pattern = new goog.dom.pattern.StartTag('SPAN', {id: /span\d/});
|
||||
assertEquals(
|
||||
'StartTag(span,id:/span\\d/) should match span1',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'StartTag(span,id:/span\\d/) should match span2',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testEndTag() {
|
||||
var pattern = new goog.dom.pattern.EndTag('DIV');
|
||||
assertEquals(
|
||||
'EndTag should match div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testEndTagRegex() {
|
||||
var pattern = new goog.dom.pattern.EndTag(/D/);
|
||||
assertEquals(
|
||||
'EndTag(/D/) should match /div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'EndTag(/D/) should not match /span',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'EndTag(/D/) should not match div',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testChildMatches() {
|
||||
var pattern = new goog.dom.pattern.ChildMatches(
|
||||
new goog.dom.pattern.StartTag('DIV'), 2);
|
||||
|
||||
assertEquals(
|
||||
'ChildMatches should match div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should match /div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should match div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should match /div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div2'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should finish match at /body',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
document.body,
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
|
||||
assertEquals(
|
||||
'ChildMatches should match div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should match /div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div2'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'ChildMatches should fail to match at /body: not enough child matches',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
document.body,
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testFullTag() {
|
||||
var pattern = new goog.dom.pattern.FullTag('DIV');
|
||||
assertEquals(
|
||||
'FullTag(div) should match div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'FullTag(div) should match /div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
|
||||
assertEquals(
|
||||
'FullTag(div) should start match at div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'FullTag(div) should continue to match span',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'FullTag(div) should continue to match /span',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'FullTag(div) should finish match at /div',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testAllChildren() {
|
||||
var pattern = new goog.dom.pattern.AllChildren();
|
||||
assertEquals(
|
||||
'AllChildren(div) should match div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should match /div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should match at /body',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
document.body,
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
|
||||
assertEquals(
|
||||
'AllChildren(div) should start match at div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should continue to match span',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should continue to match /span',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should continue to match at /div',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'AllChildren(div) should finish match at /body',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
document.body,
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testText() {
|
||||
var pattern = new goog.dom.pattern.Text('Text');
|
||||
assertEquals(
|
||||
'Text should match div3/text()',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div3').firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
assertEquals(
|
||||
'Text should not match div4/text()',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div4').firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
assertEquals(
|
||||
'Text should not match div3',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div3'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
|
||||
}
|
||||
|
||||
function testTextRegex() {
|
||||
var pattern = new goog.dom.pattern.Text(/Text/);
|
||||
assertEquals(
|
||||
'Text(regex) should match div3/text()',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div3').firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
assertEquals(
|
||||
'Text(regex) should match div4/text()',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div4').firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
}
|
||||
|
||||
function testNodeType() {
|
||||
var pattern = new goog.dom.pattern.NodeType(goog.dom.NodeType.COMMENT);
|
||||
assertEquals('Comment matcher should match a comment',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('nodeTypes').firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
assertEquals('Comment matcher should not match a text node',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('nodeTypes').lastChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
}
|
||||
|
||||
function testSequence() {
|
||||
var pattern = new goog.dom.pattern.Sequence([
|
||||
new goog.dom.pattern.StartTag('DIV'),
|
||||
new goog.dom.pattern.StartTag('SPAN'),
|
||||
new goog.dom.pattern.EndTag('SPAN'),
|
||||
new goog.dom.pattern.EndTag('DIV')]);
|
||||
|
||||
assertEquals(
|
||||
'Sequence[0] should match div1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[1] should match span1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[2] should match /span1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'Sequence[3] should match /div1',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
|
||||
assertEquals(
|
||||
'Sequence[0] should match div1 again',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[1] should match span1 again',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[2] should match /span1 again',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'Sequence[3] should match /div1 again',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
|
||||
assertEquals(
|
||||
'Sequence[0] should match div1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[1] should not match div1',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
|
||||
assertEquals(
|
||||
'Sequence[0] should match div1 after failure',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[1] should match span1 after failure',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[2] should match /span1 after failure',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('span1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
assertEquals(
|
||||
'Sequence[3] should match /div1 after failure',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('div1'),
|
||||
goog.dom.TagWalkType.END_TAG));
|
||||
}
|
||||
|
||||
function testRepeat() {
|
||||
var pattern = new goog.dom.pattern.Repeat(
|
||||
new goog.dom.pattern.StartTag('B'));
|
||||
|
||||
// Note: this test does not mimic an actual matcher because it is only
|
||||
// passing the START_TAG events.
|
||||
|
||||
assertEquals(
|
||||
'Repeat[B] should match b1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('b1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B] should match b2',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('b2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B] should backtrack match i1',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('i1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B] should have match count of 2',
|
||||
2,
|
||||
pattern.count);
|
||||
|
||||
assertEquals(
|
||||
'Repeat[B] should backtrack match i1 even with no b matches',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('i1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B] should have match count of 0',
|
||||
0,
|
||||
pattern.count);
|
||||
}
|
||||
|
||||
function testRepeatWithMinimum() {
|
||||
var pattern = new goog.dom.pattern.Repeat(
|
||||
new goog.dom.pattern.StartTag('B'), 1);
|
||||
|
||||
// Note: this test does not mimic an actual matcher because it is only
|
||||
// passing the START_TAG events.
|
||||
|
||||
assertEquals(
|
||||
'Repeat[B,1] should match b1',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('b1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B,1] should match b2',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('b2'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B,1] should backtrack match i1',
|
||||
goog.dom.pattern.MatchType.BACKTRACK_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('i1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Repeat[B,1] should have match count of 2',
|
||||
2,
|
||||
pattern.count);
|
||||
|
||||
assertEquals(
|
||||
'Repeat[B,1] should not match i1',
|
||||
goog.dom.pattern.MatchType.NO_MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('i1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testRepeatWithMaximum() {
|
||||
var pattern = new goog.dom.pattern.Repeat(
|
||||
new goog.dom.pattern.StartTag('B'), 1, 1);
|
||||
|
||||
// Note: this test does not mimic an actual matcher because it is only
|
||||
// passing the START_TAG events.
|
||||
|
||||
assertEquals(
|
||||
'Repeat[B,1] should match b1',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(
|
||||
goog.dom.getElement('b1'),
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
}
|
||||
|
||||
function testSequenceBacktrack() {
|
||||
var pattern = new goog.dom.pattern.Sequence([
|
||||
new goog.dom.pattern.Repeat(new goog.dom.pattern.StartTag('SPAN')),
|
||||
new goog.dom.pattern.Text('X')]);
|
||||
|
||||
var root = goog.dom.getElement('span3');
|
||||
assertEquals(
|
||||
'Sequence[Repeat[SPAN],"X"] should match span3',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(root, goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[Repeat[SPAN],"X"] should match span3.firstChild',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(root.firstChild,
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[Repeat[SPAN],"X"] should match span3.firstChild.firstChild',
|
||||
goog.dom.pattern.MatchType.MATCHING,
|
||||
pattern.matchToken(root.firstChild.firstChild,
|
||||
goog.dom.TagWalkType.START_TAG));
|
||||
assertEquals(
|
||||
'Sequence[Repeat[SPAN],"X"] should finish match text node',
|
||||
goog.dom.pattern.MatchType.MATCH,
|
||||
pattern.matchToken(root.firstChild.firstChild.firstChild,
|
||||
goog.dom.TagWalkType.OTHER));
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// 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 DOM pattern to match a tag and all of its children.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.Repeat');
|
||||
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches a repetition of another pattern.
|
||||
* @param {goog.dom.pattern.AbstractPattern} pattern The pattern to
|
||||
* repetitively match.
|
||||
* @param {number=} opt_minimum The minimum number of times to match. Defaults
|
||||
* to 0.
|
||||
* @param {number=} opt_maximum The maximum number of times to match. Defaults
|
||||
* to unlimited.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.Repeat = function(pattern,
|
||||
opt_minimum,
|
||||
opt_maximum) {
|
||||
this.pattern_ = pattern;
|
||||
this.minimum_ = opt_minimum || 0;
|
||||
this.maximum_ = opt_maximum || null;
|
||||
this.matches = [];
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.Repeat, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* Pattern to repetitively match.
|
||||
*
|
||||
* @type {goog.dom.pattern.AbstractPattern}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.pattern_;
|
||||
|
||||
|
||||
/**
|
||||
* Minimum number of times to match the pattern.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.minimum_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Optional maximum number of times to match the pattern. A {@code null} value
|
||||
* will be treated as infinity.
|
||||
*
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.maximum_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Number of times the pattern has matched.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.count = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the pattern has recently matched or failed to match and will need to
|
||||
* be reset when starting a new round of matches.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.needsReset_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* The matched nodes.
|
||||
*
|
||||
* @type {Array<Node>}
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.matches;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token continues a repeated series of matches of the
|
||||
* pattern given in the constructor.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> if the pattern
|
||||
* matches, <code>BACKTRACK_MATCH</code> if the pattern does not match
|
||||
* but already had accumulated matches, <code>MATCHING</code> if the pattern
|
||||
* starts a match, and <code>NO_MATCH</code> if the pattern does not match.
|
||||
* @suppress {missingProperties} See the broken line below.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.matchToken = function(token, type) {
|
||||
// Reset if we're starting a new match
|
||||
if (this.needsReset_) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// If the option is set, ignore any whitespace only text nodes
|
||||
if (token.nodeType == goog.dom.NodeType.TEXT &&
|
||||
token.nodeValue.match(/^\s+$/)) {
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
}
|
||||
|
||||
switch (this.pattern_.matchToken(token, type)) {
|
||||
case goog.dom.pattern.MatchType.MATCH:
|
||||
// Record the first token we match.
|
||||
if (this.count == 0) {
|
||||
this.matchedNode = token;
|
||||
}
|
||||
|
||||
// Mark the match
|
||||
this.count++;
|
||||
|
||||
// Add to the list
|
||||
this.matches.push(this.pattern_.matchedNode);
|
||||
|
||||
// Check if this match hits our maximum
|
||||
if (this.maximum_ !== null && this.count == this.maximum_) {
|
||||
this.needsReset_ = true;
|
||||
return goog.dom.pattern.MatchType.MATCH;
|
||||
} else {
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
}
|
||||
|
||||
case goog.dom.pattern.MatchType.MATCHING:
|
||||
// This can happen when our child pattern is a sequence or a repetition.
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
|
||||
case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
|
||||
// This happens if our child pattern is repetitive too.
|
||||
// TODO(robbyw): Backtrack further if necessary.
|
||||
this.count++;
|
||||
|
||||
// NOTE(nicksantos): This line of code is broken. this.patterns_ doesn't
|
||||
// exist, and this.currentPosition_ doesn't exit. When this is fixed,
|
||||
// remove the missingProperties suppression above.
|
||||
if (this.currentPosition_ == this.patterns_.length) {
|
||||
this.needsReset_ = true;
|
||||
return goog.dom.pattern.MatchType.BACKTRACK_MATCH;
|
||||
} else {
|
||||
// Retry the same token on the next iteration of the child pattern.
|
||||
return this.matchToken(token, type);
|
||||
}
|
||||
|
||||
default:
|
||||
this.needsReset_ = true;
|
||||
if (this.count >= this.minimum_) {
|
||||
return goog.dom.pattern.MatchType.BACKTRACK_MATCH;
|
||||
} else {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset any internal state this pattern keeps.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Repeat.prototype.reset = function() {
|
||||
this.pattern_.reset();
|
||||
this.count = 0;
|
||||
this.needsReset_ = false;
|
||||
this.matches.length = 0;
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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 DOM pattern to match a sequence of other patterns.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.Sequence');
|
||||
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.pattern');
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches a sequence of other patterns.
|
||||
*
|
||||
* @param {Array<goog.dom.pattern.AbstractPattern>} patterns Ordered array of
|
||||
* patterns to match.
|
||||
* @param {boolean=} opt_ignoreWhitespace Optional flag to ignore text nodes
|
||||
* consisting entirely of whitespace. The default is to not ignore them.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.Sequence = function(patterns, opt_ignoreWhitespace) {
|
||||
this.patterns = patterns;
|
||||
this.ignoreWhitespace_ = !!opt_ignoreWhitespace;
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.Sequence, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* Ordered array of patterns to match.
|
||||
*
|
||||
* @type {Array<goog.dom.pattern.AbstractPattern>}
|
||||
*/
|
||||
goog.dom.pattern.Sequence.prototype.patterns;
|
||||
|
||||
|
||||
/**
|
||||
* Position in the patterns array we have reached by successful matches.
|
||||
*
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Sequence.prototype.currentPosition_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Whether or not to ignore whitespace only Text nodes.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Sequence.prototype.ignoreWhitespace_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token starts, continues, or finishes the sequence
|
||||
* of patterns given in the constructor.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> if the pattern
|
||||
* matches, <code>MATCHING</code> if the pattern starts a match, and
|
||||
* <code>NO_MATCH</code> if the pattern does not match.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Sequence.prototype.matchToken = function(token, type) {
|
||||
// If the option is set, ignore any whitespace only text nodes
|
||||
if (this.ignoreWhitespace_ && token.nodeType == goog.dom.NodeType.TEXT &&
|
||||
goog.dom.pattern.BREAKING_TEXTNODE_RE.test(token.nodeValue)) {
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
}
|
||||
|
||||
switch (this.patterns[this.currentPosition_].matchToken(token, type)) {
|
||||
case goog.dom.pattern.MatchType.MATCH:
|
||||
// Record the first token we match.
|
||||
if (this.currentPosition_ == 0) {
|
||||
this.matchedNode = token;
|
||||
}
|
||||
|
||||
// Move forward one position.
|
||||
this.currentPosition_++;
|
||||
|
||||
// Check if this is the last position.
|
||||
if (this.currentPosition_ == this.patterns.length) {
|
||||
this.reset();
|
||||
return goog.dom.pattern.MatchType.MATCH;
|
||||
} else {
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
}
|
||||
|
||||
case goog.dom.pattern.MatchType.MATCHING:
|
||||
// This can happen when our child pattern is a sequence or a repetition.
|
||||
return goog.dom.pattern.MatchType.MATCHING;
|
||||
|
||||
case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
|
||||
// This means a repetitive match succeeded 1 token ago.
|
||||
// TODO(robbyw): Backtrack further if necessary.
|
||||
this.currentPosition_++;
|
||||
|
||||
if (this.currentPosition_ == this.patterns.length) {
|
||||
this.reset();
|
||||
return goog.dom.pattern.MatchType.BACKTRACK_MATCH;
|
||||
} else {
|
||||
// Retry the same token on the next pattern.
|
||||
return this.matchToken(token, type);
|
||||
}
|
||||
|
||||
default:
|
||||
this.reset();
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reset any internal state this pattern keeps.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Sequence.prototype.reset = function() {
|
||||
if (this.patterns[this.currentPosition_]) {
|
||||
this.patterns[this.currentPosition_].reset();
|
||||
}
|
||||
this.currentPosition_ = 0;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 DOM pattern to match the start of a tag.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.StartTag');
|
||||
|
||||
goog.require('goog.dom.TagWalkType');
|
||||
goog.require('goog.dom.pattern.Tag');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches an opening tag.
|
||||
*
|
||||
* @param {string|RegExp} tag Name of the tag. Also will accept a regular
|
||||
* expression to match against the tag name.
|
||||
* @param {Object=} opt_attrs Optional map of attribute names to desired values.
|
||||
* This pattern will only match when all attributes are present and match
|
||||
* the string or regular expression value provided here.
|
||||
* @param {Object=} opt_styles Optional map of CSS style names to desired
|
||||
* values. This pattern will only match when all styles are present and
|
||||
* match the string or regular expression value provided here.
|
||||
* @param {Function=} opt_test Optional function that takes the element as a
|
||||
* parameter and returns true if this pattern should match it.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.Tag}
|
||||
*/
|
||||
goog.dom.pattern.StartTag = function(tag, opt_attrs, opt_styles, opt_test) {
|
||||
goog.dom.pattern.Tag.call(
|
||||
this,
|
||||
tag,
|
||||
goog.dom.TagWalkType.START_TAG,
|
||||
opt_attrs,
|
||||
opt_styles,
|
||||
opt_test);
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.StartTag, goog.dom.pattern.Tag);
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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 DOM pattern to match a tag.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.Tag');
|
||||
|
||||
goog.require('goog.dom.pattern');
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
goog.require('goog.object');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches an tag.
|
||||
*
|
||||
* @param {string|RegExp} tag Name of the tag. Also will accept a regular
|
||||
* expression to match against the tag name.
|
||||
* @param {goog.dom.TagWalkType} type Type of token to match.
|
||||
* @param {Object=} opt_attrs Optional map of attribute names to desired values.
|
||||
* This pattern will only match when all attributes are present and match
|
||||
* the string or regular expression value provided here.
|
||||
* @param {Object=} opt_styles Optional map of CSS style names to desired
|
||||
* values. This pattern will only match when all styles are present and
|
||||
* match the string or regular expression value provided here.
|
||||
* @param {Function=} opt_test Optional function that takes the element as a
|
||||
* parameter and returns true if this pattern should match it.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
*/
|
||||
goog.dom.pattern.Tag = function(tag, type, opt_attrs, opt_styles, opt_test) {
|
||||
if (goog.isString(tag)) {
|
||||
this.tag_ = tag.toUpperCase();
|
||||
} else {
|
||||
this.tag_ = tag;
|
||||
}
|
||||
|
||||
this.type_ = type;
|
||||
|
||||
this.attrs_ = opt_attrs || null;
|
||||
this.styles_ = opt_styles || null;
|
||||
this.test_ = opt_test || null;
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.Tag, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* The tag to match.
|
||||
*
|
||||
* @type {string|RegExp}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.tag_;
|
||||
|
||||
|
||||
/**
|
||||
* The type of token to match.
|
||||
*
|
||||
* @type {goog.dom.TagWalkType}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.type_;
|
||||
|
||||
|
||||
/**
|
||||
* The attributes to test for.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.attrs_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The styles to test for.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.styles_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Function that takes the element as a parameter and returns true if this
|
||||
* pattern should match it.
|
||||
*
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.test_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is a tag token which matches the tag name,
|
||||
* style, and attributes provided in the constructor.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> if the pattern
|
||||
* matches, <code>NO_MATCH</code> otherwise.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Tag.prototype.matchToken = function(token, type) {
|
||||
// Check the direction and tag name.
|
||||
if (type == this.type_ &&
|
||||
goog.dom.pattern.matchStringOrRegex(this.tag_, token.nodeName)) {
|
||||
// Check the attributes.
|
||||
if (this.attrs_ &&
|
||||
!goog.object.every(
|
||||
this.attrs_,
|
||||
goog.dom.pattern.matchStringOrRegexMap,
|
||||
token)) {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
// Check the styles.
|
||||
if (this.styles_ &&
|
||||
!goog.object.every(
|
||||
this.styles_,
|
||||
goog.dom.pattern.matchStringOrRegexMap,
|
||||
token.style)) {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
|
||||
if (this.test_ && !this.test_(token)) {
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
}
|
||||
|
||||
// If we reach this point, we have a match and should save it.
|
||||
this.matchedNode = token;
|
||||
return goog.dom.pattern.MatchType.MATCH;
|
||||
}
|
||||
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 DOM pattern to match a text node.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.pattern.Text');
|
||||
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.pattern');
|
||||
goog.require('goog.dom.pattern.AbstractPattern');
|
||||
goog.require('goog.dom.pattern.MatchType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pattern object that matches text by exact matching or regular expressions.
|
||||
*
|
||||
* @param {string|RegExp} match String or regular expression to match against.
|
||||
* @constructor
|
||||
* @extends {goog.dom.pattern.AbstractPattern}
|
||||
* @final
|
||||
*/
|
||||
goog.dom.pattern.Text = function(match) {
|
||||
this.match_ = match;
|
||||
};
|
||||
goog.inherits(goog.dom.pattern.Text, goog.dom.pattern.AbstractPattern);
|
||||
|
||||
|
||||
/**
|
||||
* The text or regular expression to match.
|
||||
*
|
||||
* @type {string|RegExp}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.pattern.Text.prototype.match_;
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the given token is a text token which matches the string or
|
||||
* regular expression provided in the constructor.
|
||||
*
|
||||
* @param {Node} token Token to match against.
|
||||
* @param {goog.dom.TagWalkType} type The type of token.
|
||||
* @return {goog.dom.pattern.MatchType} <code>MATCH</code> if the pattern
|
||||
* matches, <code>NO_MATCH</code> otherwise.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.pattern.Text.prototype.matchToken = function(token, type) {
|
||||
if (token.nodeType == goog.dom.NodeType.TEXT &&
|
||||
goog.dom.pattern.matchStringOrRegex(this.match_, token.nodeValue)) {
|
||||
this.matchedNode = token;
|
||||
return goog.dom.pattern.MatchType.MATCH;
|
||||
}
|
||||
|
||||
return goog.dom.pattern.MatchType.NO_MATCH;
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for working with ranges in HTML documents.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.Range');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
goog.require('goog.dom.BrowserFeature');
|
||||
goog.require('goog.dom.ControlRange');
|
||||
goog.require('goog.dom.MultiRange');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TextRange');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Create a new selection from the given browser window's current selection.
|
||||
* Note that this object does not auto-update if the user changes their
|
||||
* selection and should be used as a snapshot.
|
||||
* @param {Window=} opt_win The window to get the selection of. Defaults to the
|
||||
* window this class was defined in.
|
||||
* @return {goog.dom.AbstractRange?} A range wrapper object, or null if there
|
||||
* was an error.
|
||||
*/
|
||||
goog.dom.Range.createFromWindow = function(opt_win) {
|
||||
var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(
|
||||
opt_win || window);
|
||||
return sel && goog.dom.Range.createFromBrowserSelection(sel);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper from the given browser selection object. Note
|
||||
* that this object does not auto-update if the user changes their selection and
|
||||
* should be used as a snapshot.
|
||||
* @param {!Object} selection The browser selection object.
|
||||
* @return {goog.dom.AbstractRange?} A range wrapper object or null if there
|
||||
* was an error.
|
||||
*/
|
||||
goog.dom.Range.createFromBrowserSelection = function(selection) {
|
||||
var range;
|
||||
var isReversed = false;
|
||||
if (selection.createRange) {
|
||||
/** @preserveTry */
|
||||
try {
|
||||
range = selection.createRange();
|
||||
} catch (e) {
|
||||
// Access denied errors can be thrown here in IE if the selection was
|
||||
// a flash obj or if there are cross domain issues
|
||||
return null;
|
||||
}
|
||||
} else if (selection.rangeCount) {
|
||||
if (selection.rangeCount > 1) {
|
||||
return goog.dom.MultiRange.createFromBrowserSelection(
|
||||
/** @type {!Selection} */ (selection));
|
||||
} else {
|
||||
range = selection.getRangeAt(0);
|
||||
isReversed = goog.dom.Range.isReversed(selection.anchorNode,
|
||||
selection.anchorOffset, selection.focusNode, selection.focusOffset);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return goog.dom.Range.createFromBrowserRange(range, isReversed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper from the given browser range object.
|
||||
* @param {Range|TextRange} range The browser range object.
|
||||
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
||||
* node.
|
||||
* @return {!goog.dom.AbstractRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.Range.createFromBrowserRange = function(range, opt_isReversed) {
|
||||
// Create an IE control range when appropriate.
|
||||
return goog.dom.AbstractRange.isNativeControlRange(range) ?
|
||||
goog.dom.ControlRange.createFromBrowserRange(range) :
|
||||
goog.dom.TextRange.createFromBrowserRange(range, opt_isReversed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that selects the given node's text.
|
||||
* @param {Node} node The node to select.
|
||||
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
||||
* node.
|
||||
* @return {!goog.dom.AbstractRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.Range.createFromNodeContents = function(node, opt_isReversed) {
|
||||
return goog.dom.TextRange.createFromNodeContents(node, opt_isReversed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that represents a caret at the given node,
|
||||
* accounting for the given offset. This always creates a TextRange, regardless
|
||||
* of whether node is an image node or other control range type node.
|
||||
* @param {Node} node The node to place a caret at.
|
||||
* @param {number} offset The offset within the node to place the caret at.
|
||||
* @return {!goog.dom.AbstractRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.Range.createCaret = function(node, offset) {
|
||||
return goog.dom.TextRange.createFromNodes(node, offset, node, offset);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that selects the area between the given nodes,
|
||||
* accounting for the given offsets.
|
||||
* @param {Node} anchorNode The node to anchor on.
|
||||
* @param {number} anchorOffset The offset within the node to anchor on.
|
||||
* @param {Node} focusNode The node to focus on.
|
||||
* @param {number} focusOffset The offset within the node to focus on.
|
||||
* @return {!goog.dom.AbstractRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.Range.createFromNodes = function(anchorNode, anchorOffset, focusNode,
|
||||
focusOffset) {
|
||||
return goog.dom.TextRange.createFromNodes(anchorNode, anchorOffset, focusNode,
|
||||
focusOffset);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the window's selection.
|
||||
* @param {Window=} opt_win The window to get the selection of. Defaults to the
|
||||
* window this class was defined in.
|
||||
*/
|
||||
goog.dom.Range.clearSelection = function(opt_win) {
|
||||
var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(
|
||||
opt_win || window);
|
||||
if (!sel) {
|
||||
return;
|
||||
}
|
||||
if (sel.empty) {
|
||||
// We can't just check that the selection is empty, becuase IE
|
||||
// sometimes gets confused.
|
||||
try {
|
||||
sel.empty();
|
||||
} catch (e) {
|
||||
// Emptying an already empty selection throws an exception in IE
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
sel.removeAllRanges();
|
||||
} catch (e) {
|
||||
// This throws in IE9 if the range has been invalidated; for example, if
|
||||
// the user clicked on an element which disappeared during the event
|
||||
// handler.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests if the window has a selection.
|
||||
* @param {Window=} opt_win The window to check the selection of. Defaults to
|
||||
* the window this class was defined in.
|
||||
* @return {boolean} Whether the window has a selection.
|
||||
*/
|
||||
goog.dom.Range.hasSelection = function(opt_win) {
|
||||
var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(
|
||||
opt_win || window);
|
||||
return !!sel &&
|
||||
(goog.dom.BrowserFeature.LEGACY_IE_RANGES ?
|
||||
sel.type != 'None' : !!sel.rangeCount);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the focus position occurs before the anchor position.
|
||||
* @param {Node} anchorNode The node to anchor on.
|
||||
* @param {number} anchorOffset The offset within the node to anchor on.
|
||||
* @param {Node} focusNode The node to focus on.
|
||||
* @param {number} focusOffset The offset within the node to focus on.
|
||||
* @return {boolean} Whether the focus position occurs before the anchor
|
||||
* position.
|
||||
*/
|
||||
goog.dom.Range.isReversed = function(anchorNode, anchorOffset, focusNode,
|
||||
focusOffset) {
|
||||
if (anchorNode == focusNode) {
|
||||
return focusOffset < anchorOffset;
|
||||
}
|
||||
var child;
|
||||
if (anchorNode.nodeType == goog.dom.NodeType.ELEMENT && anchorOffset) {
|
||||
child = anchorNode.childNodes[anchorOffset];
|
||||
if (child) {
|
||||
anchorNode = child;
|
||||
anchorOffset = 0;
|
||||
} else if (goog.dom.contains(anchorNode, focusNode)) {
|
||||
// If focus node is contained in anchorNode, it must be before the
|
||||
// end of the node. Hence we are reversed.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (focusNode.nodeType == goog.dom.NodeType.ELEMENT && focusOffset) {
|
||||
child = focusNode.childNodes[focusOffset];
|
||||
if (child) {
|
||||
focusNode = child;
|
||||
focusOffset = 0;
|
||||
} else if (goog.dom.contains(focusNode, anchorNode)) {
|
||||
// If anchor node is contained in focusNode, it must be before the
|
||||
// end of the node. Hence we are not reversed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (goog.dom.compareNodeOrder(anchorNode, focusNode) ||
|
||||
anchorOffset - focusOffset) > 0;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<!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.Range</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.RangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test1">Text</div>
|
||||
<div id="test2">abc<br id="br">def</div>
|
||||
<div id="empty"></div>
|
||||
<div id="test3"><div></div></div>
|
||||
<div id="removeTest"><div>Text that<br/>will be deleted</div></div>
|
||||
<div id="surroundTest"></div>
|
||||
<div id="insertTest"></div>
|
||||
<div id="surroundWithNodesTest"></div>
|
||||
<div id="removePartialTest">012345</div>
|
||||
<table id="tableTest"><tr><td id="cell">1</td><td>2</td></tr></table>
|
||||
<div id="ulTest"><ul><li>1</li><li>2</li></ul></div>
|
||||
<div id="olTest"><ol><li>1</li><li>2</li></ol></div>
|
||||
<img id="logo" src="http://www.google.com/intl/en_ALL/images/logo.gif">
|
||||
<div id="removeNodeTest"><div>Will be removed</div></div>
|
||||
|
||||
<div id='bug1480638'></div>
|
||||
<div id='textWithSpaces'>hello world !</div>
|
||||
<div contentEditable=true>
|
||||
<div id='rangeAroundBreaks'>abcd<br />e</div>
|
||||
<div id='breaksAroundNode'><br />abcde<br /></div>
|
||||
</div>
|
||||
|
||||
<!-- A focusable element to help restore focus to a sane state. -->
|
||||
<input id="focusableElement">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,722 @@
|
||||
// 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.RangeTest');
|
||||
goog.setTestOnly('goog.dom.RangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.dom.RangeType');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.dom.TextRange');
|
||||
goog.require('goog.dom.browserrange');
|
||||
goog.require('goog.testing.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var assertRangeEquals = goog.testing.dom.assertRangeEquals;
|
||||
|
||||
function setUp() {
|
||||
// Reset the focus; some tests may invalidate the focus to exercise various
|
||||
// browser bugs.
|
||||
var focusableElement = goog.dom.getElement('focusableElement');
|
||||
focusableElement.focus();
|
||||
focusableElement.blur();
|
||||
}
|
||||
|
||||
function normalizeHtml(str) {
|
||||
return str.toLowerCase().replace(/[\n\r\f"]/g, '')
|
||||
.replace(/<\/li>/g, ''); // " for emacs
|
||||
}
|
||||
|
||||
function testCreate() {
|
||||
assertNotNull('Browser range object can be created for node',
|
||||
goog.dom.Range.createFromNodeContents(goog.dom.getElement('test1')));
|
||||
}
|
||||
|
||||
function testTableRange() {
|
||||
var tr = goog.dom.getElement('cell').parentNode;
|
||||
var range = goog.dom.Range.createFromNodeContents(tr);
|
||||
assertEquals('Selection should have correct text', '12',
|
||||
range.getText());
|
||||
assertEquals('Selection should have correct html fragment',
|
||||
'1</td><td>2', normalizeHtml(range.getHtmlFragment()));
|
||||
|
||||
// TODO(robbyw): On IE the TR is included, on FF it is not.
|
||||
//assertEquals('Selection should have correct valid html',
|
||||
// '<tr id=row><td>1</td><td>2</td></tr>',
|
||||
// normalizeHtml(range.getValidHtml()));
|
||||
|
||||
assertEquals('Selection should have correct pastable html',
|
||||
'<table><tbody><tr><td id=cell>1</td><td>2</td></tr></tbody></table>',
|
||||
normalizeHtml(range.getPastableHtml()));
|
||||
}
|
||||
|
||||
function testUnorderedListRange() {
|
||||
var ul = goog.dom.getElement('ulTest').firstChild;
|
||||
var range = goog.dom.Range.createFromNodeContents(ul);
|
||||
assertEquals('Selection should have correct html fragment',
|
||||
'1<li>2', normalizeHtml(range.getHtmlFragment()));
|
||||
|
||||
// TODO(robbyw): On IE the UL is included, on FF it is not.
|
||||
//assertEquals('Selection should have correct valid html',
|
||||
// '<li>1</li><li>2</li>', normalizeHtml(range.getValidHtml()));
|
||||
|
||||
assertEquals('Selection should have correct pastable html',
|
||||
'<ul><li>1<li>2</ul>',
|
||||
normalizeHtml(range.getPastableHtml()));
|
||||
}
|
||||
|
||||
function testOrderedListRange() {
|
||||
var ol = goog.dom.getElement('olTest').firstChild;
|
||||
var range = goog.dom.Range.createFromNodeContents(ol);
|
||||
assertEquals('Selection should have correct html fragment',
|
||||
'1<li>2', normalizeHtml(range.getHtmlFragment()));
|
||||
|
||||
// TODO(robbyw): On IE the OL is included, on FF it is not.
|
||||
//assertEquals('Selection should have correct valid html',
|
||||
// '<li>1</li><li>2</li>', normalizeHtml(range.getValidHtml()));
|
||||
|
||||
assertEquals('Selection should have correct pastable html',
|
||||
'<ol><li>1<li>2</ol>',
|
||||
normalizeHtml(range.getPastableHtml()));
|
||||
}
|
||||
|
||||
function testCreateFromNodes() {
|
||||
var start = goog.dom.getElement('test1').firstChild;
|
||||
var end = goog.dom.getElement('br');
|
||||
var range = goog.dom.Range.createFromNodes(start, 2, end, 0);
|
||||
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('Start node should be selected at anchor endpoint', start,
|
||||
range.getAnchorNode());
|
||||
assertEquals('Selection should be anchored at offset 2', 2,
|
||||
range.getAnchorOffset());
|
||||
|
||||
var div = goog.dom.getElement('test2');
|
||||
assertEquals('DIV node should be selected at end endpoint', div,
|
||||
range.getEndNode());
|
||||
assertEquals('Selection should end at offset 1', 1, range.getEndOffset());
|
||||
assertEquals('DIV node should be selected at focus endpoint', div,
|
||||
range.getFocusNode());
|
||||
assertEquals('Selection should be focused at offset 1', 1,
|
||||
range.getFocusOffset());
|
||||
|
||||
|
||||
assertTrue('Text content should be "xt\\s*abc"',
|
||||
/xt\s*abc/.test(range.getText()));
|
||||
assertFalse('Nodes range is not collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
|
||||
function testCreateControlRange() {
|
||||
if (!goog.userAgent.IE) {
|
||||
return;
|
||||
}
|
||||
var cr = document.body.createControlRange();
|
||||
cr.addElement(goog.dom.getElement('logo'));
|
||||
|
||||
var range = goog.dom.Range.createFromBrowserRange(cr);
|
||||
assertNotNull('Control range object can be created from browser range',
|
||||
range);
|
||||
assertEquals('Created range is a control range', goog.dom.RangeType.CONTROL,
|
||||
range.getType());
|
||||
}
|
||||
|
||||
|
||||
function testTextNode() {
|
||||
var range = goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('test1').firstChild);
|
||||
|
||||
assertEquals('Created range is a text range', goog.dom.RangeType.TEXT,
|
||||
range.getType());
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
function testDiv() {
|
||||
var range = goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('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', goog.dom.getElement('test2'),
|
||||
range.getContainer());
|
||||
|
||||
assertTrue('Div text content should be "abc\\s*def"',
|
||||
/abc\s*def/.test(range.getText()));
|
||||
assertFalse('Div range is not collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
|
||||
function testEmptyNode() {
|
||||
var range = goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('empty'));
|
||||
|
||||
assertEquals('DIV be selected at start endpoint',
|
||||
goog.dom.getElement('empty'), range.getStartNode());
|
||||
assertEquals('Selection should start at offset 0', 0,
|
||||
range.getStartOffset());
|
||||
|
||||
assertEquals('DIV should be selected at end endpoint',
|
||||
goog.dom.getElement('empty'), range.getEndNode());
|
||||
assertEquals('Selection should end at offset 0', 0,
|
||||
range.getEndOffset());
|
||||
|
||||
assertEquals('Container should be DIV', goog.dom.getElement('empty'),
|
||||
range.getContainer());
|
||||
|
||||
assertEquals('Empty text content should be ""', '', range.getText());
|
||||
assertTrue('Empty range is collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
|
||||
function testCollapse() {
|
||||
var range = goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('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.Range.createFromNodeContents(goog.dom.getElement('empty'));
|
||||
assertTrue('Empty range is collapsed', range.isCollapsed());
|
||||
range.collapse();
|
||||
assertTrue('Empty range is still collapsed', range.isCollapsed());
|
||||
}
|
||||
|
||||
// TODO(robbyw): Test iteration over a strange document fragment.
|
||||
|
||||
function testIterator() {
|
||||
goog.testing.dom.assertNodesMatch(goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('test2')), ['abc', '#br', '#br', 'def']);
|
||||
}
|
||||
|
||||
function testReversedNodes() {
|
||||
var node = goog.dom.getElement('test1').firstChild;
|
||||
var range = goog.dom.Range.createFromNodes(node, 4, node, 0);
|
||||
assertTrue('Range is reversed', range.isReversed());
|
||||
node = goog.dom.getElement('test3');
|
||||
range = goog.dom.Range.createFromNodes(node, 0, node, 1);
|
||||
assertFalse('Range is not reversed', range.isReversed());
|
||||
}
|
||||
|
||||
function testReversedContents() {
|
||||
var range = goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('test1'), true);
|
||||
assertTrue('Range is reversed', range.isReversed());
|
||||
assertEquals('Range should select "Text"', 'Text',
|
||||
range.getText());
|
||||
assertEquals('Range start offset should be 0', 0, range.getStartOffset());
|
||||
assertEquals('Range end offset should be 4', 4, range.getEndOffset());
|
||||
assertEquals('Range anchor offset should be 4', 4, range.getAnchorOffset());
|
||||
assertEquals('Range focus offset should be 0', 0, range.getFocusOffset());
|
||||
|
||||
var range2 = range.clone();
|
||||
|
||||
range.collapse(true);
|
||||
assertTrue('Range is collapsed', range.isCollapsed());
|
||||
assertFalse('Collapsed range is not reversed', range.isReversed());
|
||||
assertEquals('Post collapse start offset should be 4', 4,
|
||||
range.getStartOffset());
|
||||
|
||||
range2.collapse(false);
|
||||
assertTrue('Range 2 is collapsed', range2.isCollapsed());
|
||||
assertFalse('Collapsed range 2 is not reversed', range2.isReversed());
|
||||
assertEquals('Post collapse start offset 2 should be 0', 0,
|
||||
range2.getStartOffset());
|
||||
}
|
||||
|
||||
function testRemoveContents() {
|
||||
var outer = goog.dom.getElement('removeTest');
|
||||
var range = goog.dom.Range.createFromNodeContents(outer.firstChild);
|
||||
|
||||
range.removeContents();
|
||||
|
||||
assertEquals('Removed range content should be ""', '', range.getText());
|
||||
assertTrue('Removed range should be collapsed', range.isCollapsed());
|
||||
assertEquals('Outer div should have 1 child now', 1,
|
||||
outer.childNodes.length);
|
||||
assertEquals('Inner div should be empty', 0,
|
||||
outer.firstChild.childNodes.length);
|
||||
}
|
||||
|
||||
function testRemovePartialContents() {
|
||||
var outer = goog.dom.getElement('removePartialTest');
|
||||
var originalText = goog.dom.getTextContent(outer);
|
||||
|
||||
try {
|
||||
var range = goog.dom.Range.createFromNodes(outer.firstChild, 2,
|
||||
outer.firstChild, 4);
|
||||
removeHelper(1, range, outer, 1, '0145');
|
||||
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 0,
|
||||
outer.firstChild, 1);
|
||||
removeHelper(2, range, outer, 1, '145');
|
||||
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 2,
|
||||
outer.firstChild, 3);
|
||||
removeHelper(3, range, outer, 1, '14');
|
||||
|
||||
var br = goog.dom.createDom('BR');
|
||||
outer.appendChild(br);
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 1,
|
||||
outer, 1);
|
||||
removeHelper(4, range, outer, 2, '1<br>');
|
||||
|
||||
outer.innerHTML = '<br>123';
|
||||
range = goog.dom.Range.createFromNodes(outer, 0, outer.lastChild, 2);
|
||||
removeHelper(5, range, outer, 1, '3');
|
||||
|
||||
outer.innerHTML = '123<br>456';
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.lastChild,
|
||||
2);
|
||||
removeHelper(6, range, outer, 2, '16');
|
||||
|
||||
outer.innerHTML = '123<br>456';
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 0, outer.lastChild,
|
||||
2);
|
||||
removeHelper(7, range, outer, 1, '6');
|
||||
|
||||
outer.innerHTML = '<div></div>';
|
||||
range = goog.dom.Range.createFromNodeContents(outer.firstChild);
|
||||
removeHelper(8, range, outer, 1, '<div></div>');
|
||||
} finally {
|
||||
// Restore the original text state for repeated runs.
|
||||
goog.dom.setTextContent(outer, originalText);
|
||||
}
|
||||
|
||||
// TODO(robbyw): Fix the following edge cases:
|
||||
// * Selecting contents of a node containing multiply empty divs
|
||||
// * Selecting via createFromNodes(x, 0, x, x.childNodes.length)
|
||||
// * Consistent handling of nodeContents(<div><div></div></div>).remove
|
||||
}
|
||||
|
||||
function removeHelper(testNumber, range, outer, expectedChildCount,
|
||||
expectedContent) {
|
||||
range.removeContents();
|
||||
assertTrue(testNumber + ': Removed range should now be collapsed',
|
||||
range.isCollapsed());
|
||||
assertEquals(testNumber + ': Removed range content should be ""', '',
|
||||
range.getText());
|
||||
assertEquals(testNumber + ': Outer div should contain correct text',
|
||||
expectedContent, outer.innerHTML.toLowerCase());
|
||||
assertEquals(testNumber + ': Outer div should have ' + expectedChildCount +
|
||||
' children now', expectedChildCount, outer.childNodes.length);
|
||||
assertNotNull(testNumber + ': Empty node should still exist',
|
||||
goog.dom.getElement('empty'));
|
||||
}
|
||||
|
||||
function testSurroundContents() {
|
||||
var outer = goog.dom.getElement('surroundTest');
|
||||
outer.innerHTML = '---Text that<br>will be surrounded---';
|
||||
var range = goog.dom.Range.createFromNodes(outer.firstChild, 3,
|
||||
outer.lastChild, outer.lastChild.nodeValue.length - 3);
|
||||
|
||||
var div = goog.dom.createDom(goog.dom.TagName.DIV, {'style': 'color: red'});
|
||||
var output = range.surroundContents(div);
|
||||
|
||||
assertEquals('Outer element should contain new element', outer,
|
||||
output.parentNode);
|
||||
assertFalse('New element should have no id', !!output.id);
|
||||
assertEquals('New element should be red', 'red', output.style.color);
|
||||
assertEquals('Outer element should have three children', 3,
|
||||
outer.childNodes.length);
|
||||
assertEquals('New element should have three children', 3,
|
||||
output.childNodes.length);
|
||||
|
||||
// TODO(robbyw): Ensure the range stays in a reasonable state.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given two offsets into the 'foobar' node, make sure that inserting
|
||||
* nodes at those offsets doesn't change a selection of 'oba'.
|
||||
* @bug 1480638
|
||||
*/
|
||||
function assertSurroundDoesntChangeSelectionWithOffsets(
|
||||
offset1, offset2, expectedHtml) {
|
||||
var div = goog.dom.getElement('bug1480638');
|
||||
div.innerHTML = 'foobar';
|
||||
var rangeToSelect = goog.dom.Range.createFromNodes(
|
||||
div.firstChild, 2, div.firstChild, 5);
|
||||
rangeToSelect.select();
|
||||
|
||||
var rangeToSurround = goog.dom.Range.createFromNodes(
|
||||
div.firstChild, offset1, div.firstChild, offset2);
|
||||
rangeToSurround.surroundWithNodes(goog.dom.createDom('span'),
|
||||
goog.dom.createDom('span'));
|
||||
|
||||
// Make sure that the selection didn't change.
|
||||
assertHTMLEquals('Selection must not change when contents are surrounded.',
|
||||
expectedHtml, goog.dom.Range.createFromWindow().getHtmlFragment());
|
||||
}
|
||||
|
||||
function testSurroundWithNodesDoesntChangeSelection1() {
|
||||
assertSurroundDoesntChangeSelectionWithOffsets(3, 4,
|
||||
'o<span></span>b<span></span>a');
|
||||
}
|
||||
|
||||
function testSurroundWithNodesDoesntChangeSelection2() {
|
||||
assertSurroundDoesntChangeSelectionWithOffsets(3, 6,
|
||||
'o<span></span>ba');
|
||||
}
|
||||
|
||||
function testSurroundWithNodesDoesntChangeSelection3() {
|
||||
assertSurroundDoesntChangeSelectionWithOffsets(1, 3,
|
||||
'o<span></span>ba');
|
||||
}
|
||||
|
||||
function testSurroundWithNodesDoesntChangeSelection4() {
|
||||
assertSurroundDoesntChangeSelectionWithOffsets(1, 6,
|
||||
'oba');
|
||||
}
|
||||
|
||||
function testInsertNode() {
|
||||
var outer = goog.dom.getElement('insertTest');
|
||||
outer.innerHTML = 'ACD';
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(outer.firstChild, 1,
|
||||
outer.firstChild, 2);
|
||||
range.insertNode(goog.dom.createTextNode('B'), true);
|
||||
assertEquals('Element should have correct innerHTML', 'ABCD',
|
||||
outer.innerHTML);
|
||||
|
||||
outer.innerHTML = '12';
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 0,
|
||||
outer.firstChild, 1);
|
||||
var br = range.insertNode(goog.dom.createDom(goog.dom.TagName.BR), false);
|
||||
assertEquals('New element should have correct innerHTML', '1<br>2',
|
||||
outer.innerHTML.toLowerCase());
|
||||
assertEquals('BR should be in outer', outer, br.parentNode);
|
||||
}
|
||||
|
||||
function testReplaceContentsWithNode() {
|
||||
var outer = goog.dom.getElement('insertTest');
|
||||
outer.innerHTML = 'AXC';
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(outer.firstChild, 1,
|
||||
outer.firstChild, 2);
|
||||
range.replaceContentsWithNode(goog.dom.createTextNode('B'));
|
||||
assertEquals('Element should have correct innerHTML', 'ABC',
|
||||
outer.innerHTML);
|
||||
|
||||
outer.innerHTML = 'ABC';
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 3,
|
||||
outer.firstChild, 3);
|
||||
range.replaceContentsWithNode(goog.dom.createTextNode('D'));
|
||||
assertEquals(
|
||||
'Element should have correct innerHTML after collapsed replace',
|
||||
'ABCD', outer.innerHTML);
|
||||
|
||||
outer.innerHTML = 'AX<b>X</b>XC';
|
||||
range = goog.dom.Range.createFromNodes(outer.firstChild, 1,
|
||||
outer.lastChild, 1);
|
||||
range.replaceContentsWithNode(goog.dom.createTextNode('B'));
|
||||
goog.testing.dom.assertHtmlContentsMatch('ABC', outer);
|
||||
}
|
||||
|
||||
function testSurroundWithNodes() {
|
||||
var outer = goog.dom.getElement('insertTest');
|
||||
outer.innerHTML = 'ACE';
|
||||
var range = goog.dom.Range.createFromNodes(outer.firstChild, 1,
|
||||
outer.firstChild, 2);
|
||||
|
||||
range.surroundWithNodes(goog.dom.createTextNode('B'),
|
||||
goog.dom.createTextNode('D'));
|
||||
|
||||
assertEquals('New element should have correct innerHTML', 'ABCDE',
|
||||
outer.innerHTML);
|
||||
}
|
||||
|
||||
function testIsRangeInDocument() {
|
||||
var outer = goog.dom.getElement('insertTest');
|
||||
outer.innerHTML = '<br>ABC';
|
||||
var range = goog.dom.Range.createCaret(outer.lastChild, 1);
|
||||
|
||||
assertEquals('Should get correct start element', 'ABC',
|
||||
range.getStartNode().nodeValue);
|
||||
assertTrue('Should be considered in document', range.isRangeInDocument());
|
||||
|
||||
outer.innerHTML = 'DEF';
|
||||
|
||||
assertFalse('Should be marked as out of document',
|
||||
range.isRangeInDocument());
|
||||
}
|
||||
|
||||
function testRemovedNode() {
|
||||
var node = goog.dom.getElement('removeNodeTest');
|
||||
var range = goog.dom.browserrange.createRangeFromNodeContents(node);
|
||||
range.select();
|
||||
goog.dom.removeNode(node);
|
||||
|
||||
var newRange = goog.dom.Range.createFromWindow(window);
|
||||
|
||||
// In Chrome 14 and below (<= Webkit 535.1), newRange will be null.
|
||||
// In Chrome 16 and above (>= Webkit 535.7), newRange will be collapsed
|
||||
// like on other browsers.
|
||||
// We didn't bother testing in between.
|
||||
if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('535.7')) {
|
||||
assertNull('Webkit supports rangeCount == 0', newRange);
|
||||
} else {
|
||||
assertTrue('The other browsers will just have an empty range.',
|
||||
newRange.isCollapsed());
|
||||
}
|
||||
}
|
||||
|
||||
function testReversedRange() {
|
||||
goog.dom.Range.createFromNodes(goog.dom.getElement('test2'), 0,
|
||||
goog.dom.getElement('test1'), 0).select();
|
||||
|
||||
var range = goog.dom.Range.createFromWindow(window);
|
||||
assertTrue('Range should be reversed',
|
||||
goog.userAgent.IE || range.isReversed());
|
||||
}
|
||||
|
||||
function testUnreversedRange() {
|
||||
goog.dom.Range.createFromNodes(goog.dom.getElement('test1'), 0,
|
||||
goog.dom.getElement('test2'), 0).select();
|
||||
|
||||
var range = goog.dom.Range.createFromWindow(window);
|
||||
assertFalse('Range should not be reversed', range.isReversed());
|
||||
}
|
||||
|
||||
function testReversedThenUnreversedRange() {
|
||||
// This tests a workaround for a webkit bug where webkit caches selections
|
||||
// incorrectly.
|
||||
goog.dom.Range.createFromNodes(goog.dom.getElement('test2'), 0,
|
||||
goog.dom.getElement('test1'), 0).select();
|
||||
goog.dom.Range.createFromNodes(goog.dom.getElement('test1'), 0,
|
||||
goog.dom.getElement('test2'), 0).select();
|
||||
|
||||
var range = goog.dom.Range.createFromWindow(window);
|
||||
assertFalse('Range should not be reversed', range.isReversed());
|
||||
}
|
||||
|
||||
function testHasAndClearSelection() {
|
||||
goog.dom.Range.createFromNodeContents(
|
||||
goog.dom.getElement('test1')).select();
|
||||
|
||||
assertTrue('Selection should exist', goog.dom.Range.hasSelection());
|
||||
|
||||
goog.dom.Range.clearSelection();
|
||||
|
||||
assertFalse('Selection should not exist', goog.dom.Range.hasSelection());
|
||||
}
|
||||
|
||||
function assertForward(string, startNode, startOffset, endNode, endOffset) {
|
||||
var root = goog.dom.getElement('test2');
|
||||
var originalInnerHtml = root.innerHTML;
|
||||
|
||||
assertFalse(string, goog.dom.Range.isReversed(startNode, startOffset,
|
||||
endNode, endOffset));
|
||||
assertTrue(string, goog.dom.Range.isReversed(endNode, endOffset,
|
||||
startNode, startOffset));
|
||||
assertEquals('Contents should be unaffected after: ' + string,
|
||||
root.innerHTML, originalInnerHtml);
|
||||
}
|
||||
|
||||
function testIsReversed() {
|
||||
var root = goog.dom.getElement('test2');
|
||||
var text1 = root.firstChild; // Text content: 'abc'.
|
||||
var br = root.childNodes[1];
|
||||
var text2 = root.lastChild; // Text content: 'def'.
|
||||
|
||||
assertFalse('Same element position gives false', goog.dom.Range.isReversed(
|
||||
root, 0, root, 0));
|
||||
assertFalse('Same text position gives false', goog.dom.Range.isReversed(
|
||||
text1, 0, text2, 0));
|
||||
assertForward('Element offsets should compare against each other',
|
||||
root, 0, root, 2);
|
||||
assertForward('Text node offsets should compare against each other',
|
||||
text1, 0, text2, 2);
|
||||
assertForward('Text nodes should compare correctly',
|
||||
text1, 0, text2, 0);
|
||||
assertForward('Text nodes should compare to later elements',
|
||||
text1, 0, br, 0);
|
||||
assertForward('Text nodes should compare to earlier elements',
|
||||
br, 0, text2, 0);
|
||||
assertForward('Parent is before element child', root, 0, br, 0);
|
||||
assertForward('Parent is before text child', root, 0, text1, 0);
|
||||
assertFalse('Equivalent position gives false', goog.dom.Range.isReversed(
|
||||
root, 0, text1, 0));
|
||||
assertFalse('Equivalent position gives false', goog.dom.Range.isReversed(
|
||||
root, 1, br, 0));
|
||||
assertForward('End of element is after children', text1, 0, root, 3);
|
||||
assertForward('End of element is after children', br, 0, root, 3);
|
||||
assertForward('End of element is after children', text2, 0, root, 3);
|
||||
assertForward('End of element is after end of last child',
|
||||
text2, 3, root, 3);
|
||||
}
|
||||
|
||||
function testSelectAroundSpaces() {
|
||||
// set the selection
|
||||
var textNode = goog.dom.getElement('textWithSpaces').firstChild;
|
||||
goog.dom.TextRange.createFromNodes(
|
||||
textNode, 5, textNode, 12).select();
|
||||
|
||||
// get the selection and check that it matches what we set it to
|
||||
var range = goog.dom.Range.createFromWindow();
|
||||
assertEquals(' world ', range.getText());
|
||||
assertEquals(5, range.getStartOffset());
|
||||
assertEquals(12, range.getEndOffset());
|
||||
assertEquals(textNode, range.getContainer());
|
||||
|
||||
// Check the contents again, because there used to be a bug where
|
||||
// it changed after calling getContainer().
|
||||
assertEquals(' world ', range.getText());
|
||||
}
|
||||
|
||||
function testSelectInsideSpaces() {
|
||||
// set the selection
|
||||
var textNode = goog.dom.getElement('textWithSpaces').firstChild;
|
||||
goog.dom.TextRange.createFromNodes(
|
||||
textNode, 6, textNode, 11).select();
|
||||
|
||||
// get the selection and check that it matches what we set it to
|
||||
var range = goog.dom.Range.createFromWindow();
|
||||
assertEquals('world', range.getText());
|
||||
assertEquals(6, range.getStartOffset());
|
||||
assertEquals(11, range.getEndOffset());
|
||||
assertEquals(textNode, range.getContainer());
|
||||
|
||||
// Check the contents again, because there used to be a bug where
|
||||
// it changed after calling getContainer().
|
||||
assertEquals('world', range.getText());
|
||||
}
|
||||
|
||||
function testRangeBeforeBreak() {
|
||||
var container = goog.dom.getElement('rangeAroundBreaks');
|
||||
var text = container.firstChild;
|
||||
var offset = text.length;
|
||||
assertEquals(4, offset);
|
||||
|
||||
var br = container.childNodes[1];
|
||||
var caret = goog.dom.Range.createCaret(text, offset);
|
||||
caret.select();
|
||||
assertEquals(offset, caret.getStartOffset());
|
||||
|
||||
var range = goog.dom.Range.createFromWindow();
|
||||
assertFalse('Should not contain whole <br>',
|
||||
range.containsNode(br, false));
|
||||
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
|
||||
assertTrue('Range over <br> is adjacent to the immediate range before it',
|
||||
range.containsNode(br, true));
|
||||
} else {
|
||||
assertFalse('Should not contain partial <br>',
|
||||
range.containsNode(br, true));
|
||||
}
|
||||
|
||||
assertEquals(offset, range.getStartOffset());
|
||||
assertEquals(text, range.getStartNode());
|
||||
}
|
||||
|
||||
function testRangeAfterBreak() {
|
||||
var container = goog.dom.getElement('rangeAroundBreaks');
|
||||
var br = container.childNodes[1];
|
||||
var caret = goog.dom.Range.createCaret(container.lastChild, 0);
|
||||
caret.select();
|
||||
assertEquals(0, caret.getStartOffset());
|
||||
|
||||
var range = goog.dom.Range.createFromWindow();
|
||||
assertFalse('Should not contain whole <br>',
|
||||
range.containsNode(br, false));
|
||||
var isSafari3 =
|
||||
goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('528');
|
||||
|
||||
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) ||
|
||||
isSafari3) {
|
||||
assertTrue('Range over <br> is adjacent to the immediate range after it',
|
||||
range.containsNode(br, true));
|
||||
} else {
|
||||
assertFalse('Should not contain partial <br>',
|
||||
range.containsNode(br, true));
|
||||
}
|
||||
|
||||
if (isSafari3) {
|
||||
assertEquals(2, range.getStartOffset());
|
||||
assertEquals(container, range.getStartNode());
|
||||
} else {
|
||||
assertEquals(0, range.getStartOffset());
|
||||
assertEquals(container.lastChild, range.getStartNode());
|
||||
}
|
||||
}
|
||||
|
||||
function testRangeAtBreakAtStart() {
|
||||
var container = goog.dom.getElement('breaksAroundNode');
|
||||
var br = container.firstChild;
|
||||
var caret = goog.dom.Range.createCaret(container.firstChild, 0);
|
||||
caret.select();
|
||||
assertEquals(0, caret.getStartOffset());
|
||||
|
||||
var range = goog.dom.Range.createFromWindow();
|
||||
assertTrue('Range over <br> is adjacent to the immediate range before it',
|
||||
range.containsNode(br, true));
|
||||
assertFalse('Should not contain whole <br>',
|
||||
range.containsNode(br, false));
|
||||
|
||||
assertRangeEquals(container, 0, container, 0, range);
|
||||
}
|
||||
|
||||
function testFocusedElementDisappears() {
|
||||
// This reproduces a failure case specific to Gecko, where an element is
|
||||
// created, contentEditable is set, is focused, and removed. After that
|
||||
// happens, calling selection.collapse fails.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=773137
|
||||
var disappearingElement = goog.dom.createDom('div');
|
||||
document.body.appendChild(disappearingElement);
|
||||
disappearingElement.contentEditable = true;
|
||||
disappearingElement.focus();
|
||||
document.body.removeChild(disappearingElement);
|
||||
var container = goog.dom.getElement('empty');
|
||||
var caret = goog.dom.Range.createCaret(container, 0);
|
||||
// This should not throw.
|
||||
caret.select();
|
||||
assertEquals(0, caret.getStartOffset());
|
||||
}
|
||||
|
||||
function assertNodeEquals(expected, actual) {
|
||||
assertEquals(
|
||||
'Expected: ' + goog.testing.dom.exposeNode(expected) +
|
||||
'\nActual: ' + goog.testing.dom.exposeNode(actual),
|
||||
expected, actual);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 Simple struct for endpoints of a range.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.RangeEndpoint');
|
||||
|
||||
|
||||
/**
|
||||
* Constants for selection endpoints.
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.dom.RangeEndpoint = {
|
||||
START: 1,
|
||||
END: 0
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2013 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 Type-safe wrappers for unsafe DOM APIs.
|
||||
*
|
||||
* This file provides type-safe wrappers for DOM APIs that can result in
|
||||
* cross-site scripting (XSS) vulnerabilities, if the API is supplied with
|
||||
* untrusted (attacker-controlled) input. Instead of plain strings, the type
|
||||
* safe wrappers consume values of types from the goog.html package whose
|
||||
* contract promises that values are safe to use in the corresponding context.
|
||||
*
|
||||
* Hence, a program that exclusively uses the wrappers in this file (i.e., whose
|
||||
* only reference to security-sensitive raw DOM APIs are in this file) is
|
||||
* guaranteed to be free of XSS due to incorrect use of such DOM APIs (modulo
|
||||
* correctness of code that produces values of the respective goog.html types,
|
||||
* and absent code that violates type safety).
|
||||
*
|
||||
* For example, assigning to an element's .innerHTML property a string that is
|
||||
* derived (even partially) from untrusted input typically results in an XSS
|
||||
* vulnerability. The type-safe wrapper goog.html.setInnerHtml consumes a value
|
||||
* of type goog.html.SafeHtml, whose contract states that using its values in a
|
||||
* HTML context will not result in XSS. Hence a program that is free of direct
|
||||
* assignments to any element's innerHTML property (with the exception of the
|
||||
* assignment to .innerHTML in this file) is guaranteed to be free of XSS due to
|
||||
* assignment of untrusted strings to the innerHTML property.
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.safe');
|
||||
|
||||
goog.require('goog.html.SafeHtml');
|
||||
goog.require('goog.html.SafeUrl');
|
||||
|
||||
|
||||
/**
|
||||
* Assigns known-safe HTML to an element's innerHTML property.
|
||||
* @param {!Element} elem The element whose innerHTML is to be assigned to.
|
||||
* @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
|
||||
*/
|
||||
goog.dom.safe.setInnerHtml = function(elem, html) {
|
||||
elem.innerHTML = goog.html.SafeHtml.unwrap(html);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Assigns known-safe HTML to an element's outerHTML property.
|
||||
* @param {!Element} elem The element whose outerHTML is to be assigned to.
|
||||
* @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
|
||||
*/
|
||||
goog.dom.safe.setOuterHtml = function(elem, html) {
|
||||
elem.outerHTML = goog.html.SafeHtml.unwrap(html);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Writes known-safe HTML to a document.
|
||||
* @param {!Document} doc The document to be written to.
|
||||
* @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
|
||||
*/
|
||||
goog.dom.safe.documentWrite = function(doc, html) {
|
||||
doc.write(goog.html.SafeHtml.unwrap(html));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Safely assigns a URL to an anchor element's href property.
|
||||
*
|
||||
* If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
|
||||
* anchor's href property. If url is of type string however, it is first
|
||||
* sanitized using goog.html.SafeUrl.sanitize.
|
||||
*
|
||||
* Example usage:
|
||||
* goog.dom.safe.setAnchorHref(anchorEl, url);
|
||||
* which is a safe alternative to
|
||||
* anchorEl.href = url;
|
||||
* The latter can result in XSS vulnerabilities if url is a
|
||||
* user-/attacker-controlled value.
|
||||
*
|
||||
* @param {!HTMLAnchorElement} anchor The anchor element whose href property
|
||||
* is to be assigned to.
|
||||
* @param {string|!goog.html.SafeUrl} url The URL to assign.
|
||||
* @see goog.html.SafeUrl#sanitize
|
||||
*/
|
||||
goog.dom.safe.setAnchorHref = function(anchor, url) {
|
||||
/** @type {!goog.html.SafeUrl} */
|
||||
var safeUrl;
|
||||
if (url instanceof goog.html.SafeUrl) {
|
||||
safeUrl = url;
|
||||
} else {
|
||||
safeUrl = goog.html.SafeUrl.sanitize(url);
|
||||
}
|
||||
anchor.href = goog.html.SafeUrl.unwrap(safeUrl);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Safely assigns a URL to a Location object's href property.
|
||||
*
|
||||
* If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
|
||||
* loc's href property. If url is of type string however, it is first sanitized
|
||||
* using goog.html.SafeUrl.sanitize.
|
||||
*
|
||||
* Example usage:
|
||||
* goog.dom.safe.setLocationHref(document.location, redirectUrl);
|
||||
* which is a safe alternative to
|
||||
* document.location.href = redirectUrl;
|
||||
* The latter can result in XSS vulnerabilities if redirectUrl is a
|
||||
* user-/attacker-controlled value.
|
||||
*
|
||||
* @param {!Location} loc The Location object whose href property is to be
|
||||
* assigned to.
|
||||
* @param {string|!goog.html.SafeUrl} url The URL to assign.
|
||||
* @see goog.html.SafeUrl#sanitize
|
||||
*/
|
||||
goog.dom.safe.setLocationHref = function(loc, url) {
|
||||
/** @type {!goog.html.SafeUrl} */
|
||||
var safeUrl;
|
||||
if (url instanceof goog.html.SafeUrl) {
|
||||
safeUrl = url;
|
||||
} else {
|
||||
safeUrl = goog.html.SafeUrl.sanitize(url);
|
||||
}
|
||||
loc.href = goog.html.SafeUrl.unwrap(safeUrl);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2013 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.safe</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.safeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2013 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.safeTest');
|
||||
goog.setTestOnly('goog.dom.safeTest');
|
||||
|
||||
goog.require('goog.dom.safe');
|
||||
goog.require('goog.html.SafeUrl');
|
||||
goog.require('goog.html.testing');
|
||||
goog.require('goog.string.Const');
|
||||
goog.require('goog.testing.jsunit');
|
||||
|
||||
function testSetInnerHtml() {
|
||||
var mockElement = {
|
||||
'innerHTML': 'blarg'
|
||||
};
|
||||
var html = '<script>somethingTrusted();<' + '/script>';
|
||||
var safeHtml = goog.html.testing.newSafeHtmlForTest(html);
|
||||
goog.dom.safe.setInnerHtml(mockElement, safeHtml);
|
||||
assertEquals(html, mockElement.innerHTML);
|
||||
}
|
||||
|
||||
|
||||
function testDocumentWrite() {
|
||||
var mockDoc = {
|
||||
'html': null,
|
||||
'write': function(html) {
|
||||
this['html'] = html;
|
||||
}
|
||||
};
|
||||
var html = '<script>somethingTrusted();<' + '/script>';
|
||||
var safeHtml = goog.html.testing.newSafeHtmlForTest(html);
|
||||
goog.dom.safe.documentWrite(mockDoc, safeHtml);
|
||||
assertEquals(html, mockDoc.html);
|
||||
}
|
||||
|
||||
|
||||
function testSetLocationHref() {
|
||||
var mockLoc = {
|
||||
'href': 'blarg'
|
||||
};
|
||||
goog.dom.safe.setLocationHref(mockLoc, 'javascript:evil();');
|
||||
assertEquals('about:invalid#zClosurez', mockLoc.href);
|
||||
|
||||
mockLoc = {
|
||||
'href': 'blarg'
|
||||
};
|
||||
var safeUrl = goog.html.SafeUrl.fromConstant(
|
||||
goog.string.Const.from('javascript:trusted();'));
|
||||
goog.dom.safe.setLocationHref(mockLoc, safeUrl);
|
||||
assertEquals('javascript:trusted();', mockLoc.href);
|
||||
}
|
||||
|
||||
|
||||
function testSetAnchorHref() {
|
||||
var mockAnchor = {
|
||||
'href': 'blarg'
|
||||
};
|
||||
goog.dom.safe.setAnchorHref(mockAnchor, 'javascript:evil();');
|
||||
assertEquals('about:invalid#zClosurez', mockAnchor.href);
|
||||
|
||||
mockAnchor = {
|
||||
'href': 'blarg'
|
||||
};
|
||||
var safeUrl = goog.html.SafeUrl.fromConstant(
|
||||
goog.string.Const.from('javascript:trusted();'));
|
||||
goog.dom.safe.setAnchorHref(mockAnchor, safeUrl);
|
||||
assertEquals('javascript:trusted();', mockAnchor.href);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright 2008 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 An API for saving and restoring ranges as HTML carets.
|
||||
*
|
||||
* @author nicksantos@google.com (Nick Santos)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.SavedCaretRange');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.SavedRange');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A struct for holding context about saved selections.
|
||||
* This can be used to preserve the selection and restore while the DOM is
|
||||
* manipulated, or through an asynchronous call. Use goog.dom.Range factory
|
||||
* methods to obtain an {@see goog.dom.AbstractRange} instance, and use
|
||||
* {@see goog.dom.AbstractRange#saveUsingCarets} to obtain a SavedCaretRange.
|
||||
* For editor ranges under content-editable elements or design-mode iframes,
|
||||
* prefer using {@see goog.editor.range.saveUsingNormalizedCarets}.
|
||||
* @param {goog.dom.AbstractRange} range The range being saved.
|
||||
* @constructor
|
||||
* @extends {goog.dom.SavedRange}
|
||||
*/
|
||||
goog.dom.SavedCaretRange = function(range) {
|
||||
goog.dom.SavedRange.call(this);
|
||||
|
||||
/**
|
||||
* The DOM id of the caret at the start of the range.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.startCaretId_ = goog.string.createUniqueString();
|
||||
|
||||
/**
|
||||
* The DOM id of the caret at the end of the range.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.endCaretId_ = goog.string.createUniqueString();
|
||||
|
||||
/**
|
||||
* Whether the range is reversed (anchor at the end).
|
||||
* @private {boolean}
|
||||
*/
|
||||
this.reversed_ = range.isReversed();
|
||||
|
||||
/**
|
||||
* A DOM helper for storing the current document context.
|
||||
* @type {goog.dom.DomHelper}
|
||||
* @private
|
||||
*/
|
||||
this.dom_ = goog.dom.getDomHelper(range.getDocument());
|
||||
|
||||
range.surroundWithNodes(this.createCaret_(true), this.createCaret_(false));
|
||||
};
|
||||
goog.inherits(goog.dom.SavedCaretRange, goog.dom.SavedRange);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the range that this SavedCaretRage represents, without selecting it
|
||||
* or removing the carets from the DOM.
|
||||
* @return {goog.dom.AbstractRange?} An abstract range.
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.toAbstractRange = function() {
|
||||
var range = null;
|
||||
var startCaret = this.getCaret(true);
|
||||
var endCaret = this.getCaret(false);
|
||||
if (startCaret && endCaret) {
|
||||
/** @suppress {missingRequire} circular dependency */
|
||||
range = goog.dom.Range.createFromNodes(startCaret, 0, endCaret, 0);
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets carets.
|
||||
* @param {boolean} start If true, returns the start caret. Otherwise, get the
|
||||
* end caret.
|
||||
* @return {Element} The start or end caret in the given document.
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.getCaret = function(start) {
|
||||
return this.dom_.getElement(start ? this.startCaretId_ : this.endCaretId_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes the carets from the current restoration document.
|
||||
* @param {goog.dom.AbstractRange=} opt_range A range whose offsets have already
|
||||
* been adjusted for caret removal; it will be adjusted if it is also
|
||||
* affected by post-removal operations, such as text node normalization.
|
||||
* @return {goog.dom.AbstractRange|undefined} The adjusted range, if opt_range
|
||||
* was provided.
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.removeCarets = function(opt_range) {
|
||||
goog.dom.removeNode(this.getCaret(true));
|
||||
goog.dom.removeNode(this.getCaret(false));
|
||||
return opt_range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the document where the range will be restored.
|
||||
* @param {!Document} doc An HTML document.
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.setRestorationDocument = function(doc) {
|
||||
this.dom_.setDocument(doc);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reconstruct the selection from the given saved range. Removes carets after
|
||||
* restoring the selection. If restore does not dispose this saved range, it may
|
||||
* only be restored a second time if innerHTML or some other mechanism is used
|
||||
* to restore the carets to the dom.
|
||||
* @return {goog.dom.AbstractRange?} Restored selection.
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.restoreInternal = function() {
|
||||
var range = null;
|
||||
var anchorCaret = this.getCaret(!this.reversed_);
|
||||
var focusCaret = this.getCaret(this.reversed_);
|
||||
if (anchorCaret && focusCaret) {
|
||||
var anchorNode = anchorCaret.parentNode;
|
||||
var anchorOffset = goog.array.indexOf(anchorNode.childNodes, anchorCaret);
|
||||
var focusNode = focusCaret.parentNode;
|
||||
var focusOffset = goog.array.indexOf(focusNode.childNodes, focusCaret);
|
||||
if (focusNode == anchorNode) {
|
||||
// Compensate for the start caret being removed.
|
||||
if (this.reversed_) {
|
||||
anchorOffset--;
|
||||
} else {
|
||||
focusOffset--;
|
||||
}
|
||||
}
|
||||
/** @suppress {missingRequire} circular dependency */
|
||||
range = goog.dom.Range.createFromNodes(anchorNode, anchorOffset,
|
||||
focusNode, focusOffset);
|
||||
range = this.removeCarets(range);
|
||||
range.select();
|
||||
} else {
|
||||
// If only one caret was found, remove it.
|
||||
this.removeCarets();
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dispose the saved range and remove the carets from the DOM.
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.disposeInternal = function() {
|
||||
this.removeCarets();
|
||||
this.dom_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a caret element.
|
||||
* @param {boolean} start If true, creates the start caret. Otherwise,
|
||||
* creates the end caret.
|
||||
* @return {!Element} The new caret element.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.SavedCaretRange.prototype.createCaret_ = function(start) {
|
||||
return this.dom_.createDom(goog.dom.TagName.SPAN,
|
||||
{'id': start ? this.startCaretId_ : this.endCaretId_});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A regex that will match all saved range carets in a string.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
goog.dom.SavedCaretRange.CARET_REGEX = /<span\s+id="?goog_\d+"?><\/span>/ig;
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether two strings of html are equal, ignoring any saved carets.
|
||||
* Thus two strings of html whose only difference is the id of their saved
|
||||
* carets will be considered equal, since they represent html with the
|
||||
* same selection.
|
||||
* @param {string} str1 The first string.
|
||||
* @param {string} str2 The second string.
|
||||
* @return {boolean} Whether two strings of html are equal, ignoring any
|
||||
* saved carets.
|
||||
*/
|
||||
goog.dom.SavedCaretRange.htmlEqual = function(str1, str2) {
|
||||
return str1 == str2 ||
|
||||
str1.replace(goog.dom.SavedCaretRange.CARET_REGEX, '') ==
|
||||
str2.replace(goog.dom.SavedCaretRange.CARET_REGEX, '');
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 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.SavedCaretRange</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.SavedCaretRangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='caretRangeTest'>
|
||||
abc
|
||||
<div id='def'>def</div>
|
||||
ghi
|
||||
<div id='jkl'>jkl</div>
|
||||
mno
|
||||
<div id='pqr'>pqr</div>
|
||||
stu
|
||||
</div>
|
||||
|
||||
<div id='caretRangeTest-2'>
|
||||
abc
|
||||
<div id='def-2'>def</div>
|
||||
ghi
|
||||
<div id='jkl-2'>jkl</div>
|
||||
mno
|
||||
<div id='pqr-2'>pqr</div>
|
||||
stu
|
||||
</div>
|
||||
|
||||
<div id='caretRangeTest-3'>
|
||||
abc
|
||||
<div id='def-3'>def</div>
|
||||
ghi
|
||||
<div id='jkl-3'>jkl</div>
|
||||
mno
|
||||
<div id='pqr-3'>pqr</div>
|
||||
stu
|
||||
</div>
|
||||
|
||||
<div id='removeContentsTest'>
|
||||
abc
|
||||
<div id='def-4'>def</div>
|
||||
ghi
|
||||
<div id='jkl-4'>jkl</div>
|
||||
mno
|
||||
<div id='pqr-4'>pqr</div>
|
||||
stu
|
||||
</div>
|
||||
|
||||
<div id='reversedSavedCaretRange'>
|
||||
abc
|
||||
<div id='def-5'>def</div>
|
||||
ghi
|
||||
<div id='jkl-5'>jkl</div>
|
||||
mno
|
||||
<div id='pqr-5'>pqr</div>
|
||||
stu
|
||||
</div>
|
||||
|
||||
<div id='bug1480638'>foo<table><tr><td>bar</td></tr></table>baz</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2008 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.SavedCaretRangeTest');
|
||||
goog.setTestOnly('goog.dom.SavedCaretRangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.dom.SavedCaretRange');
|
||||
goog.require('goog.testing.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
function setUp() {
|
||||
document.body.normalize();
|
||||
}
|
||||
|
||||
|
||||
/** @bug 1480638 */
|
||||
function testSavedCaretRangeDoesntChangeSelection() {
|
||||
// NOTE(nicksantos): We cannot detect this bug programatically. The only
|
||||
// way to detect it is to run this test manually and look at the selection
|
||||
// when it ends.
|
||||
var div = goog.dom.getElement('bug1480638');
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
div.firstChild, 0, div.lastChild, 1);
|
||||
range.select();
|
||||
|
||||
// Observe visible selection. Then move to next line and see it change.
|
||||
// If the bug exists, it starts with "foo" selected and ends with
|
||||
// it not selected.
|
||||
//debugger;
|
||||
var saved = range.saveUsingCarets();
|
||||
}
|
||||
|
||||
function testSavedCaretRange() {
|
||||
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8)) {
|
||||
// testSavedCaretRange fails in IE7 unless the source files are loaded in a
|
||||
// certain order. Adding goog.require('goog.dom.classes') to dom.js or
|
||||
// goog.require('goog.array') to savedcaretrange_test.js after the
|
||||
// goog.require('goog.dom') line fixes the test, but it's better to not
|
||||
// rely on such hacks without understanding the reason of the failure.
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = goog.dom.getElement('caretRangeTest');
|
||||
var def = goog.dom.getElement('def');
|
||||
var jkl = goog.dom.getElement('jkl');
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
def.firstChild, 1, jkl.firstChild, 2);
|
||||
assertFalse(range.isReversed());
|
||||
range.select();
|
||||
|
||||
var saved = range.saveUsingCarets();
|
||||
assertHTMLEquals(
|
||||
'd<span id="' + saved.startCaretId_ + '"></span>ef', def.innerHTML);
|
||||
assertHTMLEquals(
|
||||
'jk<span id="' + saved.endCaretId_ + '"></span>l', jkl.innerHTML);
|
||||
|
||||
goog.testing.dom.assertRangeEquals(
|
||||
def.childNodes[1], 0, jkl.childNodes[1], 0,
|
||||
saved.toAbstractRange());
|
||||
|
||||
def = goog.dom.getElement('def');
|
||||
jkl = goog.dom.getElement('jkl');
|
||||
|
||||
var restoredRange = clearSelectionAndRestoreSaved(parent, saved);
|
||||
assertFalse(restoredRange.isReversed());
|
||||
goog.testing.dom.assertRangeEquals(def, 1, jkl, 1, restoredRange);
|
||||
|
||||
var selection = goog.dom.Range.createFromWindow(window);
|
||||
assertHTMLEquals('def', def.innerHTML);
|
||||
assertHTMLEquals('jkl', jkl.innerHTML);
|
||||
|
||||
// def and jkl now contain fragmented text nodes.
|
||||
if (goog.userAgent.WEBKIT ||
|
||||
(goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'))) {
|
||||
goog.testing.dom.assertRangeEquals(
|
||||
def.childNodes[1], 0, jkl.childNodes[0], 2, selection);
|
||||
} else if (goog.userAgent.OPERA) {
|
||||
goog.testing.dom.assertRangeEquals(
|
||||
def.childNodes[1], 0, jkl.childNodes[1], 0, selection);
|
||||
} else {
|
||||
goog.testing.dom.assertRangeEquals(
|
||||
def, 1, jkl, 1, selection);
|
||||
}
|
||||
}
|
||||
|
||||
function testReversedSavedCaretRange() {
|
||||
var parent = goog.dom.getElement('caretRangeTest');
|
||||
var def = goog.dom.getElement('def-5');
|
||||
var jkl = goog.dom.getElement('jkl-5');
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
jkl.firstChild, 1, def.firstChild, 2);
|
||||
assertTrue(range.isReversed());
|
||||
range.select();
|
||||
|
||||
var saved = range.saveUsingCarets();
|
||||
var restoredRange = clearSelectionAndRestoreSaved(parent, saved);
|
||||
assertTrue(restoredRange.isReversed());
|
||||
goog.testing.dom.assertRangeEquals(def, 1, jkl, 1, restoredRange);
|
||||
}
|
||||
|
||||
/*
|
||||
TODO(user): Look into why removeCarets test doesn't pass.
|
||||
function testRemoveCarets() {
|
||||
var def = goog.dom.getElement('def');
|
||||
var jkl = goog.dom.getElement('jkl');
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
def.firstChild, 1, jkl.firstChild, 2);
|
||||
range.select();
|
||||
|
||||
var saved = range.saveUsingCarets();
|
||||
assertHTMLEquals(
|
||||
"d<span id="" + saved.startCaretId_ + ""></span>ef", def.innerHTML);
|
||||
assertHTMLEquals(
|
||||
"jk<span id="" + saved.endCaretId_ + ""></span>l", jkl.innerHTML);
|
||||
|
||||
saved.removeCarets();
|
||||
assertHTMLEquals("def", def.innerHTML);
|
||||
assertHTMLEquals("jkl", jkl.innerHTML);
|
||||
|
||||
var selection = goog.dom.Range.createFromWindow(window);
|
||||
|
||||
assertEquals('Wrong start node', def.firstChild, selection.getStartNode());
|
||||
assertEquals('Wrong end node', jkl.firstChild, selection.getEndNode());
|
||||
assertEquals('Wrong start offset', 1, selection.getStartOffset());
|
||||
assertEquals('Wrong end offset', 2, selection.getEndOffset());
|
||||
}
|
||||
*/
|
||||
|
||||
function testRemoveContents() {
|
||||
var def = goog.dom.getElement('def-4');
|
||||
var jkl = goog.dom.getElement('jkl-4');
|
||||
|
||||
// Sanity check.
|
||||
var container = goog.dom.getElement('removeContentsTest');
|
||||
assertEquals(7, container.childNodes.length);
|
||||
assertEquals('def', def.innerHTML);
|
||||
assertEquals('jkl', jkl.innerHTML);
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
def.firstChild, 1, jkl.firstChild, 2);
|
||||
range.select();
|
||||
|
||||
var saved = range.saveUsingCarets();
|
||||
var restored = saved.restore();
|
||||
restored.removeContents();
|
||||
|
||||
assertEquals(6, container.childNodes.length);
|
||||
assertEquals('d', def.innerHTML);
|
||||
assertEquals('l', jkl.innerHTML);
|
||||
}
|
||||
|
||||
function testHtmlEqual() {
|
||||
var parent = goog.dom.getElement('caretRangeTest-2');
|
||||
var def = goog.dom.getElement('def-2');
|
||||
var jkl = goog.dom.getElement('jkl-2');
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
def.firstChild, 1, jkl.firstChild, 2);
|
||||
range.select();
|
||||
var saved = range.saveUsingCarets();
|
||||
var html1 = parent.innerHTML;
|
||||
saved.removeCarets();
|
||||
|
||||
var saved2 = range.saveUsingCarets();
|
||||
var html2 = parent.innerHTML;
|
||||
saved2.removeCarets();
|
||||
|
||||
assertNotEquals('Same selection with different saved caret range carets ' +
|
||||
'must have different html.', html1, html2);
|
||||
|
||||
assertTrue('Same selection with different saved caret range carets must ' +
|
||||
'be considered equal by htmlEqual',
|
||||
goog.dom.SavedCaretRange.htmlEqual(html1, html2));
|
||||
|
||||
saved.dispose();
|
||||
saved2.dispose();
|
||||
}
|
||||
|
||||
function testStartCaretIsAtEndOfParent() {
|
||||
var parent = goog.dom.getElement('caretRangeTest-3');
|
||||
var def = goog.dom.getElement('def-3');
|
||||
var jkl = goog.dom.getElement('jkl-3');
|
||||
|
||||
var range = goog.dom.Range.createFromNodes(
|
||||
def, 1, jkl, 1);
|
||||
range.select();
|
||||
var saved = range.saveUsingCarets();
|
||||
clearSelectionAndRestoreSaved(parent, saved);
|
||||
range = goog.dom.Range.createFromWindow();
|
||||
assertEquals('ghijkl', range.getText().replace(/\s/g, ''));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear the selection by re-parsing the DOM. Then restore the saved
|
||||
* selection.
|
||||
* @param {Node} parent The node containing the current selection.
|
||||
* @param {goog.dom.SavedRange} saved The saved range.
|
||||
* @return {goog.dom.AbstractRange} Restored range.
|
||||
*/
|
||||
function clearSelectionAndRestoreSaved(parent, saved) {
|
||||
goog.dom.Range.clearSelection();
|
||||
assertFalse(goog.dom.Range.hasSelection(window));
|
||||
var range = saved.restore();
|
||||
assertTrue(goog.dom.Range.hasSelection(window));
|
||||
return range;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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 A generic interface for saving and restoring ranges.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.SavedRange');
|
||||
|
||||
goog.require('goog.Disposable');
|
||||
goog.require('goog.log');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Abstract interface for a saved range.
|
||||
* @constructor
|
||||
* @extends {goog.Disposable}
|
||||
*/
|
||||
goog.dom.SavedRange = function() {
|
||||
goog.Disposable.call(this);
|
||||
};
|
||||
goog.inherits(goog.dom.SavedRange, goog.Disposable);
|
||||
|
||||
|
||||
/**
|
||||
* Logging object.
|
||||
* @type {goog.log.Logger}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.SavedRange.logger_ =
|
||||
goog.log.getLogger('goog.dom.SavedRange');
|
||||
|
||||
|
||||
/**
|
||||
* Restores the range and by default disposes of the saved copy. Take note:
|
||||
* this means the by default SavedRange objects are single use objects.
|
||||
* @param {boolean=} opt_stayAlive Whether this SavedRange should stay alive
|
||||
* (not be disposed) after restoring the range. Defaults to false (dispose).
|
||||
* @return {goog.dom.AbstractRange} The restored range.
|
||||
*/
|
||||
goog.dom.SavedRange.prototype.restore = function(opt_stayAlive) {
|
||||
if (this.isDisposed()) {
|
||||
goog.log.error(goog.dom.SavedRange.logger_,
|
||||
'Disposed SavedRange objects cannot be restored.');
|
||||
}
|
||||
|
||||
var range = this.restoreInternal();
|
||||
if (!opt_stayAlive) {
|
||||
this.dispose();
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Internal method to restore the saved range.
|
||||
* @return {goog.dom.AbstractRange} The restored range.
|
||||
*/
|
||||
goog.dom.SavedRange.prototype.restoreInternal = goog.abstractMethod;
|
||||
@@ -0,0 +1,20 @@
|
||||
<!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.SavedRange</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.SavedRangeTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test1">Text</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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.SavedRangeTest');
|
||||
goog.setTestOnly('goog.dom.SavedRangeTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
function testSaved() {
|
||||
var node = goog.dom.getElement('test1');
|
||||
var range = goog.dom.Range.createFromNodeContents(node);
|
||||
var savedRange = range.saveUsingDom();
|
||||
|
||||
range = savedRange.restore(true);
|
||||
assertEquals('Restored range should select "Text"', 'Text',
|
||||
range.getText());
|
||||
assertFalse('Restored range should not be reversed.', range.isReversed());
|
||||
assertFalse('Range should not have disposed itself.',
|
||||
savedRange.isDisposed());
|
||||
|
||||
goog.dom.Range.clearSelection();
|
||||
assertFalse(goog.dom.Range.hasSelection(window));
|
||||
|
||||
range = savedRange.restore();
|
||||
assertTrue('Range should have auto-disposed.', savedRange.isDisposed());
|
||||
assertEquals('Restored range should select "Text"', 'Text',
|
||||
range.getText());
|
||||
assertFalse('Restored range should not be reversed.', range.isReversed());
|
||||
}
|
||||
|
||||
function testReversedSave() {
|
||||
var node = goog.dom.getElement('test1').firstChild;
|
||||
var range = goog.dom.Range.createFromNodes(node, 4, node, 0);
|
||||
var savedRange = range.saveUsingDom();
|
||||
|
||||
range = savedRange.restore();
|
||||
assertEquals('Restored range should select "Text"', 'Text',
|
||||
range.getText());
|
||||
if (!goog.userAgent.IE) {
|
||||
assertTrue('Restored range should be reversed.', range.isReversed());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Utilities for working with selections in input boxes and text
|
||||
* areas.
|
||||
*
|
||||
* @author arv@google.com (Erik Arvidsson)
|
||||
* @see ../demos/dom_selection.html
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.selection');
|
||||
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Sets the place where the selection should start inside a textarea or a text
|
||||
* input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @param {number} pos The position to set the start of the selection at.
|
||||
*/
|
||||
goog.dom.selection.setStart = function(textfield, pos) {
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
textfield.selectionStart = pos;
|
||||
} else if (goog.userAgent.IE) {
|
||||
// destructuring assignment would have been sweet
|
||||
var tmp = goog.dom.selection.getRangeIe_(textfield);
|
||||
var range = tmp[0];
|
||||
var selectionRange = tmp[1];
|
||||
|
||||
if (range.inRange(selectionRange)) {
|
||||
pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
|
||||
|
||||
range.collapse(true);
|
||||
range.move('character', pos);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the place where the selection starts inside a textarea or a text
|
||||
* input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @return {number} The position where the selection starts or 0 if it was
|
||||
* unable to find the position or no selection exists. Note that we can't
|
||||
* reliably tell the difference between an element that has no selection and
|
||||
* one where it starts at 0.
|
||||
*/
|
||||
goog.dom.selection.getStart = function(textfield) {
|
||||
return goog.dom.selection.getEndPoints_(textfield, true)[0];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the start and end points of the selection within a textarea in IE.
|
||||
* IE treats newline characters as \r\n characters, and we need to check for
|
||||
* these characters at the edge of our selection, to ensure that we return the
|
||||
* right cursor position.
|
||||
* @param {TextRange} range Complete range object, e.g., "Hello\r\n".
|
||||
* @param {TextRange} selRange Selected range object.
|
||||
* @param {boolean} getOnlyStart Value indicating if only start
|
||||
* cursor position is to be returned. In IE, obtaining the end position
|
||||
* involves extra work, hence we have this parameter for calls which need
|
||||
* only start position.
|
||||
* @return {!Array<number>} An array with the start and end positions where the
|
||||
* selection starts and ends or [0,0] if it was unable to find the
|
||||
* positions or no selection exists. Note that we can't reliably tell the
|
||||
* difference between an element that has no selection and one where
|
||||
* it starts and ends at 0. If getOnlyStart was true, we return
|
||||
* -1 as end offset.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.selection.getEndPointsTextareaIe_ = function(
|
||||
range, selRange, getOnlyStart) {
|
||||
// Create a duplicate of the selected range object to perform our actions
|
||||
// against. Example of selectionRange = "" (assuming that the cursor is
|
||||
// just after the \r\n combination)
|
||||
var selectionRange = selRange.duplicate();
|
||||
|
||||
// Text before the selection start, e.g.,"Hello" (notice how range.text
|
||||
// excludes the \r\n sequence)
|
||||
var beforeSelectionText = range.text;
|
||||
// Text before the selection start, e.g., "Hello" (this will later include
|
||||
// the \r\n sequences also)
|
||||
var untrimmedBeforeSelectionText = beforeSelectionText;
|
||||
// Text within the selection , e.g. "" assuming that the cursor is just after
|
||||
// the \r\n combination.
|
||||
var selectionText = selectionRange.text;
|
||||
// Text within the selection, e.g., "" (this will later include the \r\n
|
||||
// sequences also)
|
||||
var untrimmedSelectionText = selectionText;
|
||||
|
||||
// Boolean indicating whether we are done dealing with the text before the
|
||||
// selection's beginning.
|
||||
var isRangeEndTrimmed = false;
|
||||
// Go over the range until it becomes a 0-lengthed range or until the range
|
||||
// text starts changing when we move the end back by one character.
|
||||
// If after moving the end back by one character, the text remains the same,
|
||||
// then we need to add a "\r\n" at the end to get the actual text.
|
||||
while (!isRangeEndTrimmed) {
|
||||
if (range.compareEndPoints('StartToEnd', range) == 0) {
|
||||
isRangeEndTrimmed = true;
|
||||
} else {
|
||||
range.moveEnd('character', -1);
|
||||
if (range.text == beforeSelectionText) {
|
||||
// If the start position of the cursor was after a \r\n string,
|
||||
// we would skip over it in one go with the moveEnd call, but
|
||||
// range.text will still show "Hello" (because of the IE range.text
|
||||
// bug) - this implies that we should add a \r\n to our
|
||||
// untrimmedBeforeSelectionText string.
|
||||
untrimmedBeforeSelectionText += '\r\n';
|
||||
} else {
|
||||
isRangeEndTrimmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getOnlyStart) {
|
||||
// We return -1 as end, since the caller is only interested in the start
|
||||
// value.
|
||||
return [untrimmedBeforeSelectionText.length, -1];
|
||||
}
|
||||
// Boolean indicating whether we are done dealing with the text inside the
|
||||
// selection.
|
||||
var isSelectionRangeEndTrimmed = false;
|
||||
// Go over the selected range until it becomes a 0-lengthed range or until
|
||||
// the range text starts changing when we move the end back by one character.
|
||||
// If after moving the end back by one character, the text remains the same,
|
||||
// then we need to add a "\r\n" at the end to get the actual text.
|
||||
while (!isSelectionRangeEndTrimmed) {
|
||||
if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
|
||||
isSelectionRangeEndTrimmed = true;
|
||||
} else {
|
||||
selectionRange.moveEnd('character', -1);
|
||||
if (selectionRange.text == selectionText) {
|
||||
// If the selection was not empty, and the end point of the selection
|
||||
// was just after a \r\n, we would have skipped it in one go with the
|
||||
// moveEnd call, and this implies that we should add a \r\n to the
|
||||
// untrimmedSelectionText string.
|
||||
untrimmedSelectionText += '\r\n';
|
||||
} else {
|
||||
isSelectionRangeEndTrimmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
untrimmedBeforeSelectionText.length,
|
||||
untrimmedBeforeSelectionText.length + untrimmedSelectionText.length];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the start and end points of the selection inside a textarea or a
|
||||
* text input.
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @return {!Array<number>} An array with the start and end positions where the
|
||||
* selection starts and ends or [0,0] if it was unable to find the
|
||||
* positions or no selection exists. Note that we can't reliably tell the
|
||||
* difference between an element that has no selection and one where
|
||||
* it starts and ends at 0.
|
||||
*/
|
||||
goog.dom.selection.getEndPoints = function(textfield) {
|
||||
return goog.dom.selection.getEndPoints_(textfield, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the start and end points of the selection inside a textarea or a
|
||||
* text input.
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @param {boolean} getOnlyStart Value indicating if only start
|
||||
* cursor position is to be returned. In IE, obtaining the end position
|
||||
* involves extra work, hence we have this parameter. In FF, there is not
|
||||
* much extra effort involved.
|
||||
* @return {!Array<number>} An array with the start and end positions where the
|
||||
* selection starts and ends or [0,0] if it was unable to find the
|
||||
* positions or no selection exists. Note that we can't reliably tell the
|
||||
* difference between an element that has no selection and one where
|
||||
* it starts and ends at 0. If getOnlyStart was true, we return
|
||||
* -1 as end offset.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.selection.getEndPoints_ = function(textfield, getOnlyStart) {
|
||||
var startPos = 0;
|
||||
var endPos = 0;
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
startPos = textfield.selectionStart;
|
||||
endPos = getOnlyStart ? -1 : textfield.selectionEnd;
|
||||
} else if (goog.userAgent.IE) {
|
||||
var tmp = goog.dom.selection.getRangeIe_(textfield);
|
||||
var range = tmp[0];
|
||||
var selectionRange = tmp[1];
|
||||
|
||||
if (range.inRange(selectionRange)) {
|
||||
range.setEndPoint('EndToStart', selectionRange);
|
||||
if (textfield.type == 'textarea') {
|
||||
return goog.dom.selection.getEndPointsTextareaIe_(
|
||||
range, selectionRange, getOnlyStart);
|
||||
}
|
||||
startPos = range.text.length;
|
||||
if (!getOnlyStart) {
|
||||
endPos = range.text.length + selectionRange.text.length;
|
||||
} else {
|
||||
endPos = -1; // caller did not ask for end position
|
||||
}
|
||||
}
|
||||
}
|
||||
return [startPos, endPos];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the place where the selection should end inside a text area or a text
|
||||
* input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @param {number} pos The position to end the selection at.
|
||||
*/
|
||||
goog.dom.selection.setEnd = function(textfield, pos) {
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
textfield.selectionEnd = pos;
|
||||
} else if (goog.userAgent.IE) {
|
||||
var tmp = goog.dom.selection.getRangeIe_(textfield);
|
||||
var range = tmp[0];
|
||||
var selectionRange = tmp[1];
|
||||
|
||||
if (range.inRange(selectionRange)) {
|
||||
// Both the current position and the start cursor position need
|
||||
// to be canonicalized to take care of possible \r\n miscounts.
|
||||
pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
|
||||
var startCursorPos = goog.dom.selection.canonicalizePositionIe_(
|
||||
textfield, goog.dom.selection.getStart(textfield));
|
||||
|
||||
selectionRange.collapse(true);
|
||||
selectionRange.moveEnd('character', pos - startCursorPos);
|
||||
selectionRange.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the place where the selection ends inside a textarea or a text input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @return {number} The position where the selection ends or 0 if it was
|
||||
* unable to find the position or no selection exists.
|
||||
*/
|
||||
goog.dom.selection.getEnd = function(textfield) {
|
||||
return goog.dom.selection.getEndPoints_(textfield, false)[1];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the cursor position within a textfield.
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @param {number} pos The position within the text field.
|
||||
*/
|
||||
goog.dom.selection.setCursorPosition = function(textfield, pos) {
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
// Mozilla directly supports this
|
||||
textfield.selectionStart = pos;
|
||||
textfield.selectionEnd = pos;
|
||||
|
||||
} else if (goog.userAgent.IE) {
|
||||
pos = goog.dom.selection.canonicalizePositionIe_(textfield, pos);
|
||||
|
||||
// IE has textranges. A textfield's textrange encompasses the
|
||||
// entire textfield's text by default
|
||||
var sel = textfield.createTextRange();
|
||||
|
||||
sel.collapse(true);
|
||||
sel.move('character', pos);
|
||||
sel.select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the selected text inside a textarea or a text input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @param {string} text The text to change the selection to.
|
||||
*/
|
||||
goog.dom.selection.setText = function(textfield, text) {
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
var value = textfield.value;
|
||||
var oldSelectionStart = textfield.selectionStart;
|
||||
var before = value.substr(0, oldSelectionStart);
|
||||
var after = value.substr(textfield.selectionEnd);
|
||||
textfield.value = before + text + after;
|
||||
textfield.selectionStart = oldSelectionStart;
|
||||
textfield.selectionEnd = oldSelectionStart + text.length;
|
||||
} else if (goog.userAgent.IE) {
|
||||
var tmp = goog.dom.selection.getRangeIe_(textfield);
|
||||
var range = tmp[0];
|
||||
var selectionRange = tmp[1];
|
||||
|
||||
if (!range.inRange(selectionRange)) {
|
||||
return;
|
||||
}
|
||||
// When we set the selection text the selection range is collapsed to the
|
||||
// end. We therefore duplicate the current selection so we know where it
|
||||
// started. Once we've set the selection text we move the start of the
|
||||
// selection range to the old start
|
||||
var range2 = selectionRange.duplicate();
|
||||
selectionRange.text = text;
|
||||
selectionRange.setEndPoint('StartToStart', range2);
|
||||
selectionRange.select();
|
||||
} else {
|
||||
throw Error('Cannot set the selection end');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the selected text inside a textarea or a text input
|
||||
* @param {Element} textfield A textarea or text input.
|
||||
* @return {string} The selected text.
|
||||
*/
|
||||
goog.dom.selection.getText = function(textfield) {
|
||||
if (goog.dom.selection.useSelectionProperties_(textfield)) {
|
||||
var s = textfield.value;
|
||||
return s.substring(textfield.selectionStart, textfield.selectionEnd);
|
||||
}
|
||||
|
||||
if (goog.userAgent.IE) {
|
||||
var tmp = goog.dom.selection.getRangeIe_(textfield);
|
||||
var range = tmp[0];
|
||||
var selectionRange = tmp[1];
|
||||
|
||||
if (!range.inRange(selectionRange)) {
|
||||
return '';
|
||||
} else if (textfield.type == 'textarea') {
|
||||
return goog.dom.selection.getSelectionRangeText_(selectionRange);
|
||||
}
|
||||
return selectionRange.text;
|
||||
}
|
||||
|
||||
throw Error('Cannot get the selection text');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the selected text within a textarea in IE.
|
||||
* IE treats newline characters as \r\n characters, and we need to check for
|
||||
* these characters at the edge of our selection, to ensure that we return the
|
||||
* right string.
|
||||
* @param {TextRange} selRange Selected range object.
|
||||
* @return {string} Selected text in the textarea.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.selection.getSelectionRangeText_ = function(selRange) {
|
||||
// Create a duplicate of the selected range object to perform our actions
|
||||
// against. Suppose the text in the textarea is "Hello\r\nWorld" and the
|
||||
// selection encompasses the "o\r\n" bit, initial selectionRange will be "o"
|
||||
// (assuming that the cursor is just after the \r\n combination)
|
||||
var selectionRange = selRange.duplicate();
|
||||
|
||||
// Text within the selection , e.g. "o" assuming that the cursor is just after
|
||||
// the \r\n combination.
|
||||
var selectionText = selectionRange.text;
|
||||
// Text within the selection, e.g., "o" (this will later include the \r\n
|
||||
// sequences also)
|
||||
var untrimmedSelectionText = selectionText;
|
||||
|
||||
// Boolean indicating whether we are done dealing with the text inside the
|
||||
// selection.
|
||||
var isSelectionRangeEndTrimmed = false;
|
||||
// Go over the selected range until it becomes a 0-lengthed range or until
|
||||
// the range text starts changing when we move the end back by one character.
|
||||
// If after moving the end back by one character, the text remains the same,
|
||||
// then we need to add a "\r\n" at the end to get the actual text.
|
||||
while (!isSelectionRangeEndTrimmed) {
|
||||
if (selectionRange.compareEndPoints('StartToEnd', selectionRange) == 0) {
|
||||
isSelectionRangeEndTrimmed = true;
|
||||
} else {
|
||||
selectionRange.moveEnd('character', -1);
|
||||
if (selectionRange.text == selectionText) {
|
||||
// If the selection was not empty, and the end point of the selection
|
||||
// was just after a \r\n, we would have skipped it in one go with the
|
||||
// moveEnd call, and this implies that we should add a \r\n to the
|
||||
// untrimmedSelectionText string.
|
||||
untrimmedSelectionText += '\r\n';
|
||||
} else {
|
||||
isSelectionRangeEndTrimmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return untrimmedSelectionText;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper function for returning the range for an object as well as the
|
||||
* selection range
|
||||
* @private
|
||||
* @param {Element} el The element to get the range for.
|
||||
* @return {!Array<TextRange>} Range of object and selection range in two
|
||||
* element array.
|
||||
*/
|
||||
goog.dom.selection.getRangeIe_ = function(el) {
|
||||
var doc = el.ownerDocument || el.document;
|
||||
|
||||
var selectionRange = doc.selection.createRange();
|
||||
// el.createTextRange() doesn't work on textareas
|
||||
var range;
|
||||
|
||||
if (el.type == 'textarea') {
|
||||
range = doc.body.createTextRange();
|
||||
range.moveToElementText(el);
|
||||
} else {
|
||||
range = el.createTextRange();
|
||||
}
|
||||
|
||||
return [range, selectionRange];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper function for canonicalizing a position inside a textfield in IE.
|
||||
* Deals with the issue that \r\n counts as 2 characters, but
|
||||
* move('character', n) passes over both characters in one move.
|
||||
* @private
|
||||
* @param {Element} textfield The text element.
|
||||
* @param {number} pos The position desired in that element.
|
||||
* @return {number} The canonicalized position that will work properly with
|
||||
* move('character', pos).
|
||||
*/
|
||||
goog.dom.selection.canonicalizePositionIe_ = function(textfield, pos) {
|
||||
if (textfield.type == 'textarea') {
|
||||
// We do this only for textarea because it is the only one which can
|
||||
// have a \r\n (input cannot have this).
|
||||
var value = textfield.value.substring(0, pos);
|
||||
pos = goog.string.canonicalizeNewlines(value).length;
|
||||
}
|
||||
return pos;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to determine whether it's okay to use
|
||||
* selectionStart/selectionEnd.
|
||||
*
|
||||
* @param {Element} el The element to check for.
|
||||
* @return {boolean} Whether it's okay to use the selectionStart and
|
||||
* selectionEnd properties on {@code el}.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.selection.useSelectionProperties_ = function(el) {
|
||||
try {
|
||||
return typeof el.selectionStart == 'number';
|
||||
} catch (e) {
|
||||
// Firefox throws an exception if you try to access selectionStart
|
||||
// on an element with display: none.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
<!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.selection</title>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom.selectionTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,343 @@
|
||||
// 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.selectionTest');
|
||||
goog.setTestOnly('goog.dom.selectionTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.selection');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var input;
|
||||
var hiddenInput;
|
||||
var textarea;
|
||||
var hiddenTextarea;
|
||||
|
||||
function setUp() {
|
||||
input = goog.dom.createDom('input', {type: 'text'});
|
||||
textarea = goog.dom.createDom('textarea');
|
||||
hiddenInput = goog.dom.createDom(
|
||||
'input', {type: 'text', style: 'display: none'});
|
||||
hiddenTextarea = goog.dom.createDom(
|
||||
'textarea', {style: 'display: none'});
|
||||
|
||||
document.body.appendChild(input);
|
||||
document.body.appendChild(textarea);
|
||||
document.body.appendChild(hiddenInput);
|
||||
document.body.appendChild(hiddenTextarea);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.dom.removeNode(input);
|
||||
goog.dom.removeNode(textarea);
|
||||
goog.dom.removeNode(hiddenInput);
|
||||
goog.dom.removeNode(hiddenTextarea);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests getStart routine in both input and textarea.
|
||||
*/
|
||||
function testGetStartInput() {
|
||||
getStartHelper(input, hiddenInput);
|
||||
}
|
||||
|
||||
function testGetStartTextarea() {
|
||||
getStartHelper(textarea, hiddenTextarea);
|
||||
}
|
||||
|
||||
function getStartHelper(field, hiddenField) {
|
||||
assertEquals(0, goog.dom.selection.getStart(field));
|
||||
assertEquals(0, goog.dom.selection.getStart(hiddenField));
|
||||
|
||||
field.focus();
|
||||
assertEquals(0, goog.dom.selection.getStart(field));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests the setText routine for both input and textarea
|
||||
* with a single line of text.
|
||||
*/
|
||||
function testSetTextInput() {
|
||||
setTextHelper(input);
|
||||
}
|
||||
|
||||
function testSetTextTextarea() {
|
||||
setTextHelper(textarea);
|
||||
}
|
||||
|
||||
function setTextHelper(field) {
|
||||
// Test one line string only
|
||||
select(field);
|
||||
assertEquals('', goog.dom.selection.getText(field));
|
||||
|
||||
goog.dom.selection.setText(field, 'Get Behind Me Satan');
|
||||
assertEquals('Get Behind Me Satan', goog.dom.selection.getText(field));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests the setText routine for textarea with multiple lines of text.
|
||||
*/
|
||||
function testSetTextMultipleLines() {
|
||||
select(textarea);
|
||||
assertEquals('', goog.dom.selection.getText(textarea));
|
||||
var isLegacyIE = goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');
|
||||
var message = isLegacyIE ?
|
||||
'Get Behind Me\r\nSatan' :
|
||||
'Get Behind Me\nSatan';
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
assertEquals(message, goog.dom.selection.getText(textarea));
|
||||
|
||||
// Select the text upto the point just after the \r\n combination
|
||||
// or \n in GECKO.
|
||||
var endOfNewline = isLegacyIE ? 15 : 14;
|
||||
var selectedMessage = message.substring(0, endOfNewline);
|
||||
goog.dom.selection.setStart(textarea, 0);
|
||||
goog.dom.selection.setEnd(textarea, endOfNewline);
|
||||
assertEquals(selectedMessage, goog.dom.selection.getText(textarea));
|
||||
|
||||
selectedMessage = isLegacyIE ? '\r\n' : '\n';
|
||||
goog.dom.selection.setStart(textarea, 13);
|
||||
goog.dom.selection.setEnd(textarea, endOfNewline);
|
||||
assertEquals(selectedMessage, goog.dom.selection.getText(textarea));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests the setCursor routine for both input and textarea.
|
||||
*/
|
||||
function testSetCursorInput() {
|
||||
setCursorHelper(input);
|
||||
}
|
||||
|
||||
function testSetCursorTextarea() {
|
||||
setCursorHelper(textarea);
|
||||
}
|
||||
|
||||
function setCursorHelper(field) {
|
||||
select(field);
|
||||
// try to set the cursor beyond the length of the content
|
||||
goog.dom.selection.setStart(field, 5);
|
||||
goog.dom.selection.setEnd(field, 15);
|
||||
assertEquals(0, goog.dom.selection.getStart(field));
|
||||
assertEquals(0, goog.dom.selection.getEnd(field));
|
||||
|
||||
select(field);
|
||||
var message = 'Get Behind Me Satan';
|
||||
goog.dom.selection.setText(field, message);
|
||||
goog.dom.selection.setStart(field, 5);
|
||||
goog.dom.selection.setEnd(field, message.length);
|
||||
assertEquals(5, goog.dom.selection.getStart(field));
|
||||
assertEquals(message.length, goog.dom.selection.getEnd(field));
|
||||
|
||||
// Set the end before the start, and see if getEnd returns the start
|
||||
// position itself.
|
||||
goog.dom.selection.setStart(field, 5);
|
||||
goog.dom.selection.setEnd(field, 3);
|
||||
assertEquals(3, goog.dom.selection.getEnd(field));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests the getText and setText routines acting on selected text in
|
||||
* both input and textarea.
|
||||
*/
|
||||
function testGetAndSetSelectedTextInput() {
|
||||
getAndSetSelectedTextHelper(input);
|
||||
}
|
||||
|
||||
function testGetAndSetSelectedTextTextarea() {
|
||||
getAndSetSelectedTextHelper(textarea);
|
||||
}
|
||||
|
||||
function getAndSetSelectedTextHelper(field) {
|
||||
select(field);
|
||||
goog.dom.selection.setText(field, 'Get Behind Me Satan');
|
||||
|
||||
// select 'Behind'
|
||||
goog.dom.selection.setStart(field, 4);
|
||||
goog.dom.selection.setEnd(field, 10);
|
||||
assertEquals('Behind', goog.dom.selection.getText(field));
|
||||
|
||||
goog.dom.selection.setText(field, 'In Front Of');
|
||||
goog.dom.selection.setStart(field, 0);
|
||||
goog.dom.selection.setEnd(field, 100);
|
||||
assertEquals('Get In Front Of Me Satan', goog.dom.selection.getText(field));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test setStart on hidden input and hidden textarea.
|
||||
*/
|
||||
function testSetCursorOnHiddenInput() {
|
||||
setCursorOnHiddenInputHelper(hiddenInput);
|
||||
}
|
||||
|
||||
function testSetCursorOnHiddenTextarea() {
|
||||
setCursorOnHiddenInputHelper(hiddenTextarea);
|
||||
}
|
||||
|
||||
function setCursorOnHiddenInputHelper(hiddenField) {
|
||||
goog.dom.selection.setStart(hiddenField, 0);
|
||||
assertEquals(0, goog.dom.selection.getStart(hiddenField));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test setStart, setEnd, getStart and getEnd in textarea with text
|
||||
* containing line breaks.
|
||||
*/
|
||||
function testSetAndGetCursorWithLineBreaks() {
|
||||
select(textarea);
|
||||
var isLegacyIE = goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');
|
||||
var newline = isLegacyIE ? '\r\n' : '\n';
|
||||
var message = 'Hello' + newline + 'World';
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
|
||||
// Test setEnd and getEnd, by setting the cursor somewhere after the
|
||||
// \r\n combination.
|
||||
goog.dom.selection.setEnd(textarea, 9);
|
||||
assertEquals(9, goog.dom.selection.getEnd(textarea));
|
||||
|
||||
// Test basic setStart and getStart
|
||||
goog.dom.selection.setStart(textarea, 10);
|
||||
assertEquals(10, goog.dom.selection.getStart(textarea));
|
||||
|
||||
// Test setEnd and getEnd, by setting the cursor exactly after the
|
||||
// \r\n combination in IE or after \n in GECKO.
|
||||
var endOfNewline = isLegacyIE ? 7 : 6;
|
||||
checkSetAndGetTextarea(endOfNewline, endOfNewline);
|
||||
|
||||
// Select a \r\n combination in IE or \n in GECKO and see if
|
||||
// getStart and getEnd work correctly.
|
||||
clearField(textarea);
|
||||
message = 'Hello' + newline + newline + 'World';
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
var startOfNewline = isLegacyIE ? 7 : 6;
|
||||
endOfNewline = isLegacyIE ? 9 : 7;
|
||||
checkSetAndGetTextarea(startOfNewline, endOfNewline);
|
||||
|
||||
// Select 2 \r\n combinations in IE or 2 \ns in GECKO and see if getStart
|
||||
// and getEnd work correctly.
|
||||
checkSetAndGetTextarea(5, endOfNewline);
|
||||
|
||||
// Position cursor b/w 2 \r\n combinations in IE or 2 \ns in GECKO and see
|
||||
// if getStart and getEnd work correctly.
|
||||
clearField(textarea);
|
||||
message = 'Hello' + newline + newline + newline + newline + 'World';
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
var middleOfNewlines = isLegacyIE ? 9 : 7;
|
||||
checkSetAndGetTextarea(middleOfNewlines, middleOfNewlines);
|
||||
|
||||
// Position cursor at end of a textarea which ends with \r\n in IE or \n in
|
||||
// GECKO.
|
||||
if (!goog.userAgent.IE || !goog.userAgent.isVersionOrHigher('11')) {
|
||||
// TODO(johnlenz): investigate why this fails in IE 11.
|
||||
clearField(textarea);
|
||||
message = 'Hello' + newline + newline;
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
var endOfTextarea = message.length;
|
||||
checkSetAndGetTextarea(endOfTextarea, endOfTextarea);
|
||||
}
|
||||
|
||||
// Position cursor at the end of the 2 starting \r\ns in IE or \ns in GECKO
|
||||
// within a textarea.
|
||||
clearField(textarea);
|
||||
message = newline + newline + 'World';
|
||||
goog.dom.selection.setText(textarea, message);
|
||||
var endOfTwoNewlines = isLegacyIE ? 4 : 2;
|
||||
checkSetAndGetTextarea(endOfTwoNewlines, endOfTwoNewlines);
|
||||
|
||||
// Position cursor at the end of the first \r\n in IE or \n in
|
||||
// GECKO within a textarea.
|
||||
endOfOneNewline = isLegacyIE ? 2 : 1;
|
||||
checkSetAndGetTextarea(endOfOneNewline, endOfOneNewline);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test to make sure there's no error when getting the range of an unselected
|
||||
* textarea. See bug 1274027.
|
||||
*/
|
||||
function testGetStartOnUnfocusedTextarea() {
|
||||
input.value = 'White Blood Cells';
|
||||
input.focus();
|
||||
goog.dom.selection.setCursorPosition(input, 5);
|
||||
|
||||
assertEquals('getStart on input should return where we put the cursor',
|
||||
5, goog.dom.selection.getStart(input));
|
||||
|
||||
assertEquals('getStart on unfocused textarea should succeed without error',
|
||||
0, goog.dom.selection.getStart(textarea));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test to make sure there's no error setting cursor position within a
|
||||
* textarea after a newline. This is problematic on IE because of the
|
||||
* '\r\n' vs '\n' issue.
|
||||
*/
|
||||
function testSetCursorPositionTextareaWithNewlines() {
|
||||
textarea.value = 'Hello\nWorld';
|
||||
textarea.focus();
|
||||
|
||||
// Set the selection point between 'W' and 'o'. Position is computed this
|
||||
// way instead of being hard-coded because it's different in IE due to \r\n
|
||||
// vs \n.
|
||||
goog.dom.selection.setCursorPosition(textarea, textarea.value.length - 4);
|
||||
|
||||
var isLegacyIE = goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');
|
||||
var linebreak = isLegacyIE ? '\r\n' : '\n';
|
||||
var expectedLeftString = 'Hello' + linebreak + 'W';
|
||||
|
||||
assertEquals('getStart on input should return after the newline',
|
||||
expectedLeftString.length, goog.dom.selection.getStart(textarea));
|
||||
assertEquals('getEnd on input should return after the newline',
|
||||
expectedLeftString.length, goog.dom.selection.getEnd(textarea));
|
||||
|
||||
goog.dom.selection.setEnd(textarea, textarea.value.length);
|
||||
assertEquals('orld', goog.dom.selection.getText(textarea));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to clear the textfield contents.
|
||||
*/
|
||||
function clearField(field) {
|
||||
field.value = '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to set the start and end and assert the getter values.
|
||||
*/
|
||||
function checkSetAndGetTextarea(start, end) {
|
||||
goog.dom.selection.setStart(textarea, start);
|
||||
goog.dom.selection.setEnd(textarea, end);
|
||||
assertEquals(start, goog.dom.selection.getStart(textarea));
|
||||
assertEquals(end, goog.dom.selection.getEnd(textarea));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to focus and select a field. In IE8, selected
|
||||
* fields need focus.
|
||||
*/
|
||||
function select(field) {
|
||||
field.focus();
|
||||
field.select();
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright 2008 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 Iterator subclass for DOM tree traversal.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.TagIterator');
|
||||
goog.provide('goog.dom.TagWalkType');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.iter.Iterator');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
|
||||
|
||||
/**
|
||||
* There are three types of token:
|
||||
* <ol>
|
||||
* <li>{@code START_TAG} - The beginning of a tag.
|
||||
* <li>{@code OTHER} - Any non-element node position.
|
||||
* <li>{@code END_TAG} - The end of a tag.
|
||||
* </ol>
|
||||
* Users of this enumeration can rely on {@code START_TAG + END_TAG = 0} and
|
||||
* that {@code OTHER = 0}.
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.dom.TagWalkType = {
|
||||
START_TAG: 1,
|
||||
OTHER: 0,
|
||||
END_TAG: -1
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A DOM tree traversal iterator.
|
||||
*
|
||||
* Starting with the given node, the iterator walks the DOM in order, reporting
|
||||
* events for the start and end of Elements, and the presence of text nodes. For
|
||||
* example:
|
||||
*
|
||||
* <pre>
|
||||
* <div>1<span>2</span>3</div>
|
||||
* </pre>
|
||||
*
|
||||
* Will return the following nodes:
|
||||
*
|
||||
* <code>[div, 1, span, 2, span, 3, div]</code>
|
||||
*
|
||||
* With the following states:
|
||||
*
|
||||
* <code>[START, OTHER, START, OTHER, END, OTHER, END]</code>
|
||||
*
|
||||
* And the following depths
|
||||
*
|
||||
* <code>[1, 1, 2, 2, 1, 1, 0]</code>
|
||||
*
|
||||
* Imagining <code>|</code> represents iterator position, the traversal stops at
|
||||
* each of the following locations:
|
||||
*
|
||||
* <pre>
|
||||
* <div>|1|<span>|2|</span>|3|</div>|
|
||||
* </pre>
|
||||
*
|
||||
* The iterator can also be used in reverse mode, which will return the nodes
|
||||
* and states in the opposite order. The depths will be slightly different
|
||||
* since, like in normal mode, the depth is computed *after* the given node.
|
||||
*
|
||||
* Lastly, it is possible to create an iterator that is unconstrained, meaning
|
||||
* that it will continue iterating until the end of the document instead of
|
||||
* until exiting the start node.
|
||||
*
|
||||
* @param {Node=} opt_node The start node. If unspecified or null, defaults to
|
||||
* an empty iterator.
|
||||
* @param {boolean=} opt_reversed Whether to traverse the tree in reverse.
|
||||
* @param {boolean=} opt_unconstrained Whether the iterator is not constrained
|
||||
* to the starting node and its children.
|
||||
* @param {goog.dom.TagWalkType?=} opt_tagType The type of the position.
|
||||
* Defaults to the start of the given node for forward iterators, and
|
||||
* the end of the node for reverse iterators.
|
||||
* @param {number=} opt_depth The starting tree depth.
|
||||
* @constructor
|
||||
* @extends {goog.iter.Iterator<Node>}
|
||||
*/
|
||||
goog.dom.TagIterator = function(opt_node, opt_reversed,
|
||||
opt_unconstrained, opt_tagType, opt_depth) {
|
||||
this.reversed = !!opt_reversed;
|
||||
if (opt_node) {
|
||||
this.setPosition(opt_node, opt_tagType);
|
||||
}
|
||||
this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
|
||||
if (this.reversed) {
|
||||
this.depth *= -1;
|
||||
}
|
||||
this.constrained = !opt_unconstrained;
|
||||
};
|
||||
goog.inherits(goog.dom.TagIterator, goog.iter.Iterator);
|
||||
|
||||
|
||||
/**
|
||||
* The node this position is located on.
|
||||
* @type {Node}
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.node = null;
|
||||
|
||||
|
||||
/**
|
||||
* The type of this position.
|
||||
* @type {goog.dom.TagWalkType}
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.tagType = goog.dom.TagWalkType.OTHER;
|
||||
|
||||
|
||||
/**
|
||||
* The tree depth of this position relative to where the iterator started. The
|
||||
* depth is considered to be the tree depth just past the current node, so if an
|
||||
* iterator is at position <pre>
|
||||
* <div>|</div>
|
||||
* </pre>
|
||||
* (i.e. the node is the div and the type is START_TAG) its depth will be 1.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.depth;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the node iterator is moving in reverse.
|
||||
* @type {boolean}
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.reversed;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the iterator is constrained to the starting node and its children.
|
||||
* @type {boolean}
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.constrained;
|
||||
|
||||
|
||||
/**
|
||||
* Whether iteration has started.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.started_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Set the position of the iterator. Overwrite the tree node and the position
|
||||
* type which can be one of the {@link goog.dom.TagWalkType} token types.
|
||||
* Only overwrites the tree depth when the parameter is specified.
|
||||
* @param {Node} node The node to set the position to.
|
||||
* @param {goog.dom.TagWalkType?=} opt_tagType The type of the position
|
||||
* Defaults to the start of the given node.
|
||||
* @param {number=} opt_depth The tree depth.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.setPosition = function(node,
|
||||
opt_tagType, opt_depth) {
|
||||
this.node = node;
|
||||
|
||||
if (node) {
|
||||
if (goog.isNumber(opt_tagType)) {
|
||||
this.tagType = opt_tagType;
|
||||
} else {
|
||||
// Auto-determine the proper type
|
||||
this.tagType = this.node.nodeType != goog.dom.NodeType.ELEMENT ?
|
||||
goog.dom.TagWalkType.OTHER :
|
||||
this.reversed ? goog.dom.TagWalkType.END_TAG :
|
||||
goog.dom.TagWalkType.START_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
if (goog.isNumber(opt_depth)) {
|
||||
this.depth = opt_depth;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replace this iterator's values with values from another. The two iterators
|
||||
* must be of the same type.
|
||||
* @param {goog.dom.TagIterator} other The iterator to copy.
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.copyFrom = function(other) {
|
||||
this.node = other.node;
|
||||
this.tagType = other.tagType;
|
||||
this.depth = other.depth;
|
||||
this.reversed = other.reversed;
|
||||
this.constrained = other.constrained;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!goog.dom.TagIterator} A copy of this iterator.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.clone = function() {
|
||||
return new goog.dom.TagIterator(this.node, this.reversed,
|
||||
!this.constrained, this.tagType, this.depth);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Skip the current tag.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.skipTag = function() {
|
||||
var check = this.reversed ? goog.dom.TagWalkType.END_TAG :
|
||||
goog.dom.TagWalkType.START_TAG;
|
||||
if (this.tagType == check) {
|
||||
this.tagType = /** @type {goog.dom.TagWalkType} */ (check * -1);
|
||||
this.depth += this.tagType * (this.reversed ? -1 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Restart the current tag.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.restartTag = function() {
|
||||
var check = this.reversed ? goog.dom.TagWalkType.START_TAG :
|
||||
goog.dom.TagWalkType.END_TAG;
|
||||
if (this.tagType == check) {
|
||||
this.tagType = /** @type {goog.dom.TagWalkType} */ (check * -1);
|
||||
this.depth += this.tagType * (this.reversed ? -1 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move to the next position in the DOM tree.
|
||||
* @return {Node} Returns the next node, or throws a goog.iter.StopIteration
|
||||
* exception if the end of the iterator's range has been reached.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.next = function() {
|
||||
var node;
|
||||
|
||||
if (this.started_) {
|
||||
if (!this.node || this.constrained && this.depth == 0) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
node = this.node;
|
||||
|
||||
var startType = this.reversed ? goog.dom.TagWalkType.END_TAG :
|
||||
goog.dom.TagWalkType.START_TAG;
|
||||
|
||||
if (this.tagType == startType) {
|
||||
// If we have entered the tag, test if there are any children to move to.
|
||||
var child = this.reversed ? node.lastChild : node.firstChild;
|
||||
if (child) {
|
||||
this.setPosition(child);
|
||||
} else {
|
||||
// If not, move on to exiting this tag.
|
||||
this.setPosition(node,
|
||||
/** @type {goog.dom.TagWalkType} */ (startType * -1));
|
||||
}
|
||||
} else {
|
||||
var sibling = this.reversed ? node.previousSibling : node.nextSibling;
|
||||
if (sibling) {
|
||||
// Try to move to the next node.
|
||||
this.setPosition(sibling);
|
||||
} else {
|
||||
// If no such node exists, exit our parent.
|
||||
this.setPosition(node.parentNode,
|
||||
/** @type {goog.dom.TagWalkType} */ (startType * -1));
|
||||
}
|
||||
}
|
||||
|
||||
this.depth += this.tagType * (this.reversed ? -1 : 1);
|
||||
} else {
|
||||
this.started_ = true;
|
||||
}
|
||||
|
||||
// Check the new position for being last, and return it if it's not.
|
||||
node = this.node;
|
||||
if (!this.node) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether next has ever been called on this iterator.
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.isStarted = function() {
|
||||
return this.started_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether this iterator's position is a start tag position.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.isStartTag = function() {
|
||||
return this.tagType == goog.dom.TagWalkType.START_TAG;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether this iterator's position is an end tag position.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.isEndTag = function() {
|
||||
return this.tagType == goog.dom.TagWalkType.END_TAG;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether this iterator's position is not at an element node.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.isNonElement = function() {
|
||||
return this.tagType == goog.dom.TagWalkType.OTHER;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test if two iterators are at the same position - i.e. if the node and tagType
|
||||
* is the same. This will still return true if the two iterators are moving in
|
||||
* opposite directions or have different constraints.
|
||||
* @param {goog.dom.TagIterator} other The iterator to compare to.
|
||||
* @return {boolean} Whether the two iterators are at the same position.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.equals = function(other) {
|
||||
// Nodes must be equal, and we must either have reached the end of our tree
|
||||
// or be at the same position.
|
||||
return other.node == this.node && (!this.node ||
|
||||
other.tagType == this.tagType);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replace the current node with the list of nodes. Reset the iterator so that
|
||||
* it visits the first of the nodes next.
|
||||
* @param {...Object} var_args A list of nodes to replace the current node with.
|
||||
* If the first argument is array-like, it will be used, otherwise all the
|
||||
* arguments are assumed to be nodes.
|
||||
*/
|
||||
goog.dom.TagIterator.prototype.splice = function(var_args) {
|
||||
// Reset the iterator so that it iterates over the first replacement node in
|
||||
// the arguments on the next iteration.
|
||||
var node = this.node;
|
||||
this.restartTag();
|
||||
this.reversed = !this.reversed;
|
||||
goog.dom.TagIterator.prototype.next.call(this);
|
||||
this.reversed = !this.reversed;
|
||||
|
||||
// Replace the node with the arguments.
|
||||
var arr = goog.isArrayLike(arguments[0]) ? arguments[0] : arguments;
|
||||
for (var i = arr.length - 1; i >= 0; i--) {
|
||||
goog.dom.insertSiblingAfter(arr[i], node);
|
||||
}
|
||||
goog.dom.removeNode(node);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user