All Downloads are FREE. Search and download functionalities are using the official Maven repository.

goog.editor.range.js Maven / Gradle / Ivy

// 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 Utilties for working with ranges.
 *
 * @author [email protected] (Nick Santos)
 */

goog.provide('goog.editor.range');
goog.provide('goog.editor.range.Point');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.RangeEndpoint');
goog.require('goog.dom.SavedCaretRange');
goog.require('goog.editor.node');
goog.require('goog.editor.style');
goog.require('goog.iter');
goog.require('goog.userAgent');


/**
 * Given a range and an element, create a narrower range that is limited to the
 * boundaries of the element. If the range starts (or ends) outside the
 * element, the narrowed range's start point (or end point) will be the
 * leftmost (or rightmost) leaf of the element.
 * @param {goog.dom.AbstractRange} range The range.
 * @param {Element} el The element to limit the range to.
 * @return {goog.dom.AbstractRange} A new narrowed range, or null if the
 *     element does not contain any part of the given range.
 */
goog.editor.range.narrow = function(range, el) {
  var startContainer = range.getStartNode();
  var endContainer = range.getEndNode();

  if (startContainer && endContainer) {
    var isElement = function(node) { return node == el; };
    var hasStart = goog.dom.getAncestor(startContainer, isElement, true);
    var hasEnd = goog.dom.getAncestor(endContainer, isElement, true);

    if (hasStart && hasEnd) {
      // The range is contained entirely within this element.
      return range.clone();
    } else if (hasStart) {
      // The range starts inside the element, but ends outside it.
      var leaf = goog.editor.node.getRightMostLeaf(el);
      return goog.dom.Range.createFromNodes(
          range.getStartNode(), range.getStartOffset(), leaf,
          goog.editor.node.getLength(leaf));
    } else if (hasEnd) {
      // The range starts outside the element, but ends inside it.
      return goog.dom.Range.createFromNodes(
          goog.editor.node.getLeftMostLeaf(el), 0, range.getEndNode(),
          range.getEndOffset());
    }
  }

  // The selection starts and ends outside the element.
  return null;
};


