Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/dom/browserrange/ierange.js
2868 views
1
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Definition of the IE browser specific range wrapper.
17
* @suppress {missingRequire} Cannot depend on goog.dom.browserrange because it
18
* creates a circular dependency.
19
*
20
* DO NOT USE THIS FILE DIRECTLY. Use goog.dom.Range instead.
21
*
22
* @author [email protected] (Robby Walker)
23
*/
24
25
26
goog.provide('goog.dom.browserrange.IeRange');
27
28
goog.require('goog.array');
29
goog.require('goog.dom');
30
goog.require('goog.dom.NodeType');
31
goog.require('goog.dom.RangeEndpoint');
32
goog.require('goog.dom.TagName');
33
goog.require('goog.dom.browserrange.AbstractRange');
34
goog.require('goog.log');
35
goog.require('goog.string');
36
37
38
39
/**
40
* The constructor for IE specific browser ranges.
41
* @param {TextRange} range The range object.
42
* @param {Document} doc The document the range exists in.
43
* @constructor
44
* @extends {goog.dom.browserrange.AbstractRange}
45
* @final
46
*/
47
goog.dom.browserrange.IeRange = function(range, doc) {
48
/**
49
* Lazy cache of the node containing the entire selection.
50
* @private {Node}
51
*/
52
this.parentNode_ = null;
53
54
/**
55
* Lazy cache of the node containing the start of the selection.
56
* @private {Node}
57
*/
58
this.startNode_ = null;
59
60
/**
61
* Lazy cache of the node containing the end of the selection.
62
* @private {Node}
63
*/
64
this.endNode_ = null;
65
66
/**
67
* Lazy cache of the offset in startNode_ where this range starts.
68
* @private {number}
69
*/
70
this.startOffset_ = -1;
71
72
/**
73
* Lazy cache of the offset in endNode_ where this range ends.
74
* @private {number}
75
*/
76
this.endOffset_ = -1;
77
78
/**
79
* The browser range object this class wraps.
80
* @private {TextRange}
81
*/
82
this.range_ = range;
83
84
/**
85
* The document the range exists in.
86
* @private {Document}
87
*/
88
this.doc_ = doc;
89
};
90
goog.inherits(
91
goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange);
92
93
94
/**
95
* Logging object.
96
* @type {goog.log.Logger}
97
* @private
98
*/
99
goog.dom.browserrange.IeRange.logger_ =
100
goog.log.getLogger('goog.dom.browserrange.IeRange');
101
102
103
/**
104
* Returns a browser range spanning the given node's contents.
105
* @param {Node} node The node to select.
106
* @return {!TextRange} A browser range spanning the node's contents.
107
* @private
108
*/
109
goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {
110
var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();
111
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
112
// Elements are easy.
113
nodeRange.moveToElementText(node);
114
// Note(user) : If there are no child nodes of the element, the
115
// range.htmlText includes the element's outerHTML. The range created above
116
// is not collapsed, and should be collapsed explicitly.
117
// Example : node = <div></div>
118
// But if the node is sth like <br>, it shouldn't be collapsed.
119
if (goog.dom.browserrange.canContainRangeEndpoint(node) &&
120
!node.childNodes.length) {
121
nodeRange.collapse(false);
122
}
123
} else {
124
// Text nodes are hard.
125
// Compute the offset from the nearest element related position.
126
var offset = 0;
127
var sibling = node;
128
while (sibling = sibling.previousSibling) {
129
var nodeType = sibling.nodeType;
130
if (nodeType == goog.dom.NodeType.TEXT) {
131
offset += sibling.length;
132
} else if (nodeType == goog.dom.NodeType.ELEMENT) {
133
// Move to the space after this element.
134
nodeRange.moveToElementText(sibling);
135
break;
136
}
137
}
138
139
if (!sibling) {
140
nodeRange.moveToElementText(node.parentNode);
141
}
142
143
nodeRange.collapse(!sibling);
144
145
if (offset) {
146
nodeRange.move('character', offset);
147
}
148
149
nodeRange.moveEnd('character', node.length);
150
}
151
152
return nodeRange;
153
};
154
155
156
/**
157
* Returns a browser range spanning the given nodes.
158
* @param {Node} startNode The node to start with.
159
* @param {number} startOffset The offset within the start node.
160
* @param {Node} endNode The node to end with.
161
* @param {number} endOffset The offset within the end node.
162
* @return {!TextRange} A browser range spanning the node's contents.
163
* @private
164
*/
165
goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(
166
startNode, startOffset, endNode, endOffset) {
167
// Create a range starting at the correct start position.
168
var child, collapse = false;
169
if (startNode.nodeType == goog.dom.NodeType.ELEMENT) {
170
if (startOffset > startNode.childNodes.length) {
171
goog.log.error(
172
goog.dom.browserrange.IeRange.logger_,
173
'Cannot have startOffset > startNode child count');
174
}
175
child = startNode.childNodes[startOffset];
176
collapse = !child;
177
startNode = child || startNode.lastChild || startNode;
178
startOffset = 0;
179
}
180
var leftRange =
181
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode);
182
183
// This happens only when startNode is a text node.
184
if (startOffset) {
185
leftRange.move('character', startOffset);
186
}
187
188
189
// The range movements in IE are still an approximation to the standard W3C
190
// behavior, and IE has its trickery when it comes to htmlText and text
191
// properties of the range. So we short-circuit computation whenever we can.
192
if (startNode == endNode && startOffset == endOffset) {
193
leftRange.collapse(true);
194
return leftRange;
195
}
196
197
// This can happen only when the startNode is an element, and there is no node
198
// at the given offset. We start at the last point inside the startNode in
199
// that case.
200
if (collapse) {
201
leftRange.collapse(false);
202
}
203
204
// Create a range that ends at the right position.
205
collapse = false;
206
if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {
207
if (endOffset > endNode.childNodes.length) {
208
goog.log.error(
209
goog.dom.browserrange.IeRange.logger_,
210
'Cannot have endOffset > endNode child count');
211
}
212
child = endNode.childNodes[endOffset];
213
endNode = child || endNode.lastChild || endNode;
214
endOffset = 0;
215
collapse = !child;
216
}
217
var rightRange =
218
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode);
219
rightRange.collapse(!collapse);
220
if (endOffset) {
221
rightRange.moveEnd('character', endOffset);
222
}
223
224
// Merge and return.
225
leftRange.setEndPoint('EndToEnd', rightRange);
226
return leftRange;
227
};
228
229
230
/**
231
* Create a range object that selects the given node's text.
232
* @param {Node} node The node to select.
233
* @return {!goog.dom.browserrange.IeRange} An IE range wrapper object.
234
*/
235
goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {
236
var range = new goog.dom.browserrange.IeRange(
237
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node),
238
goog.dom.getOwnerDocument(node));
239
240
if (!goog.dom.browserrange.canContainRangeEndpoint(node)) {
241
range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;
242
range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);
243
range.endOffset_ = range.startOffset_ + 1;
244
} else {
245
// Note(user) : Emulate the behavior of W3CRange - Go to deepest possible
246
// range containers on both edges. It seems W3CRange did this to match the
247
// IE behavior, and now it is a circle. Changing W3CRange may break clients
248
// in all sorts of ways.
249
var tempNode, leaf = node;
250
while ((tempNode = leaf.firstChild) &&
251
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
252
leaf = tempNode;
253
}
254
range.startNode_ = leaf;
255
range.startOffset_ = 0;
256
257
leaf = node;
258
while ((tempNode = leaf.lastChild) &&
259
goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
260
leaf = tempNode;
261
}
262
range.endNode_ = leaf;
263
range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ?
264
leaf.childNodes.length :
265
leaf.length;
266
range.parentNode_ = node;
267
}
268
return range;
269
};
270
271
272
/**
273
* Static method that returns the proper type of browser range.
274
* @param {Node} startNode The node to start with.
275
* @param {number} startOffset The offset within the start node.
276
* @param {Node} endNode The node to end with.
277
* @param {number} endOffset The offset within the end node.
278
* @return {!goog.dom.browserrange.AbstractRange} A wrapper object.
279
*/
280
goog.dom.browserrange.IeRange.createFromNodes = function(
281
startNode, startOffset, endNode, endOffset) {
282
var range = new goog.dom.browserrange.IeRange(
283
goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(
284
startNode, startOffset, endNode, endOffset),
285
goog.dom.getOwnerDocument(startNode));
286
range.startNode_ = startNode;
287
range.startOffset_ = startOffset;
288
range.endNode_ = endNode;
289
range.endOffset_ = endOffset;
290
return range;
291
};
292
293
294
/**
295
* @return {!goog.dom.browserrange.IeRange} A clone of this range.
296
* @override
297
*/
298
goog.dom.browserrange.IeRange.prototype.clone = function() {
299
var range =
300
new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_);
301
range.parentNode_ = this.parentNode_;
302
range.startNode_ = this.startNode_;
303
range.endNode_ = this.endNode_;
304
return range;
305
};
306
307
308
/** @override */
309
goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {
310
return this.range_;
311
};
312
313
314
/**
315
* Clears the cached values for containers.
316
* @private
317
*/
318
goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {
319
this.parentNode_ = this.startNode_ = this.endNode_ = null;
320
this.startOffset_ = this.endOffset_ = -1;
321
};
322
323
324
/** @override */
325
goog.dom.browserrange.IeRange.prototype.getContainer = function() {
326
if (!this.parentNode_) {
327
var selectText = this.range_.text;
328
329
// If the selection ends with spaces, we need to remove these to get the
330
// parent container of only the real contents. This is to get around IE's
331
// inconsistency where it selects the spaces after a word when you double
332
// click, but leaves out the spaces during execCommands.
333
var range = this.range_.duplicate();
334
// We can't use goog.string.trimRight, as that will remove other whitespace
335
// too.
336
var rightTrimmedSelectText = selectText.replace(/ +$/, '');
337
var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;
338
if (numSpacesAtEnd) {
339
range.moveEnd('character', -numSpacesAtEnd);
340
}
341
342
// Get the parent node. This should be the end, but alas, it is not.
343
var parent = range.parentElement();
344
345
var htmlText = range.htmlText;
346
var htmlTextLen = goog.string.stripNewlines(htmlText).length;
347
if (this.isCollapsed() && htmlTextLen > 0) {
348
return (this.parentNode_ = parent);
349
}
350
351
// Deal with selection bug where IE thinks one of the selection's children
352
// is actually the selection's parent. Relies on the assumption that the
353
// HTML text of the parent container is longer than the length of the
354
// selection's HTML text.
355
356
// Also note IE will sometimes insert \r and \n whitespace, which should be
357
// disregarded. Otherwise the loop may run too long and return wrong parent
358
while (htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {
359
parent = parent.parentNode;
360
}
361
362
// Deal with IE's selecting the outer tags when you double click
363
// If the innerText is the same, then we just want the inner node
364
while (parent.childNodes.length == 1 &&
365
parent.innerText ==
366
goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) {
367
// A container should be an element which can have children or a text
368
// node. Elements like IMG, BR, etc. can not be containers.
369
if (!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {
370
break;
371
}
372
parent = parent.firstChild;
373
}
374
375
// If the selection is empty, we may need to do extra work to position it
376
// properly.
377
if (selectText.length == 0) {
378
parent = this.findDeepestContainer_(parent);
379
}
380
381
this.parentNode_ = parent;
382
}
383
384
return this.parentNode_;
385
};
386
387
388
/**
389
* Helper method to find the deepest parent for this range, starting
390
* the search from {@code node}, which must contain the range.
391
* @param {Node} node The node to start the search from.
392
* @return {Node} The deepest parent for this range.
393
* @private
394
*/
395
goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {
396
var childNodes = node.childNodes;
397
for (var i = 0, len = childNodes.length; i < len; i++) {
398
var child = childNodes[i];
399
400
if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
401
var childRange =
402
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);
403
var start = goog.dom.RangeEndpoint.START;
404
var end = goog.dom.RangeEndpoint.END;
405
406
// There are two types of erratic nodes where the range over node has
407
// different htmlText than the node's outerHTML.
408
// Case 1 - A node with magic &nbsp; child. In this case :
409
// nodeRange.htmlText shows &nbsp; ('<p>&nbsp;</p>), while
410
// node.outerHTML doesn't show the magic node (<p></p>).
411
// Case 2 - Empty span. In this case :
412
// node.outerHTML shows '<span></span>'
413
// node.htmlText is just empty string ''.
414
var isChildRangeErratic = (childRange.htmlText != child.outerHTML);
415
416
// Moreover the inRange comparison fails only when the
417
var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;
418
419
// In case 2 mentioned above, childRange is also collapsed. So we need to
420
// compare start of this range with both start and end of child range.
421
var inChildRange = isNativeInRangeErratic ?
422
(this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 &&
423
this.compareBrowserRangeEndpoints(childRange, start, end) <= 0) :
424
this.range_.inRange(childRange);
425
if (inChildRange) {
426
return this.findDeepestContainer_(child);
427
}
428
}
429
}
430
431
return node;
432
};
433
434
435
/** @override */
436
goog.dom.browserrange.IeRange.prototype.getStartNode = function() {
437
if (!this.startNode_) {
438
this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);
439
if (this.isCollapsed()) {
440
this.endNode_ = this.startNode_;
441
}
442
}
443
return this.startNode_;
444
};
445
446
447
/** @override */
448
goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {
449
if (this.startOffset_ < 0) {
450
this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);
451
if (this.isCollapsed()) {
452
this.endOffset_ = this.startOffset_;
453
}
454
}
455
return this.startOffset_;
456
};
457
458
459
/** @override */
460
goog.dom.browserrange.IeRange.prototype.getEndNode = function() {
461
if (this.isCollapsed()) {
462
return this.getStartNode();
463
}
464
if (!this.endNode_) {
465
this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END);
466
}
467
return this.endNode_;
468
};
469
470
471
/** @override */
472
goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {
473
if (this.isCollapsed()) {
474
return this.getStartOffset();
475
}
476
if (this.endOffset_ < 0) {
477
this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);
478
if (this.isCollapsed()) {
479
this.startOffset_ = this.endOffset_;
480
}
481
}
482
return this.endOffset_;
483
};
484
485
486
/** @override */
487
goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(
488
range, thisEndpoint, otherEndpoint) {
489
return this.range_.compareEndPoints(
490
(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +
491
(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
492
range);
493
};
494
495
496
/**
497
* Recurses to find the correct node for the given endpoint.
498
* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the node for.
499
* @param {Node=} opt_node Optional node to start the search from.
500
* @return {Node} The deepest node containing the endpoint.
501
* @private
502
*/
503
goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(
504
endpoint, opt_node) {
505
506
/** @type {Node} */
507
var node = opt_node || this.getContainer();
508
509
// If we're at a leaf in the DOM, we're done.
510
if (!node || !node.firstChild) {
511
return node;
512
}
513
514
var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
515
var isStartEndpoint = endpoint == start;
516
517
// Find the first/last child that overlaps the selection.
518
// NOTE(user) : One of the children can be the magic &nbsp; node. This
519
// node will have only nodeType property as valid and accessible. All other
520
// dom related properties like ownerDocument, parentNode, nextSibling etc
521
// cause error when accessed. Therefore use the for-loop on childNodes to
522
// iterate.
523
for (var j = 0, length = node.childNodes.length; j < length; j++) {
524
var i = isStartEndpoint ? j : length - j - 1;
525
var child = node.childNodes[i];
526
var childRange;
527
try {
528
childRange = goog.dom.browserrange.createRangeFromNodeContents(child);
529
} catch (e) {
530
// If the child is the magic &nbsp; node, then the above will throw
531
// error. The magic node exists only when editing using keyboard, so can
532
// not add any unit test.
533
continue;
534
}
535
var ieRange = childRange.getBrowserRange();
536
537
// Case 1 : Finding end points when this range is collapsed.
538
// Note that in case of collapsed range, getEnd{Node,Offset} call
539
// getStart{Node,Offset}.
540
if (this.isCollapsed()) {
541
// Handle situations where caret is not in a text node. In such cases,
542
// the adjacent child won't be a valid range endpoint container.
543
if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
544
// The following handles a scenario like <div><BR>[caret]<BR></div>,
545
// where point should be (div, 1).
546
if (this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {
547
this.startOffset_ = this.endOffset_ = i;
548
return node;
549
}
550
} else if (childRange.containsRange(this)) {
551
// For collapsed range, we should invert the containsRange check with
552
// childRange.
553
return this.getEndpointNode_(endpoint, child);
554
}
555
556
// Case 2 - The first child encountered to have overlap this range is
557
// contained entirely in this range.
558
} else if (this.containsRange(childRange)) {
559
// If it is an element which can not be a range endpoint container, the
560
// current child offset can be used to deduce the endpoint offset.
561
if (!goog.dom.browserrange.canContainRangeEndpoint(child)) {
562
// Container can't be any deeper, so current node is the container.
563
if (isStartEndpoint) {
564
this.startOffset_ = i;
565
} else {
566
this.endOffset_ = i + 1;
567
}
568
return node;
569
}
570
571
// If child can contain range endpoints, recurse inside this child.
572
return this.getEndpointNode_(endpoint, child);
573
574
// Case 3 - Partial non-adjacency overlap.
575
} else if (
576
this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 &&
577
this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {
578
// If this child overlaps the selection partially, recurse down to find
579
// the first/last child the next level down that overlaps the selection
580
// completely. We do not consider edge-adjacency (== 0) as overlap.
581
return this.getEndpointNode_(endpoint, child);
582
}
583
}
584
585
// None of the children of this node overlapped the selection, that means
586
// the selection starts/ends in this node directly.
587
return node;
588
};
589
590
591
/**
592
* Compares one endpoint of this range with the endpoint of a node.
593
* For internal methods, we should prefer this method to containsNode.
594
* containsNode has a lot of false negatives when we're dealing with
595
* {@code <br>} tags.
596
*
597
* @param {Node} node The node to compare against.
598
* @param {goog.dom.RangeEndpoint} thisEndpoint The endpoint of this range
599
* to compare with.
600
* @param {goog.dom.RangeEndpoint} otherEndpoint The endpoint of the node
601
* to compare with.
602
* @return {number} 0 if the endpoints are equal, negative if this range
603
* endpoint comes before the other node endpoint, and positive otherwise.
604
* @private
605
*/
606
goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(
607
node, thisEndpoint, otherEndpoint) {
608
/** @suppress {missingRequire} Circular dep with browserrange */
609
return this.range_.compareEndPoints(
610
(thisEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End') + 'To' +
611
(otherEndpoint == goog.dom.RangeEndpoint.START ? 'Start' : 'End'),
612
goog.dom.browserrange.createRangeFromNodeContents(node)
613
.getBrowserRange());
614
};
615
616
617
/**
618
* Returns the offset into the start/end container.
619
* @param {goog.dom.RangeEndpoint} endpoint The endpoint to get the offset for.
620
* @param {Node=} opt_container The container to get the offset relative to.
621
* Defaults to the value returned by getStartNode/getEndNode.
622
* @return {number} The offset.
623
* @private
624
*/
625
goog.dom.browserrange.IeRange.prototype.getOffset_ = function(
626
endpoint, opt_container) {
627
var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;
628
var container = opt_container ||
629
(isStartEndpoint ? this.getStartNode() : this.getEndNode());
630
631
if (container.nodeType == goog.dom.NodeType.ELEMENT) {
632
// Find the first/last child that overlaps the selection
633
var children = container.childNodes;
634
var len = children.length;
635
var edge = isStartEndpoint ? 0 : len - 1;
636
var sign = isStartEndpoint ? 1 : -1;
637
638
// We find the index in the child array of the endpoint of the selection.
639
for (var i = edge; i >= 0 && i < len; i += sign) {
640
var child = children[i];
641
// Ignore the child nodes, which could be end point containers.
642
/** @suppress {missingRequire} Circular dep with browserrange */
643
if (goog.dom.browserrange.canContainRangeEndpoint(child)) {
644
continue;
645
}
646
// Stop looping when we reach the edge of the selection.
647
var endPointCompare =
648
this.compareNodeEndpoints_(child, endpoint, endpoint);
649
if (endPointCompare == 0) {
650
return isStartEndpoint ? i : i + 1;
651
}
652
}
653
654
// When starting from the end in an empty container, we erroneously return
655
// -1: fix this to return 0.
656
return i == -1 ? 0 : i;
657
} else {
658
// Get a temporary range object.
659
var range = this.range_.duplicate();
660
661
// Create a range that selects the entire container.
662
var nodeRange =
663
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container);
664
665
// Now, intersect our range with the container range - this should give us
666
// the part of our selection that is in the container.
667
range.setEndPoint(isStartEndpoint ? 'EndToEnd' : 'StartToStart', nodeRange);
668
669
var rangeLength = range.text.length;
670
return isStartEndpoint ? container.length - rangeLength : rangeLength;
671
}
672
};
673
674
675
/**
676
* Returns the text of the given node. Uses IE specific properties.
677
* @param {Node} node The node to retrieve the text of.
678
* @return {string} The node's text.
679
* @private
680
*/
681
goog.dom.browserrange.IeRange.getNodeText_ = function(node) {
682
return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue :
683
node.innerText;
684
};
685
686
687
/**
688
* Tests whether this range is valid (i.e. whether its endpoints are still in
689
* the document). A range becomes invalid when, after this object was created,
690
* either one or both of its endpoints are removed from the document. Use of
691
* an invalid range can lead to runtime errors, particularly in IE.
692
* @return {boolean} Whether the range is valid.
693
*/
694
goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {
695
var range = this.doc_.body.createTextRange();
696
range.moveToElementText(this.doc_.body);
697
698
return this.containsRange(
699
new goog.dom.browserrange.IeRange(range, this.doc_), true);
700
};
701
702
703
/** @override */
704
goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {
705
// Note(user) : The earlier implementation used (range.text == ''), but this
706
// fails when (range.htmlText == '<br>')
707
// Alternative: this.range_.htmlText == '';
708
return this.range_.compareEndPoints('StartToEnd', this.range_) == 0;
709
};
710
711
712
/** @override */
713
goog.dom.browserrange.IeRange.prototype.getText = function() {
714
return this.range_.text;
715
};
716
717
718
/** @override */
719
goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {
720
return this.range_.htmlText;
721
};
722
723
724
// SELECTION MODIFICATION
725
726
727
/** @override */
728
goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {
729
// IE doesn't support programmatic reversed selections.
730
this.range_.select();
731
};
732
733
734
/** @override */
735
goog.dom.browserrange.IeRange.prototype.removeContents = function() {
736
// NOTE: Sometimes htmlText is non-empty, but the range is actually empty.
737
// TODO(gboyer): The htmlText check is probably unnecessary, but I left it in
738
// for paranoia.
739
if (!this.isCollapsed() && this.range_.htmlText) {
740
// Store some before-removal state.
741
var startNode = this.getStartNode();
742
var endNode = this.getEndNode();
743
var oldText = this.range_.text;
744
745
// IE sometimes deletes nodes unrelated to the selection. This trick fixes
746
// that problem most of the time. Even though it looks like a no-op, it is
747
// somehow changing IE's internal state such that empty unrelated nodes are
748
// no longer deleted.
749
var clone = this.range_.duplicate();
750
clone.moveStart('character', 1);
751
clone.moveStart('character', -1);
752
753
// However, sometimes moving the start back and forth ends up changing the
754
// range.
755
// TODO(gboyer): This condition used to happen for empty ranges, but (1)
756
// never worked, and (2) the isCollapsed call should protect against empty
757
// ranges better than before. However, this is left for paranoia.
758
if (clone.text == oldText) {
759
this.range_ = clone;
760
}
761
762
// Use the browser's native deletion code.
763
this.range_.text = '';
764
this.clearCachedValues_();
765
766
// Unfortunately, when deleting a portion of a single text node, IE creates
767
// an extra text node unlike other browsers which just change the text in
768
// the node. We normalize for that behavior here, making IE behave like all
769
// the other browsers.
770
var newStartNode = this.getStartNode();
771
var newStartOffset = this.getStartOffset();
772
773
try {
774
var sibling = startNode.nextSibling;
775
if (startNode == endNode && startNode.parentNode &&
776
startNode.nodeType == goog.dom.NodeType.TEXT && sibling &&
777
sibling.nodeType == goog.dom.NodeType.TEXT) {
778
startNode.nodeValue += sibling.nodeValue;
779
goog.dom.removeNode(sibling);
780
781
// Make sure to reselect the appropriate position.
782
this.range_ =
783
goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode);
784
this.range_.move('character', newStartOffset);
785
this.clearCachedValues_();
786
}
787
} catch (e) {
788
// IE throws errors on orphaned nodes.
789
}
790
}
791
};
792
793
794
/**
795
* @param {TextRange} range The range to get a dom helper for.
796
* @return {!goog.dom.DomHelper} A dom helper for the document the range
797
* resides in.
798
* @private
799
*/
800
goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {
801
return goog.dom.getDomHelper(range.parentElement());
802
};
803
804
805
/**
806
* Pastes the given element into the given range, returning the resulting
807
* element.
808
* @param {TextRange} range The range to paste into.
809
* @param {Element} element The node to insert a copy of.
810
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper object for the document
811
* the range resides in.
812
* @return {Element} The resulting copy of element.
813
* @private
814
*/
815
goog.dom.browserrange.IeRange.pasteElement_ = function(
816
range, element, opt_domHelper) {
817
opt_domHelper =
818
opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range);
819
820
// Make sure the node has a unique id.
821
var id;
822
var originalId = id = element.id;
823
if (!id) {
824
id = element.id = goog.string.createUniqueString();
825
}
826
827
// Insert (a clone of) the node.
828
range.pasteHTML(element.outerHTML);
829
830
// Pasting the outerHTML of the modified element into the document creates
831
// a clone of the element argument. We want to return a reference to the
832
// clone, not the original. However we need to remove the temporary ID
833
// first.
834
element = opt_domHelper.getElement(id);
835
836
// If element is null here, we failed.
837
if (element) {
838
if (!originalId) {
839
element.removeAttribute('id');
840
}
841
}
842
843
return element;
844
};
845
846
847
/** @override */
848
goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {
849
// Make sure the element is detached from the document.
850
goog.dom.removeNode(element);
851
852
// IE more or less guarantees that range.htmlText is well-formed & valid.
853
element.innerHTML = this.range_.htmlText;
854
element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);
855
856
// If element is null here, we failed.
857
if (element) {
858
this.range_.moveToElementText(element);
859
}
860
861
this.clearCachedValues_();
862
863
return element;
864
};
865
866
867
/**
868
* Internal handler for inserting a node.
869
* @param {TextRange} clone A clone of this range's browser range object.
870
* @param {Node} node The node to insert.
871
* @param {boolean} before Whether to insert the node before or after the range.
872
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use.
873
* @return {Node} The resulting copy of node.
874
* @private
875
*/
876
goog.dom.browserrange.IeRange.insertNode_ = function(
877
clone, node, before, opt_domHelper) {
878
// Get a DOM helper.
879
opt_domHelper =
880
opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone);
881
882
// If it's not an element, wrap it in one.
883
var isNonElement;
884
if (node.nodeType != goog.dom.NodeType.ELEMENT) {
885
isNonElement = true;
886
node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node);
887
}
888
889
clone.collapse(before);
890
node = goog.dom.browserrange.IeRange.pasteElement_(
891
clone,
892
/** @type {!Element} */ (node), opt_domHelper);
893
894
// If we didn't want an element, unwrap the element and return the node.
895
if (isNonElement) {
896
// pasteElement_() may have returned a copy of the wrapper div, and the
897
// node it wraps could also be a new copy. So we must extract that new
898
// node from the new wrapper.
899
var newNonElement = node.firstChild;
900
opt_domHelper.flattenElement(node);
901
node = newNonElement;
902
}
903
904
return node;
905
};
906
907
908
/** @override */
909
goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {
910
var output = goog.dom.browserrange.IeRange.insertNode_(
911
this.range_.duplicate(), node, before);
912
this.clearCachedValues_();
913
return output;
914
};
915
916
917
/** @override */
918
goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(
919
startNode, endNode) {
920
var clone1 = this.range_.duplicate();
921
var clone2 = this.range_.duplicate();
922
goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);
923
goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);
924
925
this.clearCachedValues_();
926
};
927
928
929
/** @override */
930
goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {
931
this.range_.collapse(toStart);
932
933
if (toStart) {
934
this.endNode_ = this.startNode_;
935
this.endOffset_ = this.startOffset_;
936
} else {
937
this.startNode_ = this.endNode_;
938
this.startOffset_ = this.endOffset_;
939
}
940
};
941
942