Adding float-no-zero branch hosted build
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// 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 adding, removing and setting ARIA roles
|
||||
* as defined by W3C ARIA Working Draft:
|
||||
* http://www.w3.org/TR/2010/WD-wai-aria-20100916/
|
||||
* All modern browsers have some form of ARIA support, so no browser checks are
|
||||
* performed when adding ARIA to components.
|
||||
*
|
||||
*
|
||||
* @deprecated Use {@link goog.a11y.aria} instead.
|
||||
* This file will be removed on 1 Apr 2013.
|
||||
*
|
||||
*/
|
||||
goog.provide('goog.dom.a11y');
|
||||
goog.provide('goog.dom.a11y.Announcer');
|
||||
goog.provide('goog.dom.a11y.LivePriority');
|
||||
goog.provide('goog.dom.a11y.Role');
|
||||
goog.provide('goog.dom.a11y.State');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Announcer');
|
||||
goog.require('goog.a11y.aria.LivePriority');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of ARIA states and properties.
|
||||
* @enum {string}
|
||||
* @deprecated Use {@link goog.a11y.aria.State} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.State = goog.a11y.aria.State;
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of ARIA roles.
|
||||
* @enum {string}
|
||||
* @deprecated Use {@link goog.a11y.aria.Role} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.Role = goog.a11y.aria.Role;
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of ARIA state values for live regions.
|
||||
*
|
||||
* See http://www.w3.org/TR/wai-aria/states_and_properties#aria-live
|
||||
* for more information.
|
||||
* @enum {string}
|
||||
* @deprecated Use {@link goog.a11y.aria.LivePriority} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.LivePriority = goog.a11y.aria.LivePriority;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the role of an element.
|
||||
* @param {Element} element DOM node to set role of.
|
||||
* @param {goog.dom.a11y.Role|string} roleName role name(s).
|
||||
* @deprecated Use {@link goog.a11y.aria.setRole} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.setRole = function(element, roleName) {
|
||||
goog.a11y.aria.setRole(
|
||||
/** @type {!Element} */ (element),
|
||||
/** @type {!goog.dom.a11y.Role} */ (roleName));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets role of an element.
|
||||
* @param {Element} element DOM node to get role of.
|
||||
* @return {?(goog.dom.a11y.Role|string)} rolename.
|
||||
* @deprecated Use {@link goog.a11y.aria.getRole} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.getRole = function(element) {
|
||||
return /** @type {?(goog.dom.a11y.Role|string)} */ (
|
||||
goog.a11y.aria.getRole(/** @type {!Element} */ (element)));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the state or property of an element.
|
||||
* @param {Element} element DOM node where we set state.
|
||||
* @param {goog.dom.a11y.State|string} state State attribute being set.
|
||||
* Automatically adds prefix 'aria-' to the state name.
|
||||
* @param {boolean|number|string} value Value for the
|
||||
* state attribute.
|
||||
* @deprecated Use {@link goog.a11y.aria.setState} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.setState = function(element, state, value) {
|
||||
goog.a11y.aria.setState(
|
||||
/** @type {!Element} */ (element),
|
||||
/** @type {!goog.dom.a11y.State} */ (state),
|
||||
/** @type {boolean|number|string} */ (value));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets value of specified state or property.
|
||||
* @param {Element} element DOM node to get state from.
|
||||
* @param {goog.dom.a11y.State|string} stateName State name.
|
||||
* @return {string} Value of the state attribute.
|
||||
* @deprecated Use {@link goog.a11y.aria.getState} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.getState = function(element, stateName) {
|
||||
return goog.a11y.aria.getState(
|
||||
/** @type {!Element} */ (element),
|
||||
/** @type {!goog.dom.a11y.State} */ (stateName));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the activedescendant of the given element.
|
||||
* @param {Element} element DOM node to get activedescendant from.
|
||||
* @return {Element} DOM node of the activedescendant.
|
||||
* @deprecated Use {@link goog.a11y.aria.getActiveDescendant} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.getActiveDescendant = function(element) {
|
||||
return goog.a11y.aria.getActiveDescendant(
|
||||
/** @type {!Element} */ (element));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the activedescendant value for an element.
|
||||
* @param {Element} element DOM node to set activedescendant to.
|
||||
* @param {Element} activeElement DOM node being set as activedescendant.
|
||||
* @deprecated Use {@link goog.a11y.aria.setActiveDescendant} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.setActiveDescendant = function(element, activeElement) {
|
||||
goog.a11y.aria.setActiveDescendant(
|
||||
/** @type {!Element} */ (element),
|
||||
activeElement);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Class that allows messages to be spoken by assistive technologies that the
|
||||
* user may have active.
|
||||
*
|
||||
* @param {goog.dom.DomHelper} domHelper DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.Disposable}
|
||||
* @deprecated Use {@link goog.a11y.aria.Announcer} instead.
|
||||
* This alias will be removed on 1 Apr 2013.
|
||||
*/
|
||||
goog.dom.a11y.Announcer = goog.a11y.aria.Announcer;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
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,528 @@
|
||||
// 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 || 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,355 @@
|
||||
// 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.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
/**
|
||||
* 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} 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 {Function} annotateFn A function that takes
|
||||
* (1) the number of the term that is "hit",
|
||||
* (2) the HTML string (search term) to be annotated,
|
||||
* and returns the annotated term as an HTML string.
|
||||
* @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.
|
||||
* @type {Array}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.annotate.NODES_TO_SKIP_ = ['SCRIPT', 'STYLE', 'TEXTAREA'];
|
||||
|
||||
|
||||
/**
|
||||
* Recursive helper function.
|
||||
*
|
||||
* @param {Node} node A DOM node.
|
||||
* @param {Array} 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 {Function} annotateFn function(number, string) : string A function
|
||||
* that takes :
|
||||
* (1) the number of the term that is "hit",
|
||||
* (2) the HTML string (search term) to be annotated,
|
||||
* and returns the annotated term as an HTML string.
|
||||
* @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');
|
||||
tempNode.innerHTML = 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} 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 {Function} annotateFn {function(number, string) : string} A function
|
||||
* that takes
|
||||
* (1) the number of the term that is "hit",
|
||||
* (2) the HTML string (search term) to be annotated,
|
||||
* and returns the annotated term as an HTML string.
|
||||
* @param {*=} opt_ignoreCase Whether to ignore the case of the query
|
||||
* terms when looking for matches.
|
||||
* @return {?string} 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} 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 {Function} annotateFn {function(number, string) : string} A function
|
||||
* that takes
|
||||
* (1) the number of the term that is "hit",
|
||||
* (2) the HTML string (search term) to be annotated,
|
||||
* and returns the annotated term as an HTML string.
|
||||
* @param {*} ignoreCase Whether to ignore the case of the query terms
|
||||
* when looking for matches.
|
||||
* @return {?string} 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(goog.string.htmlEscape(text.substr(pos, posOfNextHit - pos)));
|
||||
|
||||
// Append the annotated term.
|
||||
var termLen = terms[termIndexOfNextHit][0].length;
|
||||
var termHtml = goog.string.htmlEscape(text.substr(posOfNextHit, termLen));
|
||||
html.push(annotateFn(termIndexOfNextHit, termHtml));
|
||||
|
||||
pos = posOfNextHit + termLen;
|
||||
}
|
||||
|
||||
// Append everything after the last hit.
|
||||
html.push(goog.string.htmlEscape(text.substr(pos)));
|
||||
return html.join('');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Converts terms to lowercase.
|
||||
*
|
||||
* @param {Array} terms An array of
|
||||
* [{string} searchTerm, {boolean} matchWholeWordOnly] tuples.
|
||||
* @return {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,67 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,352 @@
|
||||
// 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)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
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,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 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)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*
|
||||
* @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.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.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
|
||||
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,86 @@
|
||||
// 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}
|
||||
*/
|
||||
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.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,952 @@
|
||||
// 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)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
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}
|
||||
*/
|
||||
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,83 @@
|
||||
// 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}
|
||||
*/
|
||||
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,365 @@
|
||||
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview Definition of the W3C spec following range wrapper.
|
||||
*
|
||||
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.browserrange.W3cRange');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.RangeEndpoint');
|
||||
goog.require('goog.dom.browserrange.AbstractRange');
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The constructor for W3C specific browser ranges.
|
||||
* @param {Range} range The range object.
|
||||
* @constructor
|
||||
* @extends {goog.dom.browserrange.AbstractRange}
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange = function(range) {
|
||||
this.range_ = range;
|
||||
};
|
||||
goog.inherits(goog.dom.browserrange.W3cRange,
|
||||
goog.dom.browserrange.AbstractRange);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a browser range spanning the given node's contents.
|
||||
* @param {Node} node The node to select.
|
||||
* @return {Range} A browser range spanning the node's contents.
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {
|
||||
var nodeRange = goog.dom.getOwnerDocument(node).createRange();
|
||||
|
||||
if (node.nodeType == goog.dom.NodeType.TEXT) {
|
||||
nodeRange.setStart(node, 0);
|
||||
nodeRange.setEnd(node, node.length);
|
||||
} else {
|
||||
if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
|
||||
var rangeParent = node.parentNode;
|
||||
var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);
|
||||
nodeRange.setStart(rangeParent, rangeStartOffset);
|
||||
nodeRange.setEnd(rangeParent, rangeStartOffset + 1);
|
||||
} else {
|
||||
var tempNode, leaf = node;
|
||||
while ((tempNode = leaf.firstChild) &&
|
||||
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
|
||||
leaf = tempNode;
|
||||
}
|
||||
nodeRange.setStart(leaf, 0);
|
||||
|
||||
leaf = node;
|
||||
while ((tempNode = leaf.lastChild) &&
|
||||
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
|
||||
leaf = tempNode;
|
||||
}
|
||||
nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ?
|
||||
leaf.childNodes.length : leaf.length);
|
||||
}
|
||||
}
|
||||
|
||||
return nodeRange;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a browser range spanning the given nodes.
|
||||
* @param {Node} startNode The node to start with - should not be a BR.
|
||||
* @param {number} startOffset The offset within the start node.
|
||||
* @param {Node} endNode The node to end with - should not be a BR.
|
||||
* @param {number} endOffset The offset within the end node.
|
||||
* @return {Range} A browser range spanning the node's contents.
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode,
|
||||
startOffset, endNode, endOffset) {
|
||||
// Create and return the range.
|
||||
var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();
|
||||
nodeRange.setStart(startNode, startOffset);
|
||||
nodeRange.setEnd(endNode, endOffset);
|
||||
return nodeRange;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a range object that selects the given node's text.
|
||||
* @param {Node} node The node to select.
|
||||
* @return {goog.dom.browserrange.W3cRange} A Gecko range wrapper object.
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {
|
||||
return new goog.dom.browserrange.W3cRange(
|
||||
goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a range object that selects between the given nodes.
|
||||
* @param {Node} startNode The node to start with.
|
||||
* @param {number} startOffset The offset within the start node.
|
||||
* @param {Node} endNode The node to end with.
|
||||
* @param {number} endOffset The offset within the end node.
|
||||
* @return {goog.dom.browserrange.W3cRange} A wrapper object.
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.createFromNodes = function(startNode,
|
||||
startOffset, endNode, endOffset) {
|
||||
return new goog.dom.browserrange.W3cRange(
|
||||
goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode,
|
||||
startOffset, endNode, endOffset));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.browserrange.W3cRange} A clone of this range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.prototype.clone = function() {
|
||||
return new this.constructor(this.range_.cloneRange());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {
|
||||
return this.range_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getContainer = function() {
|
||||
return this.range_.commonAncestorContainer;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {
|
||||
return this.range_.startContainer;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {
|
||||
return this.range_.startOffset;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {
|
||||
return this.range_.endContainer;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {
|
||||
return this.range_.endOffset;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints =
|
||||
function(range, thisEndpoint, otherEndpoint) {
|
||||
return this.range_.compareBoundaryPoints(
|
||||
otherEndpoint == goog.dom.RangeEndpoint.START ?
|
||||
(thisEndpoint == goog.dom.RangeEndpoint.START ?
|
||||
goog.global['Range'].START_TO_START :
|
||||
goog.global['Range'].START_TO_END) :
|
||||
(thisEndpoint == goog.dom.RangeEndpoint.START ?
|
||||
goog.global['Range'].END_TO_START :
|
||||
goog.global['Range'].END_TO_END),
|
||||
/** @type {Range} */ (range));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {
|
||||
return this.range_.collapsed;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getText = function() {
|
||||
return this.range_.toString();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {
|
||||
var div = goog.dom.getDomHelper(this.range_.startContainer).createDom('div');
|
||||
div.appendChild(this.range_.cloneContents());
|
||||
var result = div.innerHTML;
|
||||
|
||||
if (goog.string.startsWith(result, '<') ||
|
||||
!this.isCollapsed() && !goog.string.contains(result, '<')) {
|
||||
// We attempt to mimic IE, which returns no containing element when a
|
||||
// only text nodes are selected, does return the containing element when
|
||||
// the selection is empty, and does return the element when multiple nodes
|
||||
// are selected.
|
||||
return result;
|
||||
}
|
||||
|
||||
var container = this.getContainer();
|
||||
container = container.nodeType == goog.dom.NodeType.ELEMENT ? container :
|
||||
container.parentNode;
|
||||
|
||||
var html = goog.dom.getOuterHtml(
|
||||
/** @type {Element} */ (container.cloneNode(false)));
|
||||
return html.replace('>', '>' + result);
|
||||
};
|
||||
|
||||
|
||||
// SELECTION MODIFICATION
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {
|
||||
var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
|
||||
this.selectInternal(win.getSelection(), reverse);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Select this range.
|
||||
* @param {Selection} selection Browser selection object.
|
||||
* @param {*} reverse Whether to select this range in reverse.
|
||||
* @protected
|
||||
*/
|
||||
goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection,
|
||||
reverse) {
|
||||
// Browser-specific tricks are needed to create reversed selections
|
||||
// programatically. For this generic W3C codepath, ignore the reverse
|
||||
// parameter.
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(this.range_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.removeContents = function() {
|
||||
var range = this.range_;
|
||||
range.extractContents();
|
||||
|
||||
if (range.startContainer.hasChildNodes()) {
|
||||
// Remove any now empty nodes surrounding the extracted contents.
|
||||
var rangeStartContainer =
|
||||
range.startContainer.childNodes[range.startOffset];
|
||||
if (rangeStartContainer) {
|
||||
var rangePrevious = rangeStartContainer.previousSibling;
|
||||
|
||||
if (goog.dom.getRawTextContent(rangeStartContainer) == '') {
|
||||
goog.dom.removeNode(rangeStartContainer);
|
||||
}
|
||||
|
||||
if (rangePrevious && goog.dom.getRawTextContent(rangePrevious) == '') {
|
||||
goog.dom.removeNode(rangePrevious);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {
|
||||
this.range_.surroundContents(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {
|
||||
var range = this.range_.cloneRange();
|
||||
range.collapse(before);
|
||||
range.insertNode(node);
|
||||
range.detach();
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(
|
||||
startNode, endNode) {
|
||||
var win = goog.dom.getWindow(
|
||||
goog.dom.getOwnerDocument(this.getStartNode()));
|
||||
var selectionRange = goog.dom.Range.createFromWindow(win);
|
||||
if (selectionRange) {
|
||||
var sNode = selectionRange.getStartNode();
|
||||
var eNode = selectionRange.getEndNode();
|
||||
var sOffset = selectionRange.getStartOffset();
|
||||
var eOffset = selectionRange.getEndOffset();
|
||||
}
|
||||
|
||||
var clone1 = this.range_.cloneRange();
|
||||
var clone2 = this.range_.cloneRange();
|
||||
|
||||
clone1.collapse(false);
|
||||
clone2.collapse(true);
|
||||
|
||||
clone1.insertNode(endNode);
|
||||
clone2.insertNode(startNode);
|
||||
|
||||
clone1.detach();
|
||||
clone2.detach();
|
||||
|
||||
if (selectionRange) {
|
||||
// There are 4 ways that surroundWithNodes can wreck the saved
|
||||
// selection object. All of them happen when an inserted node splits
|
||||
// a text node, and one of the end points of the selection was in the
|
||||
// latter half of that text node.
|
||||
//
|
||||
// Clients of this library should use saveUsingCarets to avoid this
|
||||
// problem. Unfortunately, saveUsingCarets uses this method, so that's
|
||||
// not really an option for us. :( We just recompute the offsets.
|
||||
var isInsertedNode = function(n) {
|
||||
return n == startNode || n == endNode;
|
||||
};
|
||||
if (sNode.nodeType == goog.dom.NodeType.TEXT) {
|
||||
while (sOffset > sNode.length) {
|
||||
sOffset -= sNode.length;
|
||||
do {
|
||||
sNode = sNode.nextSibling;
|
||||
} while (isInsertedNode(sNode));
|
||||
}
|
||||
}
|
||||
|
||||
if (eNode.nodeType == goog.dom.NodeType.TEXT) {
|
||||
while (eOffset > eNode.length) {
|
||||
eOffset -= eNode.length;
|
||||
do {
|
||||
eNode = eNode.nextSibling;
|
||||
} while (isInsertedNode(eNode));
|
||||
}
|
||||
}
|
||||
|
||||
goog.dom.Range.createFromNodes(
|
||||
sNode, /** @type {number} */ (sOffset),
|
||||
eNode, /** @type {number} */ (eOffset)).select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {
|
||||
this.range_.collapse(toStart);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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}
|
||||
*/
|
||||
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,202 @@
|
||||
// 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}
|
||||
*/
|
||||
goog.dom.BufferedViewportSizeMonitor = function(
|
||||
viewportSizeMonitor, opt_bufferMs) {
|
||||
goog.base(this);
|
||||
|
||||
/**
|
||||
* 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.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,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,227 @@
|
||||
// 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).
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
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.
|
||||
*/
|
||||
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!
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
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,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,271 @@
|
||||
// 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
|
||||
* will not work on elements with differing interfaces (such as SVGElements).
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.classlist');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
/**
|
||||
* Enables use of the native DOMTokenList methods. See the spec at
|
||||
* {@link http://dom.spec.whatwg.org/#domtokenlist}.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ =
|
||||
goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST ||
|
||||
// Whether DOMTokenList exists.
|
||||
(!!goog.global['DOMTokenList']);
|
||||
|
||||
|
||||
/**
|
||||
* 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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element) {
|
||||
return element.classList;
|
||||
} :
|
||||
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) || [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element, className) {
|
||||
goog.asserts.assert(!!element.classList);
|
||||
return element.classList.contains(className);
|
||||
} :
|
||||
function(element, 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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element, className) {
|
||||
element.classList.add(className);
|
||||
} :
|
||||
function(element, className) {
|
||||
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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element, classesToAdd) {
|
||||
goog.array.forEach(classesToAdd, function(className) {
|
||||
goog.dom.classlist.add(element, className);
|
||||
});
|
||||
} :
|
||||
function(element, classesToAdd) {
|
||||
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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element, className) {
|
||||
element.classList.remove(className);
|
||||
} :
|
||||
function(element, className) {
|
||||
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 = goog.dom.classlist.NATIVE_DOM_TOKEN_LIST_ ?
|
||||
function(element, classesToRemove) {
|
||||
goog.array.forEach(classesToRemove, function(className) {
|
||||
goog.dom.classlist.remove(element, className);
|
||||
});
|
||||
} :
|
||||
function(element, classesToRemove) {
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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,244 @@
|
||||
// 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.jsunit');
|
||||
|
||||
|
||||
var classlist = goog.dom.classlist;
|
||||
|
||||
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 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,505 @@
|
||||
// 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)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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,135 @@
|
||||
// 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) {
|
||||
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 attibute 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;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
// 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.
|
||||
*/
|
||||
|
||||
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}
|
||||
*/
|
||||
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,414 @@
|
||||
// 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)
|
||||
* @author jonp@google.com (Jon Perlow)
|
||||
* @author elsigh@google.com (Lindsey Simon)
|
||||
*/
|
||||
|
||||
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} A map of the form data as form 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} 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} 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.
|
||||
*/
|
||||
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} */ (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 ? 'checked' : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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,124 @@
|
||||
// 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');
|
||||
goog.require('goog.userAgent.product');
|
||||
|
||||
|
||||
/**
|
||||
* Event types for full screen.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.fullscreen.EventType = {
|
||||
/** Dispatched by the Document when the fullscreen status changes. */
|
||||
CHANGE: goog.userAgent.WEBKIT ?
|
||||
'webkitfullscreenchange' :
|
||||
'mozfullscreenchange'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.ALLOW_KEYBOARD_INPUT &&
|
||||
goog.userAgent.product.CHROME) {
|
||||
// Safari has the ALLOW_KEYBOARD_INPUT property but using it gives an error.
|
||||
element.webkitRequestFullScreen(element.ALLOW_KEYBOARD_INPUT);
|
||||
} 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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return !!doc.webkitIsFullScreen || !!doc.mozFullScreen;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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,127 @@
|
||||
// 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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,519 @@
|
||||
// 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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,86 @@
|
||||
// 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}
|
||||
*/
|
||||
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,112 @@
|
||||
// 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.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
*/
|
||||
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,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,72 @@
|
||||
// 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
|
||||
*/
|
||||
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,77 @@
|
||||
// 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
|
||||
*/
|
||||
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,154 @@
|
||||
// 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}
|
||||
*/
|
||||
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,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 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}
|
||||
*/
|
||||
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,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 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}
|
||||
*/
|
||||
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,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 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
|
||||
*/
|
||||
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,58 @@
|
||||
// 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}
|
||||
*/
|
||||
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,190 @@
|
||||
// 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}
|
||||
*/
|
||||
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,141 @@
|
||||
// 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.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}
|
||||
*/
|
||||
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,70 @@
|
||||
// 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}
|
||||
*/
|
||||
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,226 @@
|
||||
// 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)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.Range');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
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.userAgent.IE ? 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,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,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,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,470 @@
|
||||
// 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.
|
||||
*
|
||||
* @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,367 @@
|
||||
// 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.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}
|
||||
*/
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
// 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 Defines the goog.dom.TagName enum. This enumerates
|
||||
* all HTML tag names specified in either the the W3C HTML 4.01 index of
|
||||
* elements or the HTML5 draft specification.
|
||||
*
|
||||
* References:
|
||||
* http://www.w3.org/TR/html401/index/elements.html
|
||||
* http://dev.w3.org/html5/spec/section-index.html
|
||||
*
|
||||
*/
|
||||
goog.provide('goog.dom.TagName');
|
||||
|
||||
|
||||
/**
|
||||
* Enum of all html tag names specified by the W3C HTML4.01 and HTML5
|
||||
* specifications.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.dom.TagName = {
|
||||
A: 'A',
|
||||
ABBR: 'ABBR',
|
||||
ACRONYM: 'ACRONYM',
|
||||
ADDRESS: 'ADDRESS',
|
||||
APPLET: 'APPLET',
|
||||
AREA: 'AREA',
|
||||
ARTICLE: 'ARTICLE',
|
||||
ASIDE: 'ASIDE',
|
||||
AUDIO: 'AUDIO',
|
||||
B: 'B',
|
||||
BASE: 'BASE',
|
||||
BASEFONT: 'BASEFONT',
|
||||
BDI: 'BDI',
|
||||
BDO: 'BDO',
|
||||
BIG: 'BIG',
|
||||
BLOCKQUOTE: 'BLOCKQUOTE',
|
||||
BODY: 'BODY',
|
||||
BR: 'BR',
|
||||
BUTTON: 'BUTTON',
|
||||
CANVAS: 'CANVAS',
|
||||
CAPTION: 'CAPTION',
|
||||
CENTER: 'CENTER',
|
||||
CITE: 'CITE',
|
||||
CODE: 'CODE',
|
||||
COL: 'COL',
|
||||
COLGROUP: 'COLGROUP',
|
||||
COMMAND: 'COMMAND',
|
||||
DATA: 'DATA',
|
||||
DATALIST: 'DATALIST',
|
||||
DD: 'DD',
|
||||
DEL: 'DEL',
|
||||
DETAILS: 'DETAILS',
|
||||
DFN: 'DFN',
|
||||
DIALOG: 'DIALOG',
|
||||
DIR: 'DIR',
|
||||
DIV: 'DIV',
|
||||
DL: 'DL',
|
||||
DT: 'DT',
|
||||
EM: 'EM',
|
||||
EMBED: 'EMBED',
|
||||
FIELDSET: 'FIELDSET',
|
||||
FIGCAPTION: 'FIGCAPTION',
|
||||
FIGURE: 'FIGURE',
|
||||
FONT: 'FONT',
|
||||
FOOTER: 'FOOTER',
|
||||
FORM: 'FORM',
|
||||
FRAME: 'FRAME',
|
||||
FRAMESET: 'FRAMESET',
|
||||
H1: 'H1',
|
||||
H2: 'H2',
|
||||
H3: 'H3',
|
||||
H4: 'H4',
|
||||
H5: 'H5',
|
||||
H6: 'H6',
|
||||
HEAD: 'HEAD',
|
||||
HEADER: 'HEADER',
|
||||
HGROUP: 'HGROUP',
|
||||
HR: 'HR',
|
||||
HTML: 'HTML',
|
||||
I: 'I',
|
||||
IFRAME: 'IFRAME',
|
||||
IMG: 'IMG',
|
||||
INPUT: 'INPUT',
|
||||
INS: 'INS',
|
||||
ISINDEX: 'ISINDEX',
|
||||
KBD: 'KBD',
|
||||
KEYGEN: 'KEYGEN',
|
||||
LABEL: 'LABEL',
|
||||
LEGEND: 'LEGEND',
|
||||
LI: 'LI',
|
||||
LINK: 'LINK',
|
||||
MAP: 'MAP',
|
||||
MARK: 'MARK',
|
||||
MATH: 'MATH',
|
||||
MENU: 'MENU',
|
||||
META: 'META',
|
||||
METER: 'METER',
|
||||
NAV: 'NAV',
|
||||
NOFRAMES: 'NOFRAMES',
|
||||
NOSCRIPT: 'NOSCRIPT',
|
||||
OBJECT: 'OBJECT',
|
||||
OL: 'OL',
|
||||
OPTGROUP: 'OPTGROUP',
|
||||
OPTION: 'OPTION',
|
||||
OUTPUT: 'OUTPUT',
|
||||
P: 'P',
|
||||
PARAM: 'PARAM',
|
||||
PRE: 'PRE',
|
||||
PROGRESS: 'PROGRESS',
|
||||
Q: 'Q',
|
||||
RP: 'RP',
|
||||
RT: 'RT',
|
||||
RUBY: 'RUBY',
|
||||
S: 'S',
|
||||
SAMP: 'SAMP',
|
||||
SCRIPT: 'SCRIPT',
|
||||
SECTION: 'SECTION',
|
||||
SELECT: 'SELECT',
|
||||
SMALL: 'SMALL',
|
||||
SOURCE: 'SOURCE',
|
||||
SPAN: 'SPAN',
|
||||
STRIKE: 'STRIKE',
|
||||
STRONG: 'STRONG',
|
||||
STYLE: 'STYLE',
|
||||
SUB: 'SUB',
|
||||
SUMMARY: 'SUMMARY',
|
||||
SUP: 'SUP',
|
||||
SVG: 'SVG',
|
||||
TABLE: 'TABLE',
|
||||
TBODY: 'TBODY',
|
||||
TD: 'TD',
|
||||
TEXTAREA: 'TEXTAREA',
|
||||
TFOOT: 'TFOOT',
|
||||
TH: 'TH',
|
||||
THEAD: 'THEAD',
|
||||
TIME: 'TIME',
|
||||
TITLE: 'TITLE',
|
||||
TR: 'TR',
|
||||
TRACK: 'TRACK',
|
||||
TT: 'TT',
|
||||
U: 'U',
|
||||
UL: 'UL',
|
||||
VAR: 'VAR',
|
||||
VIDEO: 'VIDEO',
|
||||
WBR: 'WBR'
|
||||
};
|
||||
@@ -0,0 +1,630 @@
|
||||
// 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 text ranges in HTML documents.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author ojan@google.com (Ojan Vafai)
|
||||
* @author jparent@google.com (Julie Parent)
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.dom.TextRange');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.AbstractRange');
|
||||
goog.require('goog.dom.RangeType');
|
||||
goog.require('goog.dom.SavedRange');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.dom.TextRangeIterator');
|
||||
goog.require('goog.dom.browserrange');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new text selection 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.TextRange = function() {
|
||||
};
|
||||
goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
|
||||
|
||||
|
||||
/**
|
||||
* 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 {Range|TextRange} range The browser range object.
|
||||
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
||||
* node.
|
||||
* @return {goog.dom.TextRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
|
||||
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
|
||||
goog.dom.browserrange.createRange(range), opt_isReversed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper from the given browser range wrapper.
|
||||
* @param {goog.dom.browserrange.AbstractRange} browserRange The browser range
|
||||
* wrapper.
|
||||
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
||||
* node.
|
||||
* @return {goog.dom.TextRange} A range wrapper object.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange,
|
||||
opt_isReversed) {
|
||||
var range = new goog.dom.TextRange();
|
||||
|
||||
// Initialize the range as a browser range wrapper type range.
|
||||
range.browserRangeWrapper_ = browserRange;
|
||||
range.isReversed_ = !!opt_isReversed;
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that selects the given node's text. Do not use
|
||||
* this method directly - please use goog.dom.Range.createFrom* instead.
|
||||
* @param {Node} node The node to select.
|
||||
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
|
||||
* node.
|
||||
* @return {goog.dom.TextRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
|
||||
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
|
||||
goog.dom.browserrange.createRangeFromNodeContents(node),
|
||||
opt_isReversed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range wrapper that selects the area between the given nodes,
|
||||
* accounting for the given offsets. Do not use this method directly - please
|
||||
* use goog.dom.Range.createFrom* instead.
|
||||
* @param {Node} anchorNode The node to start with.
|
||||
* @param {number} anchorOffset The offset within the node to start.
|
||||
* @param {Node} focusNode The node to end with.
|
||||
* @param {number} focusOffset The offset within the node to end.
|
||||
* @return {goog.dom.TextRange} A range wrapper object.
|
||||
*/
|
||||
goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset,
|
||||
focusNode, focusOffset) {
|
||||
var range = new goog.dom.TextRange();
|
||||
range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset,
|
||||
focusNode, focusOffset);
|
||||
|
||||
// Avoid selecting terminal elements directly
|
||||
if (goog.dom.isElement(anchorNode) && !goog.dom.canHaveChildren(anchorNode)) {
|
||||
var parent = anchorNode.parentNode;
|
||||
anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
|
||||
anchorNode = parent;
|
||||
}
|
||||
|
||||
if (goog.dom.isElement(focusNode) && !goog.dom.canHaveChildren(focusNode)) {
|
||||
var parent = focusNode.parentNode;
|
||||
focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
|
||||
focusNode = parent;
|
||||
}
|
||||
|
||||
// Initialize the range as a W3C style range.
|
||||
if (range.isReversed_) {
|
||||
range.startNode_ = focusNode;
|
||||
range.startOffset_ = focusOffset;
|
||||
range.endNode_ = anchorNode;
|
||||
range.endOffset_ = anchorOffset;
|
||||
} else {
|
||||
range.startNode_ = anchorNode;
|
||||
range.startOffset_ = anchorOffset;
|
||||
range.endNode_ = focusNode;
|
||||
range.endOffset_ = focusOffset;
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
// Representation 1: a browser range wrapper.
|
||||
|
||||
|
||||
/**
|
||||
* The browser specific range wrapper. This can be null if one of the other
|
||||
* representations of the range is specified.
|
||||
* @type {goog.dom.browserrange.AbstractRange?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.browserRangeWrapper_ = null;
|
||||
|
||||
|
||||
// Representation 2: two endpoints specified as nodes + offsets
|
||||
|
||||
|
||||
/**
|
||||
* The start node of the range. This can be null if one of the other
|
||||
* representations of the range is specified.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.startNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The start offset of the range. This can be null if one of the other
|
||||
* representations of the range is specified.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.startOffset_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The end node of the range. This can be null if one of the other
|
||||
* representations of the range is specified.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.endNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The end offset of the range. This can be null if one of the other
|
||||
* representations of the range is specified.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.endOffset_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the focus node is before the anchor node.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.isReversed_ = false;
|
||||
|
||||
|
||||
// Method implementations
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.TextRange} A clone of this range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.TextRange.prototype.clone = function() {
|
||||
var range = new goog.dom.TextRange();
|
||||
range.browserRangeWrapper_ = this.browserRangeWrapper_;
|
||||
range.startNode_ = this.startNode_;
|
||||
range.startOffset_ = this.startOffset_;
|
||||
range.endNode_ = this.endNode_;
|
||||
range.endOffset_ = this.endOffset_;
|
||||
range.isReversed_ = this.isReversed_;
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getType = function() {
|
||||
return goog.dom.RangeType.TEXT;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
|
||||
return this.getBrowserRangeWrapper_().getBrowserRange();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
|
||||
// Test if it's a control range by seeing if a control range only method
|
||||
// exists.
|
||||
if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
|
||||
return false;
|
||||
}
|
||||
this.browserRangeWrapper_ = goog.dom.browserrange.createRange(
|
||||
nativeRange);
|
||||
this.clearCachedValues_();
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clear all cached values.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.clearCachedValues_ = function() {
|
||||
this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getTextRangeCount = function() {
|
||||
return 1;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getTextRange = function(i) {
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.browserrange.AbstractRange} The range wrapper object.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
|
||||
return this.browserRangeWrapper_ ||
|
||||
(this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(
|
||||
this.getStartNode(), this.getStartOffset(),
|
||||
this.getEndNode(), this.getEndOffset()));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getContainer = function() {
|
||||
return this.getBrowserRangeWrapper_().getContainer();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getStartNode = function() {
|
||||
return this.startNode_ ||
|
||||
(this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getStartOffset = function() {
|
||||
return this.startOffset_ != null ? this.startOffset_ :
|
||||
(this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getStartPosition = function() {
|
||||
return this.isReversed() ?
|
||||
this.getBrowserRangeWrapper_().getEndPosition() :
|
||||
this.getBrowserRangeWrapper_().getStartPosition();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getEndNode = function() {
|
||||
return this.endNode_ ||
|
||||
(this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getEndOffset = function() {
|
||||
return this.endOffset_ != null ? this.endOffset_ :
|
||||
(this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getEndPosition = function() {
|
||||
return this.isReversed() ?
|
||||
this.getBrowserRangeWrapper_().getStartPosition() :
|
||||
this.getBrowserRangeWrapper_().getEndPosition();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Moves a TextRange to the provided nodes and offsets.
|
||||
* @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.
|
||||
* @param {boolean} isReversed Whether the range is reversed.
|
||||
*/
|
||||
goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset,
|
||||
endNode, endOffset,
|
||||
isReversed) {
|
||||
this.startNode_ = startNode;
|
||||
this.startOffset_ = startOffset;
|
||||
this.endNode_ = endNode;
|
||||
this.endOffset_ = endOffset;
|
||||
this.isReversed_ = isReversed;
|
||||
this.browserRangeWrapper_ = null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.isReversed = function() {
|
||||
return this.isReversed_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.containsRange = function(otherRange,
|
||||
opt_allowPartial) {
|
||||
var otherRangeType = otherRange.getType();
|
||||
if (otherRangeType == goog.dom.RangeType.TEXT) {
|
||||
return this.getBrowserRangeWrapper_().containsRange(
|
||||
otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
|
||||
} else if (otherRangeType == goog.dom.RangeType.CONTROL) {
|
||||
var elements = otherRange.getElements();
|
||||
var fn = opt_allowPartial ? goog.array.some : goog.array.every;
|
||||
return fn(elements, function(el) {
|
||||
return this.containsNode(el, opt_allowPartial);
|
||||
}, this);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests if the given node is in a document.
|
||||
* @param {Node} node The node to check.
|
||||
* @return {boolean} Whether the given node is in the given document.
|
||||
*/
|
||||
goog.dom.TextRange.isAttachedNode = function(node) {
|
||||
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
|
||||
var returnValue = false;
|
||||
/** @preserveTry */
|
||||
try {
|
||||
returnValue = node.parentNode;
|
||||
} catch (e) {
|
||||
// IE sometimes throws Invalid Argument errors when a node is detached.
|
||||
// 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;
|
||||
} else {
|
||||
return goog.dom.contains(node.ownerDocument.body, node);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.isRangeInDocument = function() {
|
||||
// Ensure any cached nodes are in the document. IE also allows ranges to
|
||||
// become detached, so we check if the range is still in the document as
|
||||
// well for IE.
|
||||
return (!this.startNode_ ||
|
||||
goog.dom.TextRange.isAttachedNode(this.startNode_)) &&
|
||||
(!this.endNode_ ||
|
||||
goog.dom.TextRange.isAttachedNode(this.endNode_)) &&
|
||||
(!(goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||
|
||||
this.getBrowserRangeWrapper_().isRangeInDocument());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.isCollapsed = function() {
|
||||
return this.getBrowserRangeWrapper_().isCollapsed();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getText = function() {
|
||||
return this.getBrowserRangeWrapper_().getText();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getHtmlFragment = function() {
|
||||
// TODO(robbyw): Generalize the code in browserrange so it is static and
|
||||
// just takes an iterator. This would mean we don't always have to create a
|
||||
// browser range.
|
||||
return this.getBrowserRangeWrapper_().getHtmlFragment();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getValidHtml = function() {
|
||||
return this.getBrowserRangeWrapper_().getValidHtml();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.getPastableHtml = function() {
|
||||
// TODO(robbyw): Get any attributes the table or tr has.
|
||||
|
||||
var html = this.getValidHtml();
|
||||
|
||||
if (html.match(/^\s*<td\b/i)) {
|
||||
// Match html starting with a TD.
|
||||
html = '<table><tbody><tr>' + html + '</tr></tbody></table>';
|
||||
} else if (html.match(/^\s*<tr\b/i)) {
|
||||
// Match html starting with a TR.
|
||||
html = '<table><tbody>' + html + '</tbody></table>';
|
||||
} else if (html.match(/^\s*<tbody\b/i)) {
|
||||
// Match html starting with a TBODY.
|
||||
html = '<table>' + html + '</table>';
|
||||
} else if (html.match(/^\s*<li\b/i)) {
|
||||
// Match html starting with an LI.
|
||||
var container = this.getContainer();
|
||||
var tagType = goog.dom.TagName.UL;
|
||||
while (container) {
|
||||
if (container.tagName == goog.dom.TagName.OL) {
|
||||
tagType = goog.dom.TagName.OL;
|
||||
break;
|
||||
} else if (container.tagName == goog.dom.TagName.UL) {
|
||||
break;
|
||||
}
|
||||
container = container.parentNode;
|
||||
}
|
||||
html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a TextRangeIterator 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.TextRangeIterator} An iterator over tags in the range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
|
||||
return new goog.dom.TextRangeIterator(this.getStartNode(),
|
||||
this.getStartOffset(), this.getEndNode(), this.getEndOffset());
|
||||
};
|
||||
|
||||
|
||||
// RANGE ACTIONS
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.select = function() {
|
||||
this.getBrowserRangeWrapper_().select(this.isReversed_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.removeContents = function() {
|
||||
this.getBrowserRangeWrapper_().removeContents();
|
||||
this.clearCachedValues_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.TextRange.prototype.surroundContents = function(element) {
|
||||
var output = this.getBrowserRangeWrapper_().surroundContents(element);
|
||||
this.clearCachedValues_();
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.insertNode = function(node, before) {
|
||||
var output = this.getBrowserRangeWrapper_().insertNode(node, before);
|
||||
this.clearCachedValues_();
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
|
||||
this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
|
||||
this.clearCachedValues_();
|
||||
};
|
||||
|
||||
|
||||
// SAVE/RESTORE
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.saveUsingDom = function() {
|
||||
return new goog.dom.DomSavedTextRange_(this);
|
||||
};
|
||||
|
||||
|
||||
// RANGE MODIFICATION
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRange.prototype.collapse = function(toAnchor) {
|
||||
var toStart = this.isReversed() ? !toAnchor : toAnchor;
|
||||
|
||||
if (this.browserRangeWrapper_) {
|
||||
this.browserRangeWrapper_.collapse(toStart);
|
||||
}
|
||||
|
||||
if (toStart) {
|
||||
this.endNode_ = this.startNode_;
|
||||
this.endOffset_ = this.startOffset_;
|
||||
} else {
|
||||
this.startNode_ = this.endNode_;
|
||||
this.startOffset_ = this.endOffset_;
|
||||
}
|
||||
|
||||
// Collapsed ranges can't be reversed
|
||||
this.isReversed_ = false;
|
||||
};
|
||||
|
||||
|
||||
// SAVED RANGE OBJECTS
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A SavedRange implementation using DOM endpoints.
|
||||
* @param {goog.dom.AbstractRange} range The range to save.
|
||||
* @constructor
|
||||
* @extends {goog.dom.SavedRange}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.DomSavedTextRange_ = function(range) {
|
||||
/**
|
||||
* The anchor node.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
this.anchorNode_ = range.getAnchorNode();
|
||||
|
||||
/**
|
||||
* The anchor node offset.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.anchorOffset_ = range.getAnchorOffset();
|
||||
|
||||
/**
|
||||
* The focus node.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
this.focusNode_ = range.getFocusNode();
|
||||
|
||||
/**
|
||||
* The focus node offset.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.focusOffset_ = range.getFocusOffset();
|
||||
};
|
||||
goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.AbstractRange} The restored range.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
|
||||
return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_,
|
||||
this.focusNode_, this.focusOffset_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
|
||||
goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
|
||||
|
||||
this.anchorNode_ = null;
|
||||
this.focusNode_ = null;
|
||||
};
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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 Iterator between two DOM text range positions.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.TextRangeIterator');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.RangeIterator');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.iter.StopIteration');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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} startNode The starting node position.
|
||||
* @param {number} startOffset The offset in to startNode. If startNode is
|
||||
* an element, indicates an offset in to childNodes. If startNode is a
|
||||
* text node, indicates an offset in to nodeValue.
|
||||
* @param {Node} endNode The ending node position.
|
||||
* @param {number} endOffset The offset in to endNode. If endNode is
|
||||
* an element, indicates an offset in to childNodes. If endNode is a
|
||||
* text node, indicates an offset in to nodeValue.
|
||||
* @param {boolean=} opt_reverse Whether to traverse nodes in reverse.
|
||||
* @constructor
|
||||
* @extends {goog.dom.RangeIterator}
|
||||
*/
|
||||
goog.dom.TextRangeIterator = function(startNode, startOffset, endNode,
|
||||
endOffset, opt_reverse) {
|
||||
var goNext;
|
||||
|
||||
if (startNode) {
|
||||
this.startNode_ = startNode;
|
||||
this.startOffset_ = startOffset;
|
||||
this.endNode_ = endNode;
|
||||
this.endOffset_ = endOffset;
|
||||
|
||||
// Skip to the offset nodes - being careful to special case BRs since these
|
||||
// have no children but still can appear as the startContainer of a range.
|
||||
if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&
|
||||
startNode.tagName != goog.dom.TagName.BR) {
|
||||
var startChildren = startNode.childNodes;
|
||||
var candidate = startChildren[startOffset];
|
||||
if (candidate) {
|
||||
this.startNode_ = candidate;
|
||||
this.startOffset_ = 0;
|
||||
} else {
|
||||
if (startChildren.length) {
|
||||
this.startNode_ =
|
||||
/** @type {Node} */ (goog.array.peek(startChildren));
|
||||
}
|
||||
goNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {
|
||||
this.endNode_ = endNode.childNodes[endOffset];
|
||||
if (this.endNode_) {
|
||||
this.endOffset_ = 0;
|
||||
} else {
|
||||
// The offset was past the last element.
|
||||
this.endNode_ = endNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ :
|
||||
this.startNode_, opt_reverse);
|
||||
|
||||
if (goNext) {
|
||||
try {
|
||||
this.next();
|
||||
} catch (e) {
|
||||
if (e != goog.iter.StopIteration) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator);
|
||||
|
||||
|
||||
/**
|
||||
* The first node in the selection.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.startNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The last node in the selection.
|
||||
* @type {Node}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.endNode_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The offset within the first node in the selection.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.startOffset_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The offset within the last node in the selection.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.endOffset_ = 0;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() {
|
||||
// Offsets only apply to text nodes. If our current node is the start node,
|
||||
// return the saved offset. Otherwise, return 0.
|
||||
return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 :
|
||||
this.node == this.startNode_ ? this.startOffset_ : 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() {
|
||||
// Offsets only apply to text nodes. If our current node is the end node,
|
||||
// return the saved offset. Otherwise, return the length of the node.
|
||||
return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 :
|
||||
this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.getStartNode = function() {
|
||||
return this.startNode_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Change the start node of the iterator.
|
||||
* @param {Node} node The new start node.
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.setStartNode = function(node) {
|
||||
if (!this.isStarted()) {
|
||||
this.setPosition(node);
|
||||
}
|
||||
|
||||
this.startNode_ = node;
|
||||
this.startOffset_ = 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.getEndNode = function() {
|
||||
return this.endNode_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Change the end node of the iterator.
|
||||
* @param {Node} node The new end node.
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.setEndNode = function(node) {
|
||||
this.endNode_ = node;
|
||||
this.endOffset_ = 0;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.isLast = function() {
|
||||
return this.isStarted() && this.node == this.endNode_ &&
|
||||
(!this.endOffset_ || !this.isStartTag());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.TextRangeIterator.prototype.next = function() {
|
||||
if (this.isLast()) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
|
||||
// Call the super function.
|
||||
return goog.dom.TextRangeIterator.superClass_.next.call(this);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.skipTag = function() {
|
||||
goog.dom.TextRangeIterator.superClass_.skipTag.apply(this);
|
||||
|
||||
// If the node we are skipping contains the end node, we just skipped past
|
||||
// the end, so we stop the iteration.
|
||||
if (goog.dom.contains(this.node, this.endNode_)) {
|
||||
throw goog.iter.StopIteration;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.TextRangeIterator.prototype.copyFrom = function(other) {
|
||||
this.startNode_ = other.startNode_;
|
||||
this.endNode_ = other.endNode_;
|
||||
this.startOffset_ = other.startOffset_;
|
||||
this.endOffset_ = other.endOffset_;
|
||||
this.isReversed_ = other.isReversed_;
|
||||
|
||||
goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.dom.TextRangeIterator} An identical iterator.
|
||||
* @override
|
||||
*/
|
||||
goog.dom.TextRangeIterator.prototype.clone = function() {
|
||||
var copy = new goog.dom.TextRangeIterator(this.startNode_,
|
||||
this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_);
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 Vendor prefix getters.
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.vendor');
|
||||
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
/**
|
||||
* Returns the JS vendor prefix used in CSS properties. Different vendors
|
||||
* use different methods of changing the case of the property names.
|
||||
*
|
||||
* @return {?string} The JS vendor prefix or null if there is none.
|
||||
*/
|
||||
goog.dom.vendor.getVendorJsPrefix = function() {
|
||||
if (goog.userAgent.WEBKIT) {
|
||||
return 'Webkit';
|
||||
} else if (goog.userAgent.GECKO) {
|
||||
return 'Moz';
|
||||
} else if (goog.userAgent.IE) {
|
||||
return 'ms';
|
||||
} else if (goog.userAgent.OPERA) {
|
||||
return 'O';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the vendor prefix used in CSS properties.
|
||||
*
|
||||
* @return {?string} The vendor prefix or null if there is none.
|
||||
*/
|
||||
goog.dom.vendor.getVendorPrefix = function() {
|
||||
if (goog.userAgent.WEBKIT) {
|
||||
return '-webkit';
|
||||
} else if (goog.userAgent.GECKO) {
|
||||
return '-moz';
|
||||
} else if (goog.userAgent.IE) {
|
||||
return '-ms';
|
||||
} else if (goog.userAgent.OPERA) {
|
||||
return '-o';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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 Utility class that monitors viewport size changes.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
* @see ../demos/viewportsizemonitor.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.ViewportSizeMonitor');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.math.Size');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class can be used to monitor changes in the viewport size. Instances
|
||||
* dispatch a {@link goog.events.EventType.RESIZE} event when the viewport size
|
||||
* changes. Handlers can call {@link goog.dom.ViewportSizeMonitor#getSize} to
|
||||
* get the new viewport size.
|
||||
*
|
||||
* Use this class if you want to execute resize/reflow logic each time the
|
||||
* user resizes the browser window. This class is guaranteed to only dispatch
|
||||
* {@code RESIZE} events when the pixel dimensions of the viewport change.
|
||||
* (Internet Explorer fires resize events if any element on the page is resized,
|
||||
* even if the viewport dimensions are unchanged, which can lead to infinite
|
||||
* resize loops.)
|
||||
*
|
||||
* Example usage:
|
||||
* <pre>
|
||||
* var vsm = new goog.dom.ViewportSizeMonitor();
|
||||
* goog.events.listen(vsm, goog.events.EventType.RESIZE, function(e) {
|
||||
* alert('Viewport size changed to ' + vsm.getSize());
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* Manually verified on IE6, IE7, FF2, Opera 11, Safari 4 and Chrome.
|
||||
*
|
||||
* @param {Window=} opt_window The window to monitor; defaults to the window in
|
||||
* which this code is executing.
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor = function(opt_window) {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
// Default the window to the current window if unspecified.
|
||||
this.window_ = opt_window || window;
|
||||
|
||||
// Listen for window resize events.
|
||||
this.listenerKey_ = goog.events.listen(this.window_,
|
||||
goog.events.EventType.RESIZE, this.handleResize_, false, this);
|
||||
|
||||
// Set the initial size.
|
||||
this.size_ = goog.dom.getViewportSize(this.window_);
|
||||
};
|
||||
goog.inherits(goog.dom.ViewportSizeMonitor, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a viewport size monitor for the given window. A new one is created
|
||||
* if it doesn't exist already. This prevents the unnecessary creation of
|
||||
* multiple spooling monitors for a window.
|
||||
* @param {Window=} opt_window The window to monitor; defaults to the window in
|
||||
* which this code is executing.
|
||||
* @return {goog.dom.ViewportSizeMonitor} Monitor for the given window.
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.getInstanceForWindow = function(opt_window) {
|
||||
var currentWindow = opt_window || window;
|
||||
var uid = goog.getUid(currentWindow);
|
||||
|
||||
return goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid] =
|
||||
goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid] ||
|
||||
new goog.dom.ViewportSizeMonitor(currentWindow);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes and disposes a viewport size monitor for the given window if one
|
||||
* exists.
|
||||
* @param {Window=} opt_window The window whose monitor should be removed;
|
||||
* defaults to the window in which this code is executing.
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.removeInstanceForWindow = function(opt_window) {
|
||||
var uid = goog.getUid(opt_window || window);
|
||||
|
||||
goog.dispose(goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid]);
|
||||
delete goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Map of window hash code to viewport size monitor for that window, if
|
||||
* created.
|
||||
* @type {Object.<number,goog.dom.ViewportSizeMonitor>}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.windowInstanceMap_ = {};
|
||||
|
||||
|
||||
/**
|
||||
* Event listener key for window the window resize handler, as returned by
|
||||
* {@link goog.events.listen}.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.prototype.listenerKey_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The window to monitor. Defaults to the window in which the code is running.
|
||||
* @type {Window}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.prototype.window_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The most recently recorded size of the viewport, in pixels.
|
||||
* @type {goog.math.Size?}
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.prototype.size_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the most recently recorded size of the viewport, in pixels. May
|
||||
* return null if no window resize event has been handled yet.
|
||||
* @return {goog.math.Size} The viewport dimensions, in pixels.
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.prototype.getSize = function() {
|
||||
// Return a clone instead of the original to preserve encapsulation.
|
||||
return this.size_ ? this.size_.clone() : null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.dom.ViewportSizeMonitor.prototype.disposeInternal = function() {
|
||||
goog.dom.ViewportSizeMonitor.superClass_.disposeInternal.call(this);
|
||||
|
||||
if (this.listenerKey_) {
|
||||
goog.events.unlistenByKey(this.listenerKey_);
|
||||
this.listenerKey_ = null;
|
||||
}
|
||||
|
||||
this.window_ = null;
|
||||
this.size_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles window resize events by measuring the dimensions of the
|
||||
* viewport and dispatching a {@link goog.events.EventType.RESIZE} event if the
|
||||
* current dimensions are different from the previous ones.
|
||||
* @param {goog.events.Event} event The window resize event to handle.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.ViewportSizeMonitor.prototype.handleResize_ = function(event) {
|
||||
var size = goog.dom.getViewportSize(this.window_);
|
||||
if (!goog.math.Size.equals(size, this.size_)) {
|
||||
this.size_ = size;
|
||||
this.dispatchEvent(goog.events.EventType.RESIZE);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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
|
||||
* XML utilities.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.dom.xml');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
|
||||
|
||||
/**
|
||||
* Max XML size for MSXML2. Used to prevent potential DoS attacks.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.dom.xml.MAX_XML_SIZE_KB = 2 * 1024; // In kB
|
||||
|
||||
|
||||
/**
|
||||
* Max XML size for MSXML2. Used to prevent potential DoS attacks.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.dom.xml.MAX_ELEMENT_DEPTH = 256; // Same default as MSXML6.
|
||||
|
||||
|
||||
/**
|
||||
* Creates an XML document appropriate for the current JS runtime
|
||||
* @param {string=} opt_rootTagName The root tag name.
|
||||
* @param {string=} opt_namespaceUri Namespace URI of the document element.
|
||||
* @return {Document} The new document.
|
||||
*/
|
||||
goog.dom.xml.createDocument = function(opt_rootTagName, opt_namespaceUri) {
|
||||
if (opt_namespaceUri && !opt_rootTagName) {
|
||||
throw Error("Can't create document with namespace and no root tag");
|
||||
}
|
||||
if (document.implementation && document.implementation.createDocument) {
|
||||
return document.implementation.createDocument(opt_namespaceUri || '',
|
||||
opt_rootTagName || '',
|
||||
null);
|
||||
} else if (typeof ActiveXObject != 'undefined') {
|
||||
var doc = goog.dom.xml.createMsXmlDocument_();
|
||||
if (doc) {
|
||||
if (opt_rootTagName) {
|
||||
doc.appendChild(doc.createNode(goog.dom.NodeType.ELEMENT,
|
||||
opt_rootTagName,
|
||||
opt_namespaceUri || ''));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
throw Error('Your browser does not support creating new documents');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates an XML document from a string
|
||||
* @param {string} xml The text.
|
||||
* @return {Document} XML document from the text.
|
||||
*/
|
||||
goog.dom.xml.loadXml = function(xml) {
|
||||
if (typeof DOMParser != 'undefined') {
|
||||
return new DOMParser().parseFromString(xml, 'application/xml');
|
||||
} else if (typeof ActiveXObject != 'undefined') {
|
||||
var doc = goog.dom.xml.createMsXmlDocument_();
|
||||
doc.loadXML(xml);
|
||||
return doc;
|
||||
}
|
||||
throw Error('Your browser does not support loading xml documents');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes an XML document or subtree to string.
|
||||
* @param {Document|Element} xml The document or the root node of the subtree.
|
||||
* @return {string} The serialized XML.
|
||||
*/
|
||||
goog.dom.xml.serialize = function(xml) {
|
||||
// Compatible with Firefox, Opera and WebKit.
|
||||
if (typeof XMLSerializer != 'undefined') {
|
||||
return new XMLSerializer().serializeToString(xml);
|
||||
}
|
||||
// Compatible with Internet Explorer.
|
||||
var text = xml.xml;
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
throw Error('Your browser does not support serializing XML documents');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Selects a single node using an Xpath expression and a root node
|
||||
* @param {Node} node The root node.
|
||||
* @param {string} path Xpath selector.
|
||||
* @return {Node} The selected node, or null if no matching node.
|
||||
*/
|
||||
goog.dom.xml.selectSingleNode = function(node, path) {
|
||||
if (typeof node.selectSingleNode != 'undefined') {
|
||||
var doc = goog.dom.getOwnerDocument(node);
|
||||
if (typeof doc.setProperty != 'undefined') {
|
||||
doc.setProperty('SelectionLanguage', 'XPath');
|
||||
}
|
||||
return node.selectSingleNode(path);
|
||||
} else if (document.implementation.hasFeature('XPath', '3.0')) {
|
||||
var doc = goog.dom.getOwnerDocument(node);
|
||||
var resolver = doc.createNSResolver(doc.documentElement);
|
||||
var result = doc.evaluate(path, node, resolver,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
||||
return result.singleNodeValue;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Selects multiple nodes using an Xpath expression and a root node
|
||||
* @param {Node} node The root node.
|
||||
* @param {string} path Xpath selector.
|
||||
* @return {(NodeList,Array.<Node>)} The selected nodes, or empty array if no
|
||||
* matching nodes.
|
||||
*/
|
||||
goog.dom.xml.selectNodes = function(node, path) {
|
||||
if (typeof node.selectNodes != 'undefined') {
|
||||
var doc = goog.dom.getOwnerDocument(node);
|
||||
if (typeof doc.setProperty != 'undefined') {
|
||||
doc.setProperty('SelectionLanguage', 'XPath');
|
||||
}
|
||||
return node.selectNodes(path);
|
||||
} else if (document.implementation.hasFeature('XPath', '3.0')) {
|
||||
var doc = goog.dom.getOwnerDocument(node);
|
||||
var resolver = doc.createNSResolver(doc.documentElement);
|
||||
var nodes = doc.evaluate(path, node, resolver,
|
||||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
||||
var results = [];
|
||||
var count = nodes.snapshotLength;
|
||||
for (var i = 0; i < count; i++) {
|
||||
results.push(nodes.snapshotItem(i));
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets multiple attributes on an element. Differs from goog.dom.setProperties
|
||||
* in that it exclusively uses the element's setAttributes method. Use this
|
||||
* when you need to ensure that the exact property is available as an attribute
|
||||
* and can be read later by the native getAttribute method.
|
||||
* @param {!Element} element XML or DOM element to set attributes on.
|
||||
* @param {!Object.<string, string>} attributes Map of property:value pairs.
|
||||
*/
|
||||
goog.dom.xml.setAttributes = function(element, attributes) {
|
||||
for (var key in attributes) {
|
||||
if (attributes.hasOwnProperty(key)) {
|
||||
element.setAttribute(key, attributes[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates an instance of the MSXML2.DOMDocument.
|
||||
* @return {Document} The new document.
|
||||
* @private
|
||||
*/
|
||||
goog.dom.xml.createMsXmlDocument_ = function() {
|
||||
var doc = new ActiveXObject('MSXML2.DOMDocument');
|
||||
if (doc) {
|
||||
// Prevent potential vulnerabilities exposed by MSXML2, see
|
||||
// http://b/1707300 and http://wiki/Main/ISETeamXMLAttacks for details.
|
||||
doc.resolveExternals = false;
|
||||
doc.validateOnParse = false;
|
||||
// Add a try catch block because accessing these properties will throw an
|
||||
// error on unsupported MSXML versions. This affects Windows machines
|
||||
// running IE6 or IE7 that are on XP SP2 or earlier without MSXML updates.
|
||||
// See http://msdn.microsoft.com/en-us/library/ms766391(VS.85).aspx for
|
||||
// specific details on which MSXML versions support these properties.
|
||||
try {
|
||||
doc.setProperty('ProhibitDTD', true);
|
||||
doc.setProperty('MaxXMLSize', goog.dom.xml.MAX_XML_SIZE_KB);
|
||||
doc.setProperty('MaxElementDepth', goog.dom.xml.MAX_ELEMENT_DEPTH);
|
||||
} catch (e) {
|
||||
// No-op.
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
Reference in New Issue
Block a user