/**
 * Given a range, expand the range to include outer tags if the full contents of
 * those tags are entirely selected.  This essentially changes the dom position,
 * but not the visible position of the range.
 * Ex. 
  • foo
  • if "foo" is selected, instead of returning * start and end nodes as the foo text node, return the li. * @param {goog.dom.AbstractRange} range The range. * @param {Node=} opt_stopNode Optional node to stop expanding past. * @return {!goog.dom.AbstractRange} The expanded range. */ goog.editor.range.expand = function(range, opt_stopNode) { // Expand the start out to the common container. var expandedRange = goog.editor.range.expandEndPointToContainer_( range, goog.dom.RangeEndpoint.START, opt_stopNode); // Expand the end out to the common container. expandedRange = goog.editor.range.expandEndPointToContainer_( expandedRange, goog.dom.RangeEndpoint.END, opt_stopNode); var startNode = expandedRange.getStartNode(); var endNode = expandedRange.getEndNode(); var startOffset = expandedRange.getStartOffset(); var endOffset = expandedRange.getEndOffset(); // If we have reached a common container, now expand out. if (startNode == endNode) { while (endNode != opt_stopNode && startOffset == 0 && endOffset == goog.editor.node.getLength(endNode)) { // Select the parent instead. var parentNode = endNode.parentNode; startOffset = goog.array.indexOf(parentNode.childNodes, endNode); endOffset = startOffset + 1; endNode = parentNode; } startNode = endNode; } return goog.dom.Range.createFromNodes( startNode, startOffset, endNode, endOffset); }; /** * Given a range, expands the start or end points as far out towards the * range's common container (or stopNode, if provided) as possible, while * perserving the same visible position. * * @param {goog.dom.AbstractRange} range The range to expand. * @param {goog.dom.RangeEndpoint} endpoint The endpoint to expand. * @param {Node=} opt_stopNode Optional node to stop expanding past. * @return {!goog.dom.AbstractRange} The expanded range. * @private */ goog.editor.range.expandEndPointToContainer_ = function( range, endpoint, opt_stopNode) { var expandStart = endpoint == goog.dom.RangeEndpoint.START; var node = expandStart ? range.getStartNode() : range.getEndNode(); var offset = expandStart ? range.getStartOffset() : range.getEndOffset(); var container = range.getContainerElement(); // Expand the node out until we reach the container or the stop node. while (node != container && node != opt_stopNode) { // It is only valid to expand the start if we are at the start of a node // (offset 0) or expand the end if we are at the end of a node // (offset length). if (expandStart && offset != 0 || !expandStart && offset != goog.editor.node.getLength(node)) { break; } var parentNode = node.parentNode; var index = goog.array.indexOf(parentNode.childNodes, node); offset = expandStart ? index : index + 1; node = parentNode; } return goog.dom.Range.createFromNodes( expandStart ? node : range.getStartNode(), expandStart ? offset : range.getStartOffset(), expandStart ? range.getEndNode() : node, expandStart ? range.getEndOffset() : offset); }; /** * Cause the window's selection to be the start of this node. * @param {Node} node The node to select the start of. */ goog.editor.range.selectNodeStart = function(node) { goog.dom.Range.createCaret(goog.editor.node.getLeftMostLeaf(node), 0) .select(); }; /** * Position the cursor immediately to the left or right of "node". * In Firefox, the selection parent is outside of "node", so the cursor can * effectively be moved to the end of a link node, without being considered * inside of it. * Note: This does not always work in WebKit. In particular, if you try to * place a cursor to the right of a link, typing still puts you in the link. * Bug: http://bugs.webkit.org/show_bug.cgi?id=17697 * @param {Node} node The node to position the cursor relative to. * @param {boolean} toLeft True to place it to the left, false to the right. * @return {!goog.dom.AbstractRange} The newly selected range. */ goog.editor.range.placeCursorNextTo = function(node, toLeft) { var parent = node.parentNode; var offset = goog.array.indexOf(parent.childNodes, node) + (toLeft ? 0 : 1); var point = goog.editor.range.Point.createDeepestPoint(parent, offset, toLeft, true); var range = goog.dom.Range.createCaret(point.node, point.offset); range.select(); return range; }; /** * Normalizes the node, preserving the selection of the document. * * May also normalize things outside the node, if it is more efficient to do so. * * @param {Node} node The node to normalize. */ goog.editor.range.selectionPreservingNormalize = function(node) { var doc = goog.dom.getOwnerDocument(node); var selection = goog.dom.Range.createFromWindow(goog.dom.getWindow(doc)); var normalizedRange = goog.editor.range.rangePreservingNormalize(node, selection); if (normalizedRange) { normalizedRange.select(); } }; /** * Manually normalizes the node in IE, since native normalize in IE causes * transient problems. * @param {Node} node The node to normalize. * @private */ goog.editor.range.normalizeNodeIe_ = function(node) { var lastText = null; var child = node.firstChild; while (child) { var next = child.nextSibling; if (child.nodeType == goog.dom.NodeType.TEXT) { if (child.nodeValue == '') { node.removeChild(child); } else if (lastText) { lastText.nodeValue += child.nodeValue; node.removeChild(child); } else { lastText = child; } } else { goog.editor.range.normalizeNodeIe_(child); lastText = null; } child = next; } }; /** * Normalizes the given node. * @param {Node} node The node to normalize. */ goog.editor.range.normalizeNode = function(node) { if (goog.userAgent.IE) { goog.editor.range.normalizeNodeIe_(node); } else { node.normalize(); } }; /** * Normalizes the node, preserving a range of the document. * * May also normalize things outside the node, if it is more efficient to do so. * * @param {Node} node The node to normalize. * @param {goog.dom.AbstractRange?} range The range to normalize. * @return {goog.dom.AbstractRange?} The range, adjusted for normalization. */ goog.editor.range.rangePreservingNormalize = function(node, range) { if (range) { var rangeFactory = goog.editor.range.normalize(range); // WebKit has broken selection affinity, so carets tend to jump out of the // beginning of inline elements. This means that if we're doing the // normalize as the result of a range that will later become the selection, // we might not normalize something in the range after it is read back from // the selection. We can't just normalize the parentNode here because WebKit // can move the selection range out of multiple inline parents. var container = goog.editor.style.getContainer(range.getContainerElement()); } if (container) { goog.editor.range.normalizeNode( goog.dom.findCommonAncestor(container, node)); } else if (node) { goog.editor.range.normalizeNode(node); } if (rangeFactory) { return rangeFactory(); } else { return null; } }; /** * Get the deepest point in the DOM that's equivalent to the endpoint of the * given range. * * @param {goog.dom.AbstractRange} range A range. * @param {boolean} atStart True for the start point, false for the end point. * @return {!goog.editor.range.Point} The end point, expressed as a node * and an offset. */ goog.editor.range.getDeepEndPoint = function(range, atStart) { return atStart ? goog.editor.range.Point.createDeepestPoint( range.getStartNode(), range.getStartOffset()) : goog.editor.range.Point.createDeepestPoint( range.getEndNode(), range.getEndOffset()); }; /** * Given a range in the current DOM, create a factory for a range that * represents the same selection in a normalized DOM. The factory function * should be invoked after the DOM is normalized. * * All browsers do a bad job preserving ranges across DOM normalization. * The issue is best described in this 5-year-old bug report: * https://bugzilla.mozilla.org/show_bug.cgi?id=191864 * For most applications, this isn't a problem. The browsers do a good job * handling un-normalized text, so there's usually no reason to normalize. * * The exception to this rule is the rich text editing commands * execCommand and queryCommandValue, which will fail often if there are * un-normalized text nodes. * * The factory function creates new ranges so that we can normalize the DOM * without problems. It must be created before any normalization happens, * and invoked after normalization happens. * * @param {goog.dom.AbstractRange} range The range to normalize. It may * become invalid after body.normalize() is called. * @return {function(): goog.dom.AbstractRange} A factory for a normalized * range. Should be called after body.normalize() is called. */ goog.editor.range.normalize = function(range) { var isReversed = range.isReversed(); var anchorPoint = goog.editor.range.normalizePoint_( goog.editor.range.getDeepEndPoint(range, !isReversed)); var anchorParent = anchorPoint.getParentPoint(); var anchorPreviousSibling = anchorPoint.node.previousSibling; if (anchorPoint.node.nodeType == goog.dom.NodeType.TEXT) { anchorPoint.node = null; } var focusPoint = goog.editor.range.normalizePoint_( goog.editor.range.getDeepEndPoint(range, isReversed)); var focusParent = focusPoint.getParentPoint(); var focusPreviousSibling = focusPoint.node.previousSibling; if (focusPoint.node.nodeType == goog.dom.NodeType.TEXT) { focusPoint.node = null; } return function() { if (!anchorPoint.node && anchorPreviousSibling) { // If anchorPoint.node was previously an empty text node with no siblings, // anchorPreviousSibling may not have a nextSibling since that node will // no longer exist. Do our best and point to the end of the previous // element. anchorPoint.node = anchorPreviousSibling.nextSibling; if (!anchorPoint.node) { anchorPoint = goog.editor.range.Point.getPointAtEndOfNode(anchorPreviousSibling); } } if (!focusPoint.node && focusPreviousSibling) { // If focusPoint.node was previously an empty text node with no siblings, // focusPreviousSibling may not have a nextSibling since that node will no // longer exist. Do our best and point to the end of the previous // element. focusPoint.node = focusPreviousSibling.nextSibling; if (!focusPoint.node) { focusPoint = goog.editor.range.Point.getPointAtEndOfNode(focusPreviousSibling); } } return goog.dom.Range.createFromNodes( anchorPoint.node || anchorParent.node.firstChild || anchorParent.node, anchorPoint.offset, focusPoint.node || focusParent.node.firstChild || focusParent.node, focusPoint.offset); }; }; /** * Given a point in the current DOM, adjust it to represent the same point in * a normalized DOM. * * See the comments on goog.editor.range.normalize for more context. * * @param {goog.editor.range.Point} point A point in the document. * @return {!goog.editor.range.Point} The same point, for easy chaining. * @private */ goog.editor.range.normalizePoint_ = function(point) { var previous; if (point.node.nodeType == goog.dom.NodeType.TEXT) { // If the cursor position is in a text node, // look at all the previous text siblings of the text node, // and set the offset relative to the earliest text sibling. for (var current = point.node.previousSibling; current && current.nodeType == goog.dom.NodeType.TEXT; current = current.previousSibling) { point.offset += goog.editor.node.getLength(current); } previous = current; } else { previous = point.node.previousSibling; } var parent = point.node.parentNode; point.node = previous ? previous.nextSibling : parent.firstChild; return point; }; /** * Checks if a range is completely inside an editable region. * @param {goog.dom.AbstractRange} range The range to test. * @return {boolean} Whether the range is completely inside an editable region. */ goog.editor.range.isEditable = function(range) { var rangeContainer = range.getContainerElement(); // Closure's implementation of getContainerElement() is a little too // smart in IE when exactly one element is contained in the range. // It assumes that there's a user whose intent was actually to select // all that element's children, so it returns the element itself as its // own containing element. // This little sanity check detects this condition so we can account for it. var rangeContainerIsOutsideRange = range.getStartNode() != rangeContainer.parentElement; return (rangeContainerIsOutsideRange && goog.editor.node.isEditableContainer(rangeContainer)) || goog.editor.node.isEditable(rangeContainer); }; /** * Returns whether the given range intersects with any instance of the given * tag. * @param {goog.dom.AbstractRange} range The range to check. * @param {goog.dom.TagName} tagName The name of the tag. * @return {boolean} Whether the given range intersects with any instance of * the given tag. */ goog.editor.range.intersectsTag = function(range, tagName) { if (goog.dom.getAncestorByTagNameAndClass( range.getContainerElement(), tagName)) { return true; } return goog.iter.some( range, function(node) { return node.tagName == tagName; }); }; /** * One endpoint of a range, represented as a Node and and offset. * @param {Node} node The node containing the point. * @param {number} offset The offset of the point into the node. * @constructor * @final */ goog.editor.range.Point = function(node, offset) { /** * The node containing the point. * @type {Node} */ this.node = node; /** * The offset of the point into the node. * @type {number} */ this.offset = offset; }; /** * Gets the point of this point's node in the DOM. * @return {!goog.editor.range.Point} The node's point. */ goog.editor.range.Point.prototype.getParentPoint = function() { var parent = this.node.parentNode; return new goog.editor.range.Point( parent, goog.array.indexOf(parent.childNodes, this.node)); }; /** * Construct the deepest possible point in the DOM that's equivalent * to the given point, expressed as a node and an offset. * @param {Node} node The node containing the point. * @param {number} offset The offset of the point from the node. * @param {boolean=} opt_trendLeft Notice that a (node, offset) pair may be * equivalent to more than one descendent (node, offset) pair in the DOM. * By default, we trend rightward. If this parameter is true, then we * trend leftward. The tendency to fall rightward by default is for * consistency with other range APIs (like placeCursorNextTo). * @param {boolean=} opt_stopOnChildlessElement If true, and we encounter * a Node which is an Element that cannot have children, we return a Point * based on its parent rather than that Node itself. * @return {!goog.editor.range.Point} A new point. */ goog.editor.range.Point.createDeepestPoint = function( node, offset, opt_trendLeft, opt_stopOnChildlessElement) { while (node.nodeType == goog.dom.NodeType.ELEMENT) { var child = node.childNodes[offset]; if (!child && !node.lastChild) { break; } else if (child) { var prevSibling = child.previousSibling; if (opt_trendLeft && prevSibling) { if (opt_stopOnChildlessElement && goog.editor.range.Point.isTerminalElement_(prevSibling)) { break; } node = prevSibling; offset = goog.editor.node.getLength(node); } else { if (opt_stopOnChildlessElement && goog.editor.range.Point.isTerminalElement_(child)) { break; } node = child; offset = 0; } } else { if (opt_stopOnChildlessElement && goog.editor.range.Point.isTerminalElement_(node.lastChild)) { break; } node = node.lastChild; offset = goog.editor.node.getLength(node); } } return new goog.editor.range.Point(node, offset); }; /** * Return true if the specified node is an Element that is not expected to have * children. The createDeepestPoint() method should not traverse into * such elements. * @param {Node} node . * @return {boolean} True if the node is an Element that does not contain * child nodes (e.g. BR, IMG). * @private */ goog.editor.range.Point.isTerminalElement_ = function(node) { return ( node.nodeType == goog.dom.NodeType.ELEMENT && !goog.dom.canHaveChildren(node)); }; /** * Construct a point at the very end of the given node. * @param {Node} node The node to create a point for. * @return {!goog.editor.range.Point} A new point. */ goog.editor.range.Point.getPointAtEndOfNode = function(node) { return new goog.editor.range.Point(node, goog.editor.node.getLength(node)); }; /** * Saves the range by inserting carets into the HTML. * * Unlike the regular saveUsingCarets, this SavedRange normalizes text nodes. * Browsers have other bugs where they don't handle split text nodes in * contentEditable regions right. * * @param {goog.dom.AbstractRange} range The abstract range object. * @return {!goog.dom.SavedCaretRange} A saved caret range that normalizes * text nodes. */ goog.editor.range.saveUsingNormalizedCarets = function(range) { return new goog.editor.range.NormalizedCaretRange_(range); }; /** * Saves the range using carets, but normalizes text nodes when carets * are removed. * @see goog.editor.range.saveUsingNormalizedCarets * @param {goog.dom.AbstractRange} range The range being saved. * @constructor * @extends {goog.dom.SavedCaretRange} * @private */ goog.editor.range.NormalizedCaretRange_ = function(range) { goog.dom.SavedCaretRange.call(this, range); }; goog.inherits( goog.editor.range.NormalizedCaretRange_, goog.dom.SavedCaretRange); /** * Normalizes text nodes whenever carets are removed from the document. * @param {goog.dom.AbstractRange=} opt_range A range whose offsets have already * been adjusted for caret removal; it will be adjusted and returned 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. * @override */ goog.editor.range.NormalizedCaretRange_.prototype.removeCarets = function( opt_range) { var startCaret = this.getCaret(true); var endCaret = this.getCaret(false); var node = startCaret && endCaret ? goog.dom.findCommonAncestor(startCaret, endCaret) : startCaret || endCaret; goog.editor.range.NormalizedCaretRange_.superClass_.removeCarets.call(this); if (opt_range) { return goog.editor.range.rangePreservingNormalize(node, opt_range); } else if (node) { goog.editor.range.selectionPreservingNormalize(node); } };




    © 2015 - 2025 Weber Informatics LLC | Privacy Policy