Path: blob/trunk/third_party/closure/goog/dom/browserrange/ierange.js
2868 views
// Copyright 2007 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 Definition of the IE browser specific range wrapper.16* @suppress {missingRequire} Cannot depend on goog.dom.browserrange because it17* creates a circular dependency.18*19* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.20*21* @author [email protected] (Robby Walker)22*/232425goog.provide('goog.dom.browserrange.IeRange');2627goog.require('goog.array');28goog.require('goog.dom');29goog.require('goog.dom.NodeType');30goog.require('goog.dom.RangeEndpoint');31goog.require('goog.dom.TagName');32goog.require('goog.dom.browserrange.AbstractRange');33goog.require('goog.log');34goog.require('goog.string');35363738/**39* The constructor for IE specific browser ranges.40* @param {TextRange} range The range object.41* @param {Document} doc The document the range exists in.42* @constructor43* @extends {goog.dom.browserrange.AbstractRange}44* @final45*/46goog.dom.browserrange.IeRange = function(range, doc) {47/**48* Lazy cache of the node containing the entire selection.49* @private {Node}50*/51this.parentNode_ = null;5253/**54* Lazy cache of the node containing the start of the selection.55* @private {Node}56*/57this.startNode_ = null;5859/**60* Lazy cache of the node containing the end of the selection.61* @private {Node}62*/63this.endNode_ = null;6465/**66* Lazy cache of the offset in startNode_ where this range starts.67* @private {number}68*/69this.startOffset_ = -1;7071/**72* Lazy cache of the offset in endNode_ where this range ends.73* @private {number}74*/75this.endOffset_ = -1;7677/**78* The browser range object this class wraps.79* @private {TextRange}80*/81this.range_ = range;8283/**84* The document the range exists in.85* @private {Document}86*/87this.doc_ = doc;88};89goog.inherits(90goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange);919293/**94* Logging object.95* @type {goog.log.Logger}96* @private97*/98goog.dom.browserrange.IeRange.logger_ =99goog.log.getLogger('goog.dom.browserrange.IeRange');100101102/**103* Returns a browser range spanning the given node's contents.104* @param {Node} node The node to select.105* @return {!TextRange} A browser range spanning the node's contents.106* @private107*/108goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {109var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();110if (node.nodeType == goog.dom.NodeType.ELEMENT) {111// Elements are easy.112nodeRange.moveToElementText(node);113// Note(user) : If there are no child nodes of the element, the114// range.htmlText includes the element's outerHTML. The range created above115// is not collapsed, and should be collapsed explicitly.116// Example : node = <div></div>117// But if the node is sth like <br>, it shouldn't be collapsed.118if (goog.dom.browserrange.canContainRangeEndpoint(node) &&119!node.childNodes.length) {120nodeRange.collapse(false);121}122} else {123// Text nodes are hard.124// Compute the offset from the nearest element related position.125var offset = 0;126var sibling = node;127while (sibling = sibling.previousSibling) {128var nodeType = sibling.nodeType;129if (nodeType == goog.dom.NodeType.TEXT) {130offset += sibling.length;131} else if (nodeType == goog.dom.NodeType.ELEMENT) {132// Move to the space after this element.133nodeRange.moveToElementText(sibling);134break;135}136}137138if (!sibling) {139nodeRange.moveToElementText(node.parentNode);140}141142nodeRange.collapse(!sibling);143144if (offset) {145nodeRange.move('character', offset);146}147148nodeRange.moveEnd('character', node.length);149}150151return nodeRange;152};153154155/**156* Returns a browser range spanning the given nodes.157* @param {Node} startNode The node to start with.158* @param {number} startOffset The offset within the start node.159* @param {Node} endNode The node to end with.160* @param {number} endOffset The offset within the end node.161* @return {!TextRange} A browser range spanning the node's contents.162* @private163*/164goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(165startNode, startOffset, endNode, endOffset) {166// Create a range starting at the correct start position.167var child, collapse = false;168if (startNode.nodeType == goog.dom.NodeType.ELEMENT) {169if (startOffset > startNode.childNodes.length) {170goog.log.error(171goog.dom.browserrange.IeRange.logger_,172'Cannot have startOffset > startNode child count');173}174child = startNode.childNodes[startOffset];175collapse = !child;176startNode = child || startNode.lastChild || startNode;177startOffset = 0;178}179var leftRange =180goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode);181182// This happens only when startNode is a text node.183if (startOffset) {184leftRange.move('character', startOffset);185}186187188// The range movements in IE are still an approximation to the standard W3C189// behavior, and IE has its trickery when it comes to htmlText and text190// properties of the range. So we short-circuit computation whenever we can.191if (startNode == endNode && startOffset == endOffset) {192leftRange.collapse(true);193return leftRange;194}195196// This can happen only when the startNode is an element, and there is no node197// at the given offset. We start at the last point inside the startNode in198// that case.199if (collapse) {200leftRange.collapse(false);201}202203// Create a range that ends at the right position.204collapse = false;205if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {206if (endOffset > endNode.childNodes.length) {207goog.log.error(208goog.dom.browserrange.IeRange.logger_,209'Cannot have endOffset > endNode child count');210}211child = endNode.childNodes[endOffset];212endNode = child || endNode.lastChild || endNode;213endOffset = 0;214collapse = !child;215}216var rightRange =217goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode);218rightRange.collapse(!collapse);219if (endOffset) {220rightRange.moveEnd('character', endOffset);221}222223// Merge and return.224leftRange.setEndPoint('EndToEnd', rightRange);225return leftRange;226};227228229/**230* Create a range object that selects the given node's text.231* @param {Node} node The node to select.232* @return {!goog.dom.browserrange.IeRange} An IE range wrapper object.233*/234goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {235var range = new goog.dom.browserrange.IeRange(236goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node),237goog.dom.getOwnerDocument(node));238239if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {240range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;241range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);242range.endOffset_ = range.startOffset_ + 1;243} else {244// Note(user) : Emulate the behavior of W3CRange - Go to deepest possible245// range containers on both edges. It seems W3CRange did this to match the246// IE behavior, and now it is a circle. Changing W3CRange may break clients247// in all sorts of ways.248var tempNode, leaf = node;249while ((tempNode = leaf.firstChild) &&250goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {251leaf = tempNode;252}253range.startNode_ = leaf;254range.startOffset_ = 0;255256leaf = node;257while ((tempNode = leaf.lastChild) &&258goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {259leaf = tempNode;260}261range.endNode_ = leaf;262range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ?263leaf.childNodes.length :264leaf.length;265range.parentNode_ = node;266}267return range;268};269270271/**272* Static method that returns the proper type of browser range.273* @param {Node} startNode The node to start with.274* @param {number} startOffset The offset within the start node.275* @param {Node} endNode The node to end with.276* @param {number} endOffset The offset within the end node.277* @return {!goog.dom.browserrange.AbstractRange} A wrapper object.278*/279goog.dom.browserrange.IeRange.createFromNodes = function(280startNode, startOffset, endNode, endOffset) {281var range = new goog.dom.browserrange.IeRange(282goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(283startNode, startOffset, endNode, endOffset),284goog.dom.getOwnerDocument(startNode));285range.startNode_ = startNode;286range.startOffset_ = startOffset;287range.endNode_ = endNode;288range.endOffset_ = endOffset;289return range;290};291292293/**294* @return {!goog.dom.browserrange.IeRange} A clone of this range.295* @override296*/297goog.dom.browserrange.IeRange.prototype.clone = function() {298var range =299new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_);300range.parentNode_ = this.parentNode_;301range.startNode_ = this.startNode_;302range.endNode_ = this.endNode_;303return range;304};305306307/** @override */308goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {309return this.range_;310};311312313/**314* Clears the cached values for containers.315* @private316*/317goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {318this.parentNode_ = this.startNode_ = this.endNode_ = null;319this.startOffset_ = this.endOffset_ = -1;320};321322323/** @override */324goog.dom.browserrange.IeRange.prototype.getContainer = function() {325if (!this.parentNode_) {326var selectText = this.range_.text;327328// If the selection ends with spaces, we need to remove these to get the329// parent container of only the real contents. This is to get around IE's330// inconsistency where it selects the spaces after a word when you double331// click, but leaves out the spaces during execCommands.332var range = this.range_.duplicate();333// We can't use goog.string.trimRight, as that will remove other whitespace334// too.335var rightTrimmedSelectText = selectText.replace(/ +$/, '');336var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;337if (numSpacesAtEnd) {338range.moveEnd('character', -numSpacesAtEnd);339}340341// Get the parent node. This should be the end, but alas, it is not.342var parent = range.parentElement();343344var htmlText = range.htmlText;345var htmlTextLen = goog.string.stripNewlines(htmlText).length;346if (this.isCollapsed() && htmlTextLen > 0) {347return (this.parentNode_ = parent);348}349350// Deal with selection bug where IE thinks one of the selection's children351// is actually the selection's parent. Relies on the assumption that the352// HTML text of the parent container is longer than the length of the353// selection's HTML text.354355// Also note IE will sometimes insert \r and \n whitespace, which should be356// disregarded. Otherwise the loop may run too long and return wrong parent357while (htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {358parent = parent.parentNode;359}360361// Deal with IE's selecting the outer tags when you double click362// If the innerText is the same, then we just want the inner node363while (parent.childNodes.length == 1 &&364parent.innerText ==365goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) {366// A container should be an element which can have children or a text367// node. Elements like IMG, BR, etc. can not be containers.368if (!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {369break;370}371parent = parent.firstChild;372}373374// If the selection is empty, we may need to do extra work to position it375// properly.376if (selectText.length == 0) {377parent = this.findDeepestContainer_(parent);378}379380this.parentNode_ = parent;381}382383return this.parentNode_;384};385386387/**388* Helper method to find the deepest parent for this range, starting389* the search from {@code node}, which must contain the range.390* @param {Node} node The node to start the search from.391* @return {Node} The deepest parent for this range.392* @private393*/394goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {395var childNodes = node.childNodes;396for (var i = 0, len = childNodes.length; i < len; i++) {397var child = childNodes[i];398399if (goog.dom.browserrange.canContainRangeEndpoint(child)) {400var childRange =401goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);402var start = goog.dom.RangeEndpoint.START;403var end = goog.dom.RangeEndpoint.END;404405// There are two types of erratic nodes where the range over node has406// different htmlText than the node's outerHTML.407// Case 1 - A node with magic child. In this case :408// nodeRange.htmlText shows ('<p> </p>), while409// node.outerHTML doesn't show the magic node (<p></p>).410// Case 2 - Empty span. In this case :411// node.outerHTML shows '<span></span>'412// node.htmlText is just empty string ''.413var isChildRangeErratic = (childRange.htmlText != child.outerHTML);414415// Moreover the inRange comparison fails only when the416var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;417418// In case 2 mentioned above, childRange is also collapsed. So we need to419// compare start of this range with both start and end of child range.420var inChildRange = isNativeInRangeErratic ?421(this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 &&422this.compareBrowserRangeEndpoints(childRange, start, end) <= 0) :423this.range_.inRange(childRange);424if (inChildRange) {425return this.findDeepestContainer_(child);426}427}428}429430return node;431};432433434/** @override */435goog.dom.browserrange.IeRange.prototype.getStartNode = function() {436if (!this.startNode_) {437this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);438if (this.isCollapsed()) {439this.endNode_ = this.startNode_;440}441}442return this.startNode_;443};444445446/** @override */447goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {448if (this.startOffset_ < 0) {449this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);450if (this.isCollapsed()) {451this.endOffset_ = this.startOffset_;452}453}454return this.startOffset_;455};456457458/** @override */459goog.dom.browserrange.IeRange.prototype.getEndNode = function() {460if (this.isCollapsed()) {461return this.getStartNode();462}463if (!this.endNode_) {464this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END);465}466return this.endNode_;467};468469470/** @override */471goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {472if (this.isCollapsed()) {473return this.getStartOffset();474}475if (this.endOffset_ < 0) {476this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);477if (this.isCollapsed()) {478this.startOffset_ = this.endOffset_;479}480}481return this.endOffset_;482};483484485/** @override */486goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(487range, thisEndpoint, otherEndpoint) {488return this.range_.compareEndPoints(489(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +490(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),491range);492};493494495/**496* Recurses to find the correct node for the given endpoint.497* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the node for.498* @param {Node=} opt_node Optional node to start the search from.499* @return {Node} The deepest node containing the endpoint.500* @private501*/502goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(503endpoint, opt_node) {504505/** @type {Node} */506var node = opt_node || this.getContainer();507508// If we're at a leaf in the DOM, we're done.509if (!node || !node.firstChild) {510return node;511}512513var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;514var isStartEndpoint = endpoint == start;515516// Find the first/last child that overlaps the selection.517// NOTE(user) : One of the children can be the magic node. This518// node will have only nodeType property as valid and accessible. All other519// dom related properties like ownerDocument, parentNode, nextSibling etc520// cause error when accessed. Therefore use the for-loop on childNodes to521// iterate.522for (var j = 0, length = node.childNodes.length; j < length; j++) {523var i = isStartEndpoint ? j : length - j - 1;524var child = node.childNodes[i];525var childRange;526try {527childRange = goog.dom.browserrange.createRangeFromNodeContents(child);528} catch (e) {529// If the child is the magic node, then the above will throw530// error. The magic node exists only when editing using keyboard, so can531// not add any unit test.532continue;533}534var ieRange = childRange.getBrowserRange();535536// Case 1 : Finding end points when this range is collapsed.537// Note that in case of collapsed range, getEnd{Node,Offset} call538// getStart{Node,Offset}.539if (this.isCollapsed()) {540// Handle situations where caret is not in a text node. In such cases,541// the adjacent child won't be a valid range endpoint container.542if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {543// The following handles a scenario like <div><BR>[caret]<BR></div>,544// where point should be (div, 1).545if (this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {546this.startOffset_ = this.endOffset_ = i;547return node;548}549} else if (childRange.containsRange(this)) {550// For collapsed range, we should invert the containsRange check with551// childRange.552return this.getEndpointNode_(endpoint, child);553}554555// Case 2 - The first child encountered to have overlap this range is556// contained entirely in this range.557} else if (this.containsRange(childRange)) {558// If it is an element which can not be a range endpoint container, the559// current child offset can be used to deduce the endpoint offset.560if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {561// Container can't be any deeper, so current node is the container.562if (isStartEndpoint) {563this.startOffset_ = i;564} else {565this.endOffset_ = i + 1;566}567return node;568}569570// If child can contain range endpoints, recurse inside this child.571return this.getEndpointNode_(endpoint, child);572573// Case 3 - Partial non-adjacency overlap.574} else if (575this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 &&576this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {577// If this child overlaps the selection partially, recurse down to find578// the first/last child the next level down that overlaps the selection579// completely. We do not consider edge-adjacency (== 0) as overlap.580return this.getEndpointNode_(endpoint, child);581}582}583584// None of the children of this node overlapped the selection, that means585// the selection starts/ends in this node directly.586return node;587};588589590/**591* Compares one endpoint of this range with the endpoint of a node.592* For internal methods, we should prefer this method to containsNode.593* containsNode has a lot of false negatives when we're dealing with594* {@code <br>} tags.595*596* @param {Node} node The node to compare against.597* @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range598* to compare with.599* @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the node600* to compare with.601* @return {number} 0 if the endpoints are equal, negative if this range602* endpoint comes before the other node endpoint, and positive otherwise.603* @private604*/605goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(606node, thisEndpoint, otherEndpoint) {607/** @suppress {missingRequire} Circular dep with browserrange */608return this.range_.compareEndPoints(609(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +610(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),611goog.dom.browserrange.createRangeFromNodeContents(node)612.getBrowserRange());613};614615616/**617* Returns the offset into the start/end container.618* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the offset for.619* @param {Node=} opt_container The container to get the offset relative to.620* Defaults to the value returned by getStartNode/getEndNode.621* @return {number} The offset.622* @private623*/624goog.dom.browserrange.IeRange.prototype.getOffset_ = function(625endpoint, opt_container) {626var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;627var container = opt_container ||628(isStartEndpoint ? this.getStartNode() : this.getEndNode());629630if (container.nodeType == goog.dom.NodeType.ELEMENT) {631// Find the first/last child that overlaps the selection632var children = container.childNodes;633var len = children.length;634var edge = isStartEndpoint ? 0 : len - 1;635var sign = isStartEndpoint ? 1 : -1;636637// We find the index in the child array of the endpoint of the selection.638for (var i = edge; i >= 0 && i < len; i += sign) {639var child = children[i];640// Ignore the child nodes, which could be end point containers.641/** @suppress {missingRequire} Circular dep with browserrange */642if (goog.dom.browserrange.canContainRangeEndpoint(child)) {643continue;644}645// Stop looping when we reach the edge of the selection.646var endPointCompare =647this.compareNodeEndpoints_(child, endpoint, endpoint);648if (endPointCompare == 0) {649return isStartEndpoint ? i : i + 1;650}651}652653// When starting from the end in an empty container, we erroneously return654// -1: fix this to return 0.655return i == -1 ? 0 : i;656} else {657// Get a temporary range object.658var range = this.range_.duplicate();659660// Create a range that selects the entire container.661var nodeRange =662goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container);663664// Now, intersect our range with the container range - this should give us665// the part of our selection that is in the container.666range.setEndPoint(isStartEndpoint ? 'EndToEnd' : 'StartToStart', nodeRange);667668var rangeLength = range.text.length;669return isStartEndpoint ? container.length - rangeLength : rangeLength;670}671};672673674/**675* Returns the text of the given node. Uses IE specific properties.676* @param {Node} node The node to retrieve the text of.677* @return {string} The node's text.678* @private679*/680goog.dom.browserrange.IeRange.getNodeText_ = function(node) {681return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue :682node.innerText;683};684685686/**687* Tests whether this range is valid (i.e. whether its endpoints are still in688* the document). A range becomes invalid when, after this object was created,689* either one or both of its endpoints are removed from the document. Use of690* an invalid range can lead to runtime errors, particularly in IE.691* @return {boolean} Whether the range is valid.692*/693goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {694var range = this.doc_.body.createTextRange();695range.moveToElementText(this.doc_.body);696697return this.containsRange(698new goog.dom.browserrange.IeRange(range, this.doc_), true);699};700701702/** @override */703goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {704// Note(user) : The earlier implementation used (range.text == ''), but this705// fails when (range.htmlText == '<br>')706// Alternative: this.range_.htmlText == '';707return this.range_.compareEndPoints('StartToEnd', this.range_) == 0;708};709710711/** @override */712goog.dom.browserrange.IeRange.prototype.getText = function() {713return this.range_.text;714};715716717/** @override */718goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {719return this.range_.htmlText;720};721722723// SELECTION MODIFICATION724725726/** @override */727goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {728// IE doesn't support programmatic reversed selections.729this.range_.select();730};731732733/** @override */734goog.dom.browserrange.IeRange.prototype.removeContents = function() {735// NOTE: Sometimes htmlText is non-empty, but the range is actually empty.736// TODO(gboyer): The htmlText check is probably unnecessary, but I left it in737// for paranoia.738if (!this.isCollapsed() && this.range_.htmlText) {739// Store some before-removal state.740var startNode = this.getStartNode();741var endNode = this.getEndNode();742var oldText = this.range_.text;743744// IE sometimes deletes nodes unrelated to the selection. This trick fixes745// that problem most of the time. Even though it looks like a no-op, it is746// somehow changing IE's internal state such that empty unrelated nodes are747// no longer deleted.748var clone = this.range_.duplicate();749clone.moveStart('character', 1);750clone.moveStart('character', -1);751752// However, sometimes moving the start back and forth ends up changing the753// range.754// TODO(gboyer): This condition used to happen for empty ranges, but (1)755// never worked, and (2) the isCollapsed call should protect against empty756// ranges better than before. However, this is left for paranoia.757if (clone.text == oldText) {758this.range_ = clone;759}760761// Use the browser's native deletion code.762this.range_.text = '';763this.clearCachedValues_();764765// Unfortunately, when deleting a portion of a single text node, IE creates766// an extra text node unlike other browsers which just change the text in767// the node. We normalize for that behavior here, making IE behave like all768// the other browsers.769var newStartNode = this.getStartNode();770var newStartOffset = this.getStartOffset();771772try {773var sibling = startNode.nextSibling;774if (startNode == endNode && startNode.parentNode &&775startNode.nodeType == goog.dom.NodeType.TEXT && sibling &&776sibling.nodeType == goog.dom.NodeType.TEXT) {777startNode.nodeValue += sibling.nodeValue;778goog.dom.removeNode(sibling);779780// Make sure to reselect the appropriate position.781this.range_ =782goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode);783this.range_.move('character', newStartOffset);784this.clearCachedValues_();785}786} catch (e) {787// IE throws errors on orphaned nodes.788}789}790};791792793/**794* @param {TextRange} range The range to get a dom helper for.795* @return {!goog.dom.DomHelper} A dom helper for the document the range796* resides in.797* @private798*/799goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {800return goog.dom.getDomHelper(range.parentElement());801};802803804/**805* Pastes the given element into the given range, returning the resulting806* element.807* @param {TextRange} range The range to paste into.808* @param {Element} element The node to insert a copy of.809* @param {goog.dom.DomHelper=} opt_domHelper DOM helper object for the document810* the range resides in.811* @return {Element} The resulting copy of element.812* @private813*/814goog.dom.browserrange.IeRange.pasteElement_ = function(815range, element, opt_domHelper) {816opt_domHelper =817opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range);818819// Make sure the node has a unique id.820var id;821var originalId = id = element.id;822if (!id) {823id = element.id = goog.string.createUniqueString();824}825826// Insert (a clone of) the node.827range.pasteHTML(element.outerHTML);828829// Pasting the outerHTML of the modified element into the document creates830// a clone of the element argument. We want to return a reference to the831// clone, not the original. However we need to remove the temporary ID832// first.833element = opt_domHelper.getElement(id);834835// If element is null here, we failed.836if (element) {837if (!originalId) {838element.removeAttribute('id');839}840}841842return element;843};844845846/** @override */847goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {848// Make sure the element is detached from the document.849goog.dom.removeNode(element);850851// IE more or less guarantees that range.htmlText is well-formed & valid.852element.innerHTML = this.range_.htmlText;853element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);854855// If element is null here, we failed.856if (element) {857this.range_.moveToElementText(element);858}859860this.clearCachedValues_();861862return element;863};864865866/**867* Internal handler for inserting a node.868* @param {TextRange} clone A clone of this range's browser range object.869* @param {Node} node The node to insert.870* @param {boolean} before Whether to insert the node before or after the range.871* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use.872* @return {Node} The resulting copy of node.873* @private874*/875goog.dom.browserrange.IeRange.insertNode_ = function(876clone, node, before, opt_domHelper) {877// Get a DOM helper.878opt_domHelper =879opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone);880881// If it's not an element, wrap it in one.882var isNonElement;883if (node.nodeType != goog.dom.NodeType.ELEMENT) {884isNonElement = true;885node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node);886}887888clone.collapse(before);889node = goog.dom.browserrange.IeRange.pasteElement_(890clone,891/** @type {!Element} */ (node), opt_domHelper);892893// If we didn't want an element, unwrap the element and return the node.894if (isNonElement) {895// pasteElement_() may have returned a copy of the wrapper div, and the896// node it wraps could also be a new copy. So we must extract that new897// node from the new wrapper.898var newNonElement = node.firstChild;899opt_domHelper.flattenElement(node);900node = newNonElement;901}902903return node;904};905906907/** @override */908goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {909var output = goog.dom.browserrange.IeRange.insertNode_(910this.range_.duplicate(), node, before);911this.clearCachedValues_();912return output;913};914915916/** @override */917goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(918startNode, endNode) {919var clone1 = this.range_.duplicate();920var clone2 = this.range_.duplicate();921goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);922goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);923924this.clearCachedValues_();925};926927928/** @override */929goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {930this.range_.collapse(toStart);931932if (toStart) {933this.endNode_ = this.startNode_;934this.endOffset_ = this.startOffset_;935} else {936this.startNode_ = this.endNode_;937this.startOffset_ = this.endOffset_;938}939};940941942