Path: blob/trunk/third_party/closure/goog/dom/multirange.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 W3C multi-part ranges.16*17* @author [email protected] (Robby Walker)18*/192021goog.provide('goog.dom.MultiRange');22goog.provide('goog.dom.MultiRangeIterator');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.TextRange');32goog.require('goog.iter');33goog.require('goog.iter.StopIteration');34goog.require('goog.log');35363738/**39* Creates a new multi part range 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.MultiRange = function() {46/**47* Logging object.48* @private {goog.log.Logger}49*/50this.logger_ = goog.log.getLogger('goog.dom.MultiRange');5152/**53* Array of browser sub-ranges comprising this multi-range.54* @private {Array<Range>}55*/56this.browserRanges_ = [];5758/**59* Lazily initialized array of range objects comprising this multi-range.60* @private {Array<goog.dom.TextRange>}61*/62this.ranges_ = [];6364/**65* Lazily computed sorted version of ranges_, sorted by start point.66* @private {Array<goog.dom.TextRange>?}67*/68this.sortedRanges_ = null;6970/**71* Lazily computed container node.72* @private {Node}73*/74this.container_ = null;75};76goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange);777879/**80* Creates a new range wrapper from the given browser selection object. Do not81* use this method directly - please use goog.dom.Range.createFrom* instead.82* @param {Selection} selection The browser selection object.83* @return {!goog.dom.MultiRange} A range wrapper object.84*/85goog.dom.MultiRange.createFromBrowserSelection = function(selection) {86var range = new goog.dom.MultiRange();87for (var i = 0, len = selection.rangeCount; i < len; i++) {88range.browserRanges_.push(selection.getRangeAt(i));89}90return range;91};929394/**95* Creates a new range wrapper from the given browser ranges. Do not96* use this method directly - please use goog.dom.Range.createFrom* instead.97* @param {Array<Range>} browserRanges The browser ranges.98* @return {!goog.dom.MultiRange} A range wrapper object.99*/100goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) {101var range = new goog.dom.MultiRange();102range.browserRanges_ = goog.array.clone(browserRanges);103return range;104};105106107/**108* Creates a new range wrapper from the given goog.dom.TextRange objects. Do109* not use this method directly - please use goog.dom.Range.createFrom* instead.110* @param {Array<goog.dom.TextRange>} textRanges The text range objects.111* @return {!goog.dom.MultiRange} A range wrapper object.112*/113goog.dom.MultiRange.createFromTextRanges = function(textRanges) {114var range = new goog.dom.MultiRange();115range.ranges_ = textRanges;116range.browserRanges_ = goog.array.map(117textRanges, function(range) { return range.getBrowserRangeObject(); });118return range;119};120121122// Method implementations123124125/**126* Clears cached values. Should be called whenever this.browserRanges_ is127* modified.128* @private129*/130goog.dom.MultiRange.prototype.clearCachedValues_ = function() {131this.ranges_ = [];132this.sortedRanges_ = null;133this.container_ = null;134};135136137/**138* @return {!goog.dom.MultiRange} A clone of this range.139* @override140*/141goog.dom.MultiRange.prototype.clone = function() {142return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_);143};144145146/** @override */147goog.dom.MultiRange.prototype.getType = function() {148return goog.dom.RangeType.MULTI;149};150151152/** @override */153goog.dom.MultiRange.prototype.getBrowserRangeObject = function() {154// NOTE(robbyw): This method does not make sense for multi-ranges.155if (this.browserRanges_.length > 1) {156goog.log.warning(157this.logger_,158'getBrowserRangeObject called on MultiRange with more than 1 range');159}160return this.browserRanges_[0];161};162163164/** @override */165goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) {166// TODO(robbyw): Look in to adding setBrowserSelectionObject.167return false;168};169170171/** @override */172goog.dom.MultiRange.prototype.getTextRangeCount = function() {173return this.browserRanges_.length;174};175176177/** @override */178goog.dom.MultiRange.prototype.getTextRange = function(i) {179if (!this.ranges_[i]) {180this.ranges_[i] =181goog.dom.TextRange.createFromBrowserRange(this.browserRanges_[i]);182}183return this.ranges_[i];184};185186187/** @override */188goog.dom.MultiRange.prototype.getContainer = function() {189if (!this.container_) {190var nodes = [];191for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {192nodes.push(this.getTextRange(i).getContainer());193}194this.container_ = goog.dom.findCommonAncestor.apply(null, nodes);195}196return this.container_;197};198199200/**201* @return {!Array<goog.dom.TextRange>} An array of sub-ranges, sorted by start202* point.203*/204goog.dom.MultiRange.prototype.getSortedRanges = function() {205if (!this.sortedRanges_) {206this.sortedRanges_ = this.getTextRanges();207this.sortedRanges_.sort(function(a, b) {208var aStartNode = a.getStartNode();209var aStartOffset = a.getStartOffset();210var bStartNode = b.getStartNode();211var bStartOffset = b.getStartOffset();212213if (aStartNode == bStartNode && aStartOffset == bStartOffset) {214return 0;215}216217/**218* @suppress {missingRequire} Cannot depend on goog.dom.Range because219* it creates a circular dependency.220*/221return goog.dom.Range.isReversed(222aStartNode, aStartOffset, bStartNode, bStartOffset) ?2231 :224-1;225});226}227return this.sortedRanges_;228};229230231/** @override */232goog.dom.MultiRange.prototype.getStartNode = function() {233return this.getSortedRanges()[0].getStartNode();234};235236237/** @override */238goog.dom.MultiRange.prototype.getStartOffset = function() {239return this.getSortedRanges()[0].getStartOffset();240};241242243/** @override */244goog.dom.MultiRange.prototype.getEndNode = function() {245// NOTE(robbyw): This may return the wrong node if any subranges overlap.246return goog.array.peek(this.getSortedRanges()).getEndNode();247};248249250/** @override */251goog.dom.MultiRange.prototype.getEndOffset = function() {252// NOTE(robbyw): This may return the wrong value if any subranges overlap.253return goog.array.peek(this.getSortedRanges()).getEndOffset();254};255256257/** @override */258goog.dom.MultiRange.prototype.isRangeInDocument = function() {259return goog.array.every(this.getTextRanges(), function(range) {260return range.isRangeInDocument();261});262};263264265/** @override */266goog.dom.MultiRange.prototype.isCollapsed = function() {267return this.browserRanges_.length == 0 ||268this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed();269};270271272/** @override */273goog.dom.MultiRange.prototype.getText = function() {274return goog.array275.map(this.getTextRanges(), function(range) { return range.getText(); })276.join('');277};278279280/** @override */281goog.dom.MultiRange.prototype.getHtmlFragment = function() {282return this.getValidHtml();283};284285286/** @override */287goog.dom.MultiRange.prototype.getValidHtml = function() {288// NOTE(robbyw): This does not behave well if the sub-ranges overlap.289return goog.array290.map(291this.getTextRanges(),292function(range) { return range.getValidHtml(); })293.join('');294};295296297/** @override */298goog.dom.MultiRange.prototype.getPastableHtml = function() {299// TODO(robbyw): This should probably do something smart like group TR and TD300// selections in to the same table.301return this.getValidHtml();302};303304305/** @override */306goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) {307return new goog.dom.MultiRangeIterator(this);308};309310311// RANGE ACTIONS312313314/** @override */315goog.dom.MultiRange.prototype.select = function() {316var selection =317goog.dom.AbstractRange.getBrowserSelectionForWindow(this.getWindow());318selection.removeAllRanges();319for (var i = 0, len = this.getTextRangeCount(); i < len; i++) {320selection.addRange(this.getTextRange(i).getBrowserRangeObject());321}322};323324325/** @override */326goog.dom.MultiRange.prototype.removeContents = function() {327goog.array.forEach(328this.getTextRanges(), function(range) { range.removeContents(); });329};330331332// SAVE/RESTORE333334335/** @override */336goog.dom.MultiRange.prototype.saveUsingDom = function() {337return new goog.dom.DomSavedMultiRange_(this);338};339340341// RANGE MODIFICATION342343344/**345* Collapses this range to a single point, either the first or last point346* depending on the parameter. This will result in the number of ranges in this347* multi range becoming 1.348* @param {boolean} toAnchor Whether to collapse to the anchor.349* @override350*/351goog.dom.MultiRange.prototype.collapse = function(toAnchor) {352if (!this.isCollapsed()) {353var range = toAnchor ? this.getTextRange(0) :354this.getTextRange(this.getTextRangeCount() - 1);355356this.clearCachedValues_();357range.collapse(toAnchor);358this.ranges_ = [range];359this.sortedRanges_ = [range];360this.browserRanges_ = [range.getBrowserRangeObject()];361}362};363364365// SAVED RANGE OBJECTS366367368369/**370* A SavedRange implementation using DOM endpoints.371* @param {goog.dom.MultiRange} range The range to save.372* @constructor373* @extends {goog.dom.SavedRange}374* @private375*/376goog.dom.DomSavedMultiRange_ = function(range) {377/**378* Array of saved ranges.379* @type {Array<goog.dom.SavedRange>}380* @private381*/382this.savedRanges_ = goog.array.map(383range.getTextRanges(), function(range) { return range.saveUsingDom(); });384};385goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange);386387388/**389* @return {!goog.dom.MultiRange} The restored range.390* @override391*/392goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() {393var ranges = goog.array.map(394this.savedRanges_, function(savedRange) { return savedRange.restore(); });395return goog.dom.MultiRange.createFromTextRanges(ranges);396};397398399/** @override */400goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() {401goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this);402403goog.array.forEach(404this.savedRanges_, function(savedRange) { savedRange.dispose(); });405delete this.savedRanges_;406};407408409// RANGE ITERATION410411412413/**414* Subclass of goog.dom.TagIterator that iterates over a DOM range. It415* adds functions to determine the portion of each text node that is selected.416*417* @param {goog.dom.MultiRange} range The range to traverse.418* @constructor419* @extends {goog.dom.RangeIterator}420* @final421*/422goog.dom.MultiRangeIterator = function(range) {423/**424* The list of range iterators left to traverse.425* @private {Array<goog.dom.RangeIterator>}426*/427this.iterators_ = null;428429/**430* The index of the current sub-iterator being traversed.431* @private {number}432*/433this.currentIdx_ = 0;434435if (range) {436this.iterators_ = goog.array.map(range.getSortedRanges(), function(r) {437return goog.iter.toIterator(r);438});439}440441goog.dom.MultiRangeIterator.base(442this, 'constructor', range ? this.getStartNode() : null, false);443};444goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator);445446447/** @override */448goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() {449return this.iterators_[this.currentIdx_].getStartTextOffset();450};451452453/** @override */454goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() {455return this.iterators_[this.currentIdx_].getEndTextOffset();456};457458459/** @override */460goog.dom.MultiRangeIterator.prototype.getStartNode = function() {461return this.iterators_[0].getStartNode();462};463464465/** @override */466goog.dom.MultiRangeIterator.prototype.getEndNode = function() {467return goog.array.peek(this.iterators_).getEndNode();468};469470471/** @override */472goog.dom.MultiRangeIterator.prototype.isLast = function() {473return this.iterators_[this.currentIdx_].isLast();474};475476477/** @override */478goog.dom.MultiRangeIterator.prototype.next = function() {479480try {481var it = this.iterators_[this.currentIdx_];482var next = it.next();483this.setPosition(it.node, it.tagType, it.depth);484return next;485} catch (ex) {486if (ex !== goog.iter.StopIteration ||487this.iterators_.length - 1 == this.currentIdx_) {488throw ex;489} else {490// In case we got a StopIteration, increment counter and try again.491this.currentIdx_++;492return this.next();493}494}495};496497498/** @override */499goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) {500this.iterators_ = goog.array.clone(other.iterators_);501goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other);502};503504505/**506* @return {!goog.dom.MultiRangeIterator} An identical iterator.507* @override508*/509goog.dom.MultiRangeIterator.prototype.clone = function() {510var copy = new goog.dom.MultiRangeIterator(null);511copy.copyFrom(this);512return copy;513};514515516