// 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. * @suppress {missingRequire} Cannot depend on goog.dom.browserrange because it * creates a circular dependency. * * DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead. * * @author robbyw@google.com (Robby Walker) */ 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} * @final */ goog.dom.browserrange.IeRange = function(range, doc) { /** * Lazy cache of the node containing the entire selection. * @private {Node} */ this.parentNode_ = null; /** * Lazy cache of the node containing the start of the selection. * @private {Node} */ this.startNode_ = null; /** * Lazy cache of the node containing the end of the selection. * @private {Node} */ this.endNode_ = null; /** * Lazy cache of the offset in startNode_ where this range starts. * @private {number} */ this.startOffset_ = -1; /** * Lazy cache of the offset in endNode_ where this range ends. * @private {number} */ this.endOffset_ = -1; /** * The browser range object this class wraps. * @private {TextRange} */ this.range_ = range; /** * The document the range exists in. * @private {Document} */ 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 =
// But if the node is sth like
, it shouldn't 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; }; /** * @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   ('

 

), while // node.outerHTML doesn't show the magic node (

). // Case 2 - Empty span. In this case : // node.outerHTML shows '' // 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

[caret]
, // 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
} 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) { /** @suppress {missingRequire} Circular dep with browserrange */ 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. /** @suppress {missingRequire} Circular dep with browserrange */ 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 == '
') // 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(); 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_; } };