Path: blob/trunk/third_party/closure/goog/editor/range.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 Utilties for working with ranges.16*17* @author [email protected] (Nick Santos)18*/1920goog.provide('goog.editor.range');21goog.provide('goog.editor.range.Point');2223goog.require('goog.array');24goog.require('goog.dom');25goog.require('goog.dom.NodeType');26goog.require('goog.dom.Range');27goog.require('goog.dom.RangeEndpoint');28goog.require('goog.dom.SavedCaretRange');29goog.require('goog.editor.node');30goog.require('goog.editor.style');31goog.require('goog.iter');32goog.require('goog.userAgent');333435/**36* Given a range and an element, create a narrower range that is limited to the37* boundaries of the element. If the range starts (or ends) outside the38* element, the narrowed range's start point (or end point) will be the39* leftmost (or rightmost) leaf of the element.40* @param {goog.dom.AbstractRange} range The range.41* @param {Element} el The element to limit the range to.42* @return {goog.dom.AbstractRange} A new narrowed range, or null if the43* element does not contain any part of the given range.44*/45goog.editor.range.narrow = function(range, el) {46var startContainer = range.getStartNode();47var endContainer = range.getEndNode();4849if (startContainer && endContainer) {50var isElement = function(node) { return node == el; };51var hasStart = goog.dom.getAncestor(startContainer, isElement, true);52var hasEnd = goog.dom.getAncestor(endContainer, isElement, true);5354if (hasStart && hasEnd) {55// The range is contained entirely within this element.56return range.clone();57} else if (hasStart) {58// The range starts inside the element, but ends outside it.59var leaf = goog.editor.node.getRightMostLeaf(el);60return goog.dom.Range.createFromNodes(61range.getStartNode(), range.getStartOffset(), leaf,62goog.editor.node.getLength(leaf));63} else if (hasEnd) {64// The range starts outside the element, but ends inside it.65return goog.dom.Range.createFromNodes(66goog.editor.node.getLeftMostLeaf(el), 0, range.getEndNode(),67range.getEndOffset());68}69}7071// The selection starts and ends outside the element.72return null;73};747576/**77* Given a range, expand the range to include outer tags if the full contents of78* those tags are entirely selected. This essentially changes the dom position,79* but not the visible position of the range.80* Ex. <code><li>foo</li></code> if "foo" is selected, instead of returning81* start and end nodes as the foo text node, return the li.82* @param {goog.dom.AbstractRange} range The range.83* @param {Node=} opt_stopNode Optional node to stop expanding past.84* @return {!goog.dom.AbstractRange} The expanded range.85*/86goog.editor.range.expand = function(range, opt_stopNode) {87// Expand the start out to the common container.88var expandedRange = goog.editor.range.expandEndPointToContainer_(89range, goog.dom.RangeEndpoint.START, opt_stopNode);90// Expand the end out to the common container.91expandedRange = goog.editor.range.expandEndPointToContainer_(92expandedRange, goog.dom.RangeEndpoint.END, opt_stopNode);9394var startNode = expandedRange.getStartNode();95var endNode = expandedRange.getEndNode();96var startOffset = expandedRange.getStartOffset();97var endOffset = expandedRange.getEndOffset();9899// If we have reached a common container, now expand out.100if (startNode == endNode) {101while (endNode != opt_stopNode && startOffset == 0 &&102endOffset == goog.editor.node.getLength(endNode)) {103// Select the parent instead.104var parentNode = endNode.parentNode;105startOffset = goog.array.indexOf(parentNode.childNodes, endNode);106endOffset = startOffset + 1;107endNode = parentNode;108}109startNode = endNode;110}111112return goog.dom.Range.createFromNodes(113startNode, startOffset, endNode, endOffset);114};115116117/**118* Given a range, expands the start or end points as far out towards the119* range's common container (or stopNode, if provided) as possible, while120* perserving the same visible position.121*122* @param {goog.dom.AbstractRange} range The range to expand.123* @param {goog.dom.RangeEndpoint} endpoint The endpoint to expand.124* @param {Node=} opt_stopNode Optional node to stop expanding past.125* @return {!goog.dom.AbstractRange} The expanded range.126* @private127*/128goog.editor.range.expandEndPointToContainer_ = function(129range, endpoint, opt_stopNode) {130var expandStart = endpoint == goog.dom.RangeEndpoint.START;131var node = expandStart ? range.getStartNode() : range.getEndNode();132var offset = expandStart ? range.getStartOffset() : range.getEndOffset();133var container = range.getContainerElement();134135// Expand the node out until we reach the container or the stop node.136while (node != container && node != opt_stopNode) {137// It is only valid to expand the start if we are at the start of a node138// (offset 0) or expand the end if we are at the end of a node139// (offset length).140if (expandStart && offset != 0 ||141!expandStart && offset != goog.editor.node.getLength(node)) {142break;143}144145var parentNode = node.parentNode;146var index = goog.array.indexOf(parentNode.childNodes, node);147offset = expandStart ? index : index + 1;148node = parentNode;149}150151return goog.dom.Range.createFromNodes(152expandStart ? node : range.getStartNode(),153expandStart ? offset : range.getStartOffset(),154expandStart ? range.getEndNode() : node,155expandStart ? range.getEndOffset() : offset);156};157158159/**160* Cause the window's selection to be the start of this node.161* @param {Node} node The node to select the start of.162*/163goog.editor.range.selectNodeStart = function(node) {164goog.dom.Range.createCaret(goog.editor.node.getLeftMostLeaf(node), 0)165.select();166};167168169/**170* Position the cursor immediately to the left or right of "node".171* In Firefox, the selection parent is outside of "node", so the cursor can172* effectively be moved to the end of a link node, without being considered173* inside of it.174* Note: This does not always work in WebKit. In particular, if you try to175* place a cursor to the right of a link, typing still puts you in the link.176* Bug: http://bugs.webkit.org/show_bug.cgi?id=17697177* @param {Node} node The node to position the cursor relative to.178* @param {boolean} toLeft True to place it to the left, false to the right.179* @return {!goog.dom.AbstractRange} The newly selected range.180*/181goog.editor.range.placeCursorNextTo = function(node, toLeft) {182var parent = node.parentNode;183var offset = goog.array.indexOf(parent.childNodes, node) + (toLeft ? 0 : 1);184var point =185goog.editor.range.Point.createDeepestPoint(parent, offset, toLeft, true);186var range = goog.dom.Range.createCaret(point.node, point.offset);187range.select();188return range;189};190191192/**193* Normalizes the node, preserving the selection of the document.194*195* May also normalize things outside the node, if it is more efficient to do so.196*197* @param {Node} node The node to normalize.198*/199goog.editor.range.selectionPreservingNormalize = function(node) {200var doc = goog.dom.getOwnerDocument(node);201var selection = goog.dom.Range.createFromWindow(goog.dom.getWindow(doc));202var normalizedRange =203goog.editor.range.rangePreservingNormalize(node, selection);204if (normalizedRange) {205normalizedRange.select();206}207};208209210/**211* Manually normalizes the node in IE, since native normalize in IE causes212* transient problems.213* @param {Node} node The node to normalize.214* @private215*/216goog.editor.range.normalizeNodeIe_ = function(node) {217var lastText = null;218var child = node.firstChild;219while (child) {220var next = child.nextSibling;221if (child.nodeType == goog.dom.NodeType.TEXT) {222if (child.nodeValue == '') {223node.removeChild(child);224} else if (lastText) {225lastText.nodeValue += child.nodeValue;226node.removeChild(child);227} else {228lastText = child;229}230} else {231goog.editor.range.normalizeNodeIe_(child);232lastText = null;233}234child = next;235}236};237238239/**240* Normalizes the given node.241* @param {Node} node The node to normalize.242*/243goog.editor.range.normalizeNode = function(node) {244if (goog.userAgent.IE) {245goog.editor.range.normalizeNodeIe_(node);246} else {247node.normalize();248}249};250251252/**253* Normalizes the node, preserving a range of the document.254*255* May also normalize things outside the node, if it is more efficient to do so.256*257* @param {Node} node The node to normalize.258* @param {goog.dom.AbstractRange?} range The range to normalize.259* @return {goog.dom.AbstractRange?} The range, adjusted for normalization.260*/261goog.editor.range.rangePreservingNormalize = function(node, range) {262if (range) {263var rangeFactory = goog.editor.range.normalize(range);264// WebKit has broken selection affinity, so carets tend to jump out of the265// beginning of inline elements. This means that if we're doing the266// normalize as the result of a range that will later become the selection,267// we might not normalize something in the range after it is read back from268// the selection. We can't just normalize the parentNode here because WebKit269// can move the selection range out of multiple inline parents.270var container = goog.editor.style.getContainer(range.getContainerElement());271}272273if (container) {274goog.editor.range.normalizeNode(275goog.dom.findCommonAncestor(container, node));276} else if (node) {277goog.editor.range.normalizeNode(node);278}279280if (rangeFactory) {281return rangeFactory();282} else {283return null;284}285};286287288/**289* Get the deepest point in the DOM that's equivalent to the endpoint of the290* given range.291*292* @param {goog.dom.AbstractRange} range A range.293* @param {boolean} atStart True for the start point, false for the end point.294* @return {!goog.editor.range.Point} The end point, expressed as a node295* and an offset.296*/297goog.editor.range.getDeepEndPoint = function(range, atStart) {298return atStart ?299goog.editor.range.Point.createDeepestPoint(300range.getStartNode(), range.getStartOffset()) :301goog.editor.range.Point.createDeepestPoint(302range.getEndNode(), range.getEndOffset());303};304305306/**307* Given a range in the current DOM, create a factory for a range that308* represents the same selection in a normalized DOM. The factory function309* should be invoked after the DOM is normalized.310*311* All browsers do a bad job preserving ranges across DOM normalization.312* The issue is best described in this 5-year-old bug report:313* https://bugzilla.mozilla.org/show_bug.cgi?id=191864314* For most applications, this isn't a problem. The browsers do a good job315* handling un-normalized text, so there's usually no reason to normalize.316*317* The exception to this rule is the rich text editing commands318* execCommand and queryCommandValue, which will fail often if there are319* un-normalized text nodes.320*321* The factory function creates new ranges so that we can normalize the DOM322* without problems. It must be created before any normalization happens,323* and invoked after normalization happens.324*325* @param {goog.dom.AbstractRange} range The range to normalize. It may326* become invalid after body.normalize() is called.327* @return {function(): goog.dom.AbstractRange} A factory for a normalized328* range. Should be called after body.normalize() is called.329*/330goog.editor.range.normalize = function(range) {331var isReversed = range.isReversed();332var anchorPoint = goog.editor.range.normalizePoint_(333goog.editor.range.getDeepEndPoint(range, !isReversed));334var anchorParent = anchorPoint.getParentPoint();335var anchorPreviousSibling = anchorPoint.node.previousSibling;336if (anchorPoint.node.nodeType == goog.dom.NodeType.TEXT) {337anchorPoint.node = null;338}339340var focusPoint = goog.editor.range.normalizePoint_(341goog.editor.range.getDeepEndPoint(range, isReversed));342var focusParent = focusPoint.getParentPoint();343var focusPreviousSibling = focusPoint.node.previousSibling;344if (focusPoint.node.nodeType == goog.dom.NodeType.TEXT) {345focusPoint.node = null;346}347348return function() {349if (!anchorPoint.node && anchorPreviousSibling) {350// If anchorPoint.node was previously an empty text node with no siblings,351// anchorPreviousSibling may not have a nextSibling since that node will352// no longer exist. Do our best and point to the end of the previous353// element.354anchorPoint.node = anchorPreviousSibling.nextSibling;355if (!anchorPoint.node) {356anchorPoint =357goog.editor.range.Point.getPointAtEndOfNode(anchorPreviousSibling);358}359}360361if (!focusPoint.node && focusPreviousSibling) {362// If focusPoint.node was previously an empty text node with no siblings,363// focusPreviousSibling may not have a nextSibling since that node will no364// longer exist. Do our best and point to the end of the previous365// element.366focusPoint.node = focusPreviousSibling.nextSibling;367if (!focusPoint.node) {368focusPoint =369goog.editor.range.Point.getPointAtEndOfNode(focusPreviousSibling);370}371}372373return goog.dom.Range.createFromNodes(374anchorPoint.node || anchorParent.node.firstChild || anchorParent.node,375anchorPoint.offset,376focusPoint.node || focusParent.node.firstChild || focusParent.node,377focusPoint.offset);378};379};380381382/**383* Given a point in the current DOM, adjust it to represent the same point in384* a normalized DOM.385*386* See the comments on goog.editor.range.normalize for more context.387*388* @param {goog.editor.range.Point} point A point in the document.389* @return {!goog.editor.range.Point} The same point, for easy chaining.390* @private391*/392goog.editor.range.normalizePoint_ = function(point) {393var previous;394if (point.node.nodeType == goog.dom.NodeType.TEXT) {395// If the cursor position is in a text node,396// look at all the previous text siblings of the text node,397// and set the offset relative to the earliest text sibling.398for (var current = point.node.previousSibling;399current && current.nodeType == goog.dom.NodeType.TEXT;400current = current.previousSibling) {401point.offset += goog.editor.node.getLength(current);402}403404previous = current;405} else {406previous = point.node.previousSibling;407}408409var parent = point.node.parentNode;410point.node = previous ? previous.nextSibling : parent.firstChild;411return point;412};413414415/**416* Checks if a range is completely inside an editable region.417* @param {goog.dom.AbstractRange} range The range to test.418* @return {boolean} Whether the range is completely inside an editable region.419*/420goog.editor.range.isEditable = function(range) {421var rangeContainer = range.getContainerElement();422423// Closure's implementation of getContainerElement() is a little too424// smart in IE when exactly one element is contained in the range.425// It assumes that there's a user whose intent was actually to select426// all that element's children, so it returns the element itself as its427// own containing element.428// This little sanity check detects this condition so we can account for it.429var rangeContainerIsOutsideRange =430range.getStartNode() != rangeContainer.parentElement;431432return (rangeContainerIsOutsideRange &&433goog.editor.node.isEditableContainer(rangeContainer)) ||434goog.editor.node.isEditable(rangeContainer);435};436437438/**439* Returns whether the given range intersects with any instance of the given440* tag.441* @param {goog.dom.AbstractRange} range The range to check.442* @param {!goog.dom.TagName} tagName The name of the tag.443* @return {boolean} Whether the given range intersects with any instance of444* the given tag.445*/446goog.editor.range.intersectsTag = function(range, tagName) {447if (goog.dom.getAncestorByTagNameAndClass(448range.getContainerElement(), tagName)) {449return true;450}451452return goog.iter.some(453range, function(node) { return node.tagName == tagName; });454};455456457458/**459* One endpoint of a range, represented as a Node and and offset.460* @param {Node} node The node containing the point.461* @param {number} offset The offset of the point into the node.462* @constructor463* @final464*/465goog.editor.range.Point = function(node, offset) {466/**467* The node containing the point.468* @type {Node}469*/470this.node = node;471472/**473* The offset of the point into the node.474* @type {number}475*/476this.offset = offset;477};478479480/**481* Gets the point of this point's node in the DOM.482* @return {!goog.editor.range.Point} The node's point.483*/484goog.editor.range.Point.prototype.getParentPoint = function() {485var parent = this.node.parentNode;486return new goog.editor.range.Point(487parent, goog.array.indexOf(parent.childNodes, this.node));488};489490491/**492* Construct the deepest possible point in the DOM that's equivalent493* to the given point, expressed as a node and an offset.494* @param {Node} node The node containing the point.495* @param {number} offset The offset of the point from the node.496* @param {boolean=} opt_trendLeft Notice that a (node, offset) pair may be497* equivalent to more than one descendent (node, offset) pair in the DOM.498* By default, we trend rightward. If this parameter is true, then we499* trend leftward. The tendency to fall rightward by default is for500* consistency with other range APIs (like placeCursorNextTo).501* @param {boolean=} opt_stopOnChildlessElement If true, and we encounter502* a Node which is an Element that cannot have children, we return a Point503* based on its parent rather than that Node itself.504* @return {!goog.editor.range.Point} A new point.505*/506goog.editor.range.Point.createDeepestPoint = function(507node, offset, opt_trendLeft, opt_stopOnChildlessElement) {508while (node.nodeType == goog.dom.NodeType.ELEMENT) {509var child = node.childNodes[offset];510if (!child && !node.lastChild) {511break;512} else if (child) {513var prevSibling = child.previousSibling;514if (opt_trendLeft && prevSibling) {515if (opt_stopOnChildlessElement &&516goog.editor.range.Point.isTerminalElement_(prevSibling)) {517break;518}519node = prevSibling;520offset = goog.editor.node.getLength(node);521} else {522if (opt_stopOnChildlessElement &&523goog.editor.range.Point.isTerminalElement_(child)) {524break;525}526node = child;527offset = 0;528}529} else {530if (opt_stopOnChildlessElement &&531goog.editor.range.Point.isTerminalElement_(node.lastChild)) {532break;533}534node = node.lastChild;535offset = goog.editor.node.getLength(node);536}537}538539return new goog.editor.range.Point(node, offset);540};541542543/**544* Return true if the specified node is an Element that is not expected to have545* children. The createDeepestPoint() method should not traverse into546* such elements.547* @param {Node} node .548* @return {boolean} True if the node is an Element that does not contain549* child nodes (e.g. BR, IMG).550* @private551*/552goog.editor.range.Point.isTerminalElement_ = function(node) {553return (554node.nodeType == goog.dom.NodeType.ELEMENT &&555!goog.dom.canHaveChildren(node));556};557558559/**560* Construct a point at the very end of the given node.561* @param {Node} node The node to create a point for.562* @return {!goog.editor.range.Point} A new point.563*/564goog.editor.range.Point.getPointAtEndOfNode = function(node) {565return new goog.editor.range.Point(node, goog.editor.node.getLength(node));566};567568569/**570* Saves the range by inserting carets into the HTML.571*572* Unlike the regular saveUsingCarets, this SavedRange normalizes text nodes.573* Browsers have other bugs where they don't handle split text nodes in574* contentEditable regions right.575*576* @param {goog.dom.AbstractRange} range The abstract range object.577* @return {!goog.dom.SavedCaretRange} A saved caret range that normalizes578* text nodes.579*/580goog.editor.range.saveUsingNormalizedCarets = function(range) {581return new goog.editor.range.NormalizedCaretRange_(range);582};583584585586/**587* Saves the range using carets, but normalizes text nodes when carets588* are removed.589* @see goog.editor.range.saveUsingNormalizedCarets590* @param {goog.dom.AbstractRange} range The range being saved.591* @constructor592* @extends {goog.dom.SavedCaretRange}593* @private594*/595goog.editor.range.NormalizedCaretRange_ = function(range) {596goog.dom.SavedCaretRange.call(this, range);597};598goog.inherits(599goog.editor.range.NormalizedCaretRange_, goog.dom.SavedCaretRange);600601602/**603* Normalizes text nodes whenever carets are removed from the document.604* @param {goog.dom.AbstractRange=} opt_range A range whose offsets have already605* been adjusted for caret removal; it will be adjusted and returned if it606* is also affected by post-removal operations, such as text node607* normalization.608* @return {goog.dom.AbstractRange|undefined} The adjusted range, if opt_range609* was provided.610* @override611*/612goog.editor.range.NormalizedCaretRange_.prototype.removeCarets = function(613opt_range) {614var startCaret = this.getCaret(true);615var endCaret = this.getCaret(false);616var node = startCaret && endCaret ?617goog.dom.findCommonAncestor(startCaret, endCaret) :618startCaret || endCaret;619620goog.editor.range.NormalizedCaretRange_.superClass_.removeCarets.call(this);621622if (opt_range) {623return goog.editor.range.rangePreservingNormalize(node, opt_range);624} else if (node) {625goog.editor.range.selectionPreservingNormalize(node);626}627};628629630