Path: blob/trunk/third_party/closure/goog/dom/controlrange.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Utilities for working with IE control ranges.16*17* @author [email protected] (Robby Walker)18*/192021goog.provide('goog.dom.ControlRange');22goog.provide('goog.dom.ControlRangeIterator');2324goog.require('goog.array');25goog.require('goog.dom');26goog.require('goog.dom.AbstractMultiRange');27goog.require('goog.dom.AbstractRange');28goog.require('goog.dom.RangeIterator');29goog.require('goog.dom.RangeType');30goog.require('goog.dom.SavedRange');31goog.require('goog.dom.TagWalkType');32goog.require('goog.dom.TextRange');33goog.require('goog.iter.StopIteration');34goog.require('goog.userAgent');35363738/**39* Create a new control selection with no properties. Do not use this40* constructor: use one of the goog.dom.Range.createFrom* methods instead.41* @constructor42* @extends {goog.dom.AbstractMultiRange}43* @final44*/45goog.dom.ControlRange = function() {46/**47* The IE control range obejct.48* @private {Object}49*/50this.range_ = null;5152/**53* Cached list of elements.54* @private {Array<Element>}55*/56this.elements_ = null;5758/**59* Cached sorted list of elements.60* @private {Array<Element>}61*/62this.sortedElements_ = null;63};64goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange);656667/**68* Create a new range wrapper from the given browser range object. Do not use69* this method directly - please use goog.dom.Range.createFrom* instead.70* @param {Object} controlRange The browser range object.71* @return {!goog.dom.ControlRange} A range wrapper object.72*/73goog.dom.ControlRange.createFromBrowserRange = function(controlRange) {74var range = new goog.dom.ControlRange();75range.range_ = controlRange;76return range;77};787980/**81* Create a new range wrapper that selects the given element. Do not use82* this method directly - please use goog.dom.Range.createFrom* instead.83* @param {...Element} var_args The element(s) to select.84* @return {!goog.dom.ControlRange} A range wrapper object.85*/86goog.dom.ControlRange.createFromElements = function(var_args) {87var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange();88for (var i = 0, len = arguments.length; i < len; i++) {89range.addElement(arguments[i]);90}91return goog.dom.ControlRange.createFromBrowserRange(range);92};939495// Method implementations969798/**99* Clear cached values.100* @private101*/102goog.dom.ControlRange.prototype.clearCachedValues_ = function() {103this.elements_ = null;104this.sortedElements_ = null;105};106107108/** @override */109goog.dom.ControlRange.prototype.clone = function() {110return goog.dom.ControlRange.createFromElements.apply(111this, this.getElements());112};113114115/** @override */116goog.dom.ControlRange.prototype.getType = function() {117return goog.dom.RangeType.CONTROL;118};119120121/** @override */122goog.dom.ControlRange.prototype.getBrowserRangeObject = function() {123return this.range_ || document.body.createControlRange();124};125126127/** @override */128goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) {129if (!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {130return false;131}132this.range_ = nativeRange;133return true;134};135136137/** @override */138goog.dom.ControlRange.prototype.getTextRangeCount = function() {139return this.range_ ? this.range_.length : 0;140};141142143/** @override */144goog.dom.ControlRange.prototype.getTextRange = function(i) {145return goog.dom.TextRange.createFromNodeContents(this.range_.item(i));146};147148149/** @override */150goog.dom.ControlRange.prototype.getContainer = function() {151return goog.dom.findCommonAncestor.apply(null, this.getElements());152};153154155/** @override */156goog.dom.ControlRange.prototype.getStartNode = function() {157return this.getSortedElements()[0];158};159160161/** @override */162goog.dom.ControlRange.prototype.getStartOffset = function() {163return 0;164};165166167/** @override */168goog.dom.ControlRange.prototype.getEndNode = function() {169var sorted = this.getSortedElements();170var startsLast = /** @type {Node} */ (goog.array.peek(sorted));171return /** @type {Node} */ (goog.array.find(sorted, function(el) {172return goog.dom.contains(el, startsLast);173}));174};175176177/** @override */178goog.dom.ControlRange.prototype.getEndOffset = function() {179return this.getEndNode().childNodes.length;180};181182183// TODO(robbyw): Figure out how to unify getElements with TextRange API.184/**185* @return {!Array<Element>} Array of elements in the control range.186*/187goog.dom.ControlRange.prototype.getElements = function() {188if (!this.elements_) {189this.elements_ = [];190if (this.range_) {191for (var i = 0; i < this.range_.length; i++) {192this.elements_.push(this.range_.item(i));193}194}195}196197return this.elements_;198};199200201/**202* @return {!Array<Element>} Array of elements comprising the control range,203* sorted by document order.204*/205goog.dom.ControlRange.prototype.getSortedElements = function() {206if (!this.sortedElements_) {207this.sortedElements_ = this.getElements().concat();208this.sortedElements_.sort(function(a, b) {209return a.sourceIndex - b.sourceIndex;210});211}212213return this.sortedElements_;214};215216217/** @override */218goog.dom.ControlRange.prototype.isRangeInDocument = function() {219var returnValue = false;220221try {222returnValue = goog.array.every(this.getElements(), function(element) {223// On IE, this throws an exception when the range is detached.224return goog.userAgent.IE ?225!!element.parentNode :226goog.dom.contains(element.ownerDocument.body, element);227});228} catch (e) {229// IE sometimes throws Invalid Argument errors for detached elements.230// Note: trying to return a value from the above try block can cause IE231// to crash. It is necessary to use the local returnValue.232}233234return returnValue;235};236237238/** @override */239goog.dom.ControlRange.prototype.isCollapsed = function() {240return !this.range_ || !this.range_.length;241};242243244/** @override */245goog.dom.ControlRange.prototype.getText = function() {246// TODO(robbyw): What about for table selections? Should those have text?247return '';248};249250251/** @override */252goog.dom.ControlRange.prototype.getHtmlFragment = function() {253return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml)254.join('');255};256257258/** @override */259goog.dom.ControlRange.prototype.getValidHtml = function() {260return this.getHtmlFragment();261};262263264/** @override */265goog.dom.ControlRange.prototype.getPastableHtml =266goog.dom.ControlRange.prototype.getValidHtml;267268269/** @override */270goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) {271return new goog.dom.ControlRangeIterator(this);272};273274275// RANGE ACTIONS276277278/** @override */279goog.dom.ControlRange.prototype.select = function() {280if (this.range_) {281this.range_.select();282}283};284285286/** @override */287goog.dom.ControlRange.prototype.removeContents = function() {288// TODO(robbyw): Test implementing with execCommand('Delete')289if (this.range_) {290var nodes = [];291for (var i = 0, len = this.range_.length; i < len; i++) {292nodes.push(this.range_.item(i));293}294goog.array.forEach(nodes, goog.dom.removeNode);295296this.collapse(false);297}298};299300301/** @override */302goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) {303// Control selections have to have the node inserted before removing the304// selection contents because a collapsed control range doesn't have start or305// end nodes.306var result = this.insertNode(node, true);307308if (!this.isCollapsed()) {309this.removeContents();310}311312return result;313};314315316// SAVE/RESTORE317318319/** @override */320goog.dom.ControlRange.prototype.saveUsingDom = function() {321return new goog.dom.DomSavedControlRange_(this);322};323324325// RANGE MODIFICATION326327328/** @override */329goog.dom.ControlRange.prototype.collapse = function(toAnchor) {330// TODO(robbyw): Should this return a text range? If so, API needs to change.331this.range_ = null;332this.clearCachedValues_();333};334335336// SAVED RANGE OBJECTS337338339340/**341* A SavedRange implementation using DOM endpoints.342* @param {goog.dom.ControlRange} range The range to save.343* @constructor344* @extends {goog.dom.SavedRange}345* @private346*/347goog.dom.DomSavedControlRange_ = function(range) {348/**349* The element list.350* @type {Array<Element>}351* @private352*/353this.elements_ = range.getElements();354};355goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange);356357358/** @override */359goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() {360var doc = this.elements_.length ?361goog.dom.getOwnerDocument(this.elements_[0]) :362document;363var controlRange = doc.body.createControlRange();364for (var i = 0, len = this.elements_.length; i < len; i++) {365controlRange.addElement(this.elements_[i]);366}367return goog.dom.ControlRange.createFromBrowserRange(controlRange);368};369370371/** @override */372goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() {373goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this);374delete this.elements_;375};376377378// RANGE ITERATION379380381382/**383* Subclass of goog.dom.TagIterator that iterates over a DOM range. It384* adds functions to determine the portion of each text node that is selected.385*386* @param {goog.dom.ControlRange?} range The range to traverse.387* @constructor388* @extends {goog.dom.RangeIterator}389* @final390*/391goog.dom.ControlRangeIterator = function(range) {392/**393* The first node in the selection.394* @private {Node}395*/396this.startNode_ = null;397398/**399* The last node in the selection.400* @private {Node}401*/402this.endNode_ = null;403404/**405* The list of elements left to traverse.406* @private {Array<Element>?}407*/408this.elements_ = null;409410if (range) {411this.elements_ = range.getSortedElements();412this.startNode_ = this.elements_.shift();413this.endNode_ = /** @type {Node} */ (goog.array.peek(this.elements_)) ||414this.startNode_;415}416417goog.dom.ControlRangeIterator.base(418this, 'constructor', this.startNode_, false);419};420goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator);421422423/** @override */424goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() {425return 0;426};427428429/** @override */430goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() {431return 0;432};433434435/** @override */436goog.dom.ControlRangeIterator.prototype.getStartNode = function() {437return this.startNode_;438};439440441/** @override */442goog.dom.ControlRangeIterator.prototype.getEndNode = function() {443return this.endNode_;444};445446447/** @override */448goog.dom.ControlRangeIterator.prototype.isLast = function() {449return !this.depth && !this.elements_.length;450};451452453/**454* Move to the next position in the selection.455* Throws {@code goog.iter.StopIteration} when it passes the end of the range.456* @return {Node} The node at the next position.457* @override458*/459goog.dom.ControlRangeIterator.prototype.next = function() {460// Iterate over each element in the range, and all of its children.461if (this.isLast()) {462throw goog.iter.StopIteration;463} else if (!this.depth) {464var el = this.elements_.shift();465this.setPosition(466el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG);467return el;468}469470// Call the super function.471return goog.dom.ControlRangeIterator.superClass_.next.call(this);472};473474475/** @override */476goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) {477this.elements_ = other.elements_;478this.startNode_ = other.startNode_;479this.endNode_ = other.endNode_;480481goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other);482};483484485/**486* @return {!goog.dom.ControlRangeIterator} An identical iterator.487* @override488*/489goog.dom.ControlRangeIterator.prototype.clone = function() {490var copy = new goog.dom.ControlRangeIterator(null);491copy.copyFrom(this);492return copy;493};494495496