Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/dom/textrange.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 Utilities for working with text ranges in HTML documents.
17
*
18
* @author [email protected] (Robby Walker)
19
*/
20
21
22
goog.provide('goog.dom.TextRange');
23
24
goog.require('goog.array');
25
goog.require('goog.dom');
26
goog.require('goog.dom.AbstractRange');
27
goog.require('goog.dom.RangeType');
28
goog.require('goog.dom.SavedRange');
29
goog.require('goog.dom.TagName');
30
goog.require('goog.dom.TextRangeIterator');
31
goog.require('goog.dom.browserrange');
32
goog.require('goog.string');
33
goog.require('goog.userAgent');
34
35
36
37
/**
38
* Create a new text selection with no properties. Do not use this constructor:
39
* use one of the goog.dom.Range.createFrom* methods instead.
40
* @constructor
41
* @extends {goog.dom.AbstractRange}
42
* @final
43
*/
44
goog.dom.TextRange = function() {
45
/**
46
* The browser specific range wrapper. This can be null if one of the other
47
* representations of the range is specified.
48
* @private {goog.dom.browserrange.AbstractRange?}
49
*/
50
this.browserRangeWrapper_ = null;
51
52
/**
53
* The start node of the range. This can be null if one of the other
54
* representations of the range is specified.
55
* @private {Node}
56
*/
57
this.startNode_ = null;
58
59
/**
60
* The start offset of the range. This can be null if one of the other
61
* representations of the range is specified.
62
* @private {?number}
63
*/
64
this.startOffset_ = null;
65
66
/**
67
* The end node of the range. This can be null if one of the other
68
* representations of the range is specified.
69
* @private {Node}
70
*/
71
this.endNode_ = null;
72
73
/**
74
* The end offset of the range. This can be null if one of the other
75
* representations of the range is specified.
76
* @private {?number}
77
*/
78
this.endOffset_ = null;
79
80
/**
81
* Whether the focus node is before the anchor node.
82
* @private {boolean}
83
*/
84
this.isReversed_ = false;
85
};
86
goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
87
88
89
/**
90
* Create a new range wrapper from the given browser range object. Do not use
91
* this method directly - please use goog.dom.Range.createFrom* instead.
92
* @param {Range|TextRange} range The browser range object.
93
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
94
* node.
95
* @return {!goog.dom.TextRange} A range wrapper object.
96
*/
97
goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
98
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
99
goog.dom.browserrange.createRange(range), opt_isReversed);
100
};
101
102
103
/**
104
* Create a new range wrapper from the given browser range wrapper.
105
* @param {goog.dom.browserrange.AbstractRange} browserRange The browser range
106
* wrapper.
107
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
108
* node.
109
* @return {!goog.dom.TextRange} A range wrapper object.
110
* @private
111
*/
112
goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(
113
browserRange, opt_isReversed) {
114
var range = new goog.dom.TextRange();
115
116
// Initialize the range as a browser range wrapper type range.
117
range.browserRangeWrapper_ = browserRange;
118
range.isReversed_ = !!opt_isReversed;
119
120
return range;
121
};
122
123
124
/**
125
* Create a new range wrapper that selects the given node's text. Do not use
126
* this method directly - please use goog.dom.Range.createFrom* instead.
127
* @param {Node} node The node to select.
128
* @param {boolean=} opt_isReversed Whether the focus node is before the anchor
129
* node.
130
* @return {!goog.dom.TextRange} A range wrapper object.
131
*/
132
goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
133
return goog.dom.TextRange.createFromBrowserRangeWrapper_(
134
goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed);
135
};
136
137
138
/**
139
* Create a new range wrapper that selects the area between the given nodes,
140
* accounting for the given offsets. Do not use this method directly - please
141
* use goog.dom.Range.createFrom* instead.
142
* @param {Node} anchorNode The node to start with.
143
* @param {number} anchorOffset The offset within the node to start.
144
* @param {Node} focusNode The node to end with.
145
* @param {number} focusOffset The offset within the node to end.
146
* @return {!goog.dom.TextRange} A range wrapper object.
147
*/
148
goog.dom.TextRange.createFromNodes = function(
149
anchorNode, anchorOffset, focusNode, focusOffset) {
150
var range = new goog.dom.TextRange();
151
range.isReversed_ = /** @suppress {missingRequire} */ (
152
goog.dom.Range.isReversed(
153
anchorNode, anchorOffset, focusNode, focusOffset));
154
155
// Avoid selecting terminal elements directly
156
if (goog.dom.isElement(anchorNode) && !goog.dom.canHaveChildren(anchorNode)) {
157
var parent = anchorNode.parentNode;
158
anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
159
anchorNode = parent;
160
}
161
162
if (goog.dom.isElement(focusNode) && !goog.dom.canHaveChildren(focusNode)) {
163
var parent = focusNode.parentNode;
164
focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
165
focusNode = parent;
166
}
167
168
// Initialize the range as a W3C style range.
169
if (range.isReversed_) {
170
range.startNode_ = focusNode;
171
range.startOffset_ = focusOffset;
172
range.endNode_ = anchorNode;
173
range.endOffset_ = anchorOffset;
174
} else {
175
range.startNode_ = anchorNode;
176
range.startOffset_ = anchorOffset;
177
range.endNode_ = focusNode;
178
range.endOffset_ = focusOffset;
179
}
180
181
return range;
182
};
183
184
185
// Method implementations
186
187
188
/**
189
* @return {!goog.dom.TextRange} A clone of this range.
190
* @override
191
*/
192
goog.dom.TextRange.prototype.clone = function() {
193
var range = new goog.dom.TextRange();
194
range.browserRangeWrapper_ =
195
this.browserRangeWrapper_ && this.browserRangeWrapper_.clone();
196
range.startNode_ = this.startNode_;
197
range.startOffset_ = this.startOffset_;
198
range.endNode_ = this.endNode_;
199
range.endOffset_ = this.endOffset_;
200
range.isReversed_ = this.isReversed_;
201
202
return range;
203
};
204
205
206
/** @override */
207
goog.dom.TextRange.prototype.getType = function() {
208
return goog.dom.RangeType.TEXT;
209
};
210
211
212
/** @override */
213
goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
214
return this.getBrowserRangeWrapper_().getBrowserRange();
215
};
216
217
218
/** @override */
219
goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
220
// Test if it's a control range by seeing if a control range only method
221
// exists.
222
if (goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
223
return false;
224
}
225
this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange);
226
this.clearCachedValues_();
227
return true;
228
};
229
230
231
/**
232
* Clear all cached values.
233
* @private
234
*/
235
goog.dom.TextRange.prototype.clearCachedValues_ = function() {
236
this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null;
237
};
238
239
240
/** @override */
241
goog.dom.TextRange.prototype.getTextRangeCount = function() {
242
return 1;
243
};
244
245
246
/** @override */
247
goog.dom.TextRange.prototype.getTextRange = function(i) {
248
return this;
249
};
250
251
252
/**
253
* @return {!goog.dom.browserrange.AbstractRange} The range wrapper object.
254
* @private
255
*/
256
goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
257
return this.browserRangeWrapper_ ||
258
(this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(
259
this.getStartNode(), this.getStartOffset(), this.getEndNode(),
260
this.getEndOffset()));
261
};
262
263
264
/** @override */
265
goog.dom.TextRange.prototype.getContainer = function() {
266
return this.getBrowserRangeWrapper_().getContainer();
267
};
268
269
270
/** @override */
271
goog.dom.TextRange.prototype.getStartNode = function() {
272
return this.startNode_ ||
273
(this.startNode_ = this.getBrowserRangeWrapper_().getStartNode());
274
};
275
276
277
/** @override */
278
goog.dom.TextRange.prototype.getStartOffset = function() {
279
return this.startOffset_ != null ?
280
this.startOffset_ :
281
(this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset());
282
};
283
284
285
/** @override */
286
goog.dom.TextRange.prototype.getStartPosition = function() {
287
return this.getBrowserRangeWrapper_().getStartPosition();
288
};
289
290
291
/** @override */
292
goog.dom.TextRange.prototype.getEndNode = function() {
293
return this.endNode_ ||
294
(this.endNode_ = this.getBrowserRangeWrapper_().getEndNode());
295
};
296
297
298
/** @override */
299
goog.dom.TextRange.prototype.getEndOffset = function() {
300
return this.endOffset_ != null ?
301
this.endOffset_ :
302
(this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset());
303
};
304
305
306
/** @override */
307
goog.dom.TextRange.prototype.getEndPosition = function() {
308
return this.getBrowserRangeWrapper_().getEndPosition();
309
};
310
311
312
/**
313
* Moves a TextRange to the provided nodes and offsets.
314
* @param {Node} startNode The node to start with.
315
* @param {number} startOffset The offset within the node to start.
316
* @param {Node} endNode The node to end with.
317
* @param {number} endOffset The offset within the node to end.
318
* @param {boolean} isReversed Whether the range is reversed.
319
*/
320
goog.dom.TextRange.prototype.moveToNodes = function(
321
startNode, startOffset, endNode, endOffset, isReversed) {
322
this.startNode_ = startNode;
323
this.startOffset_ = startOffset;
324
this.endNode_ = endNode;
325
this.endOffset_ = endOffset;
326
this.isReversed_ = isReversed;
327
this.browserRangeWrapper_ = null;
328
};
329
330
331
/** @override */
332
goog.dom.TextRange.prototype.isReversed = function() {
333
return this.isReversed_;
334
};
335
336
337
/** @override */
338
goog.dom.TextRange.prototype.containsRange = function(
339
otherRange, opt_allowPartial) {
340
var otherRangeType = otherRange.getType();
341
if (otherRangeType == goog.dom.RangeType.TEXT) {
342
return this.getBrowserRangeWrapper_().containsRange(
343
otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
344
} else if (otherRangeType == goog.dom.RangeType.CONTROL) {
345
var elements = otherRange.getElements();
346
var fn = opt_allowPartial ? goog.array.some : goog.array.every;
347
return fn(
348
elements,
349
/**
350
* @this {goog.dom.TextRange}
351
* @param {!Element} el
352
* @return {boolean}
353
*/
354
function(el) {
355
return this.containsNode(el, opt_allowPartial);
356
},
357
this);
358
}
359
return false;
360
};
361
362
363
/** @override */
364
goog.dom.TextRange.prototype.containsNode = function(node, opt_allowPartial) {
365
return this.containsRange(
366
goog.dom.TextRange.createFromNodeContents(node), opt_allowPartial);
367
};
368
369
370
371
/**
372
* Tests if the given node is in a document.
373
* @param {Node} node The node to check.
374
* @return {boolean} Whether the given node is in the given document.
375
*/
376
goog.dom.TextRange.isAttachedNode = function(node) {
377
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
378
var returnValue = false;
379
380
try {
381
returnValue = node.parentNode;
382
} catch (e) {
383
// IE sometimes throws Invalid Argument errors when a node is detached.
384
// Note: trying to return a value from the above try block can cause IE
385
// to crash. It is necessary to use the local returnValue
386
}
387
return !!returnValue;
388
} else {
389
return goog.dom.contains(node.ownerDocument.body, node);
390
}
391
};
392
393
394
/** @override */
395
goog.dom.TextRange.prototype.isRangeInDocument = function() {
396
// Ensure any cached nodes are in the document. IE also allows ranges to
397
// become detached, so we check if the range is still in the document as
398
// well for IE.
399
return (!this.startNode_ ||
400
goog.dom.TextRange.isAttachedNode(this.startNode_)) &&
401
(!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) &&
402
(!(goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||
403
this.getBrowserRangeWrapper_().isRangeInDocument());
404
};
405
406
407
/** @override */
408
goog.dom.TextRange.prototype.isCollapsed = function() {
409
return this.getBrowserRangeWrapper_().isCollapsed();
410
};
411
412
413
/** @override */
414
goog.dom.TextRange.prototype.getText = function() {
415
return this.getBrowserRangeWrapper_().getText();
416
};
417
418
419
/** @override */
420
goog.dom.TextRange.prototype.getHtmlFragment = function() {
421
// TODO(robbyw): Generalize the code in browserrange so it is static and
422
// just takes an iterator. This would mean we don't always have to create a
423
// browser range.
424
return this.getBrowserRangeWrapper_().getHtmlFragment();
425
};
426
427
428
/** @override */
429
goog.dom.TextRange.prototype.getValidHtml = function() {
430
return this.getBrowserRangeWrapper_().getValidHtml();
431
};
432
433
434
/** @override */
435
goog.dom.TextRange.prototype.getPastableHtml = function() {
436
// TODO(robbyw): Get any attributes the table or tr has.
437
438
var html = this.getValidHtml();
439
440
if (html.match(/^\s*<td\b/i)) {
441
// Match html starting with a TD.
442
html = '<table><tbody><tr>' + html + '</tr></tbody></table>';
443
} else if (html.match(/^\s*<tr\b/i)) {
444
// Match html starting with a TR.
445
html = '<table><tbody>' + html + '</tbody></table>';
446
} else if (html.match(/^\s*<tbody\b/i)) {
447
// Match html starting with a TBODY.
448
html = '<table>' + html + '</table>';
449
} else if (html.match(/^\s*<li\b/i)) {
450
// Match html starting with an LI.
451
var container = /** @type {!Element} */ (this.getContainer());
452
var tagType = goog.dom.TagName.UL;
453
while (container) {
454
if (container.tagName == goog.dom.TagName.OL) {
455
tagType = goog.dom.TagName.OL;
456
break;
457
} else if (container.tagName == goog.dom.TagName.UL) {
458
break;
459
}
460
container = container.parentNode;
461
}
462
html = goog.string.buildString('<', tagType, '>', html, '</', tagType, '>');
463
}
464
465
return html;
466
};
467
468
469
/**
470
* Returns a TextRangeIterator over the contents of the range. Regardless of
471
* the direction of the range, the iterator will move in document order.
472
* @param {boolean=} opt_keys Unused for this iterator.
473
* @return {!goog.dom.TextRangeIterator} An iterator over tags in the range.
474
* @override
475
*/
476
goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
477
return new goog.dom.TextRangeIterator(
478
this.getStartNode(), this.getStartOffset(), this.getEndNode(),
479
this.getEndOffset());
480
};
481
482
483
// RANGE ACTIONS
484
485
486
/** @override */
487
goog.dom.TextRange.prototype.select = function() {
488
this.getBrowserRangeWrapper_().select(this.isReversed_);
489
};
490
491
492
/** @override */
493
goog.dom.TextRange.prototype.removeContents = function() {
494
this.getBrowserRangeWrapper_().removeContents();
495
this.clearCachedValues_();
496
};
497
498
499
/**
500
* Surrounds the text range with the specified element (on Mozilla) or with a
501
* clone of the specified element (on IE). Returns a reference to the
502
* surrounding element if the operation was successful; returns null if the
503
* operation failed.
504
* @param {Element} element The element with which the selection is to be
505
* surrounded.
506
* @return {Element} The surrounding element (same as the argument on Mozilla,
507
* but not on IE), or null if unsuccessful.
508
*/
509
goog.dom.TextRange.prototype.surroundContents = function(element) {
510
var output = this.getBrowserRangeWrapper_().surroundContents(element);
511
this.clearCachedValues_();
512
return output;
513
};
514
515
516
/** @override */
517
goog.dom.TextRange.prototype.insertNode = function(node, before) {
518
var output = this.getBrowserRangeWrapper_().insertNode(node, before);
519
this.clearCachedValues_();
520
return output;
521
};
522
523
524
/** @override */
525
goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
526
this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
527
this.clearCachedValues_();
528
};
529
530
531
// SAVE/RESTORE
532
533
534
/** @override */
535
goog.dom.TextRange.prototype.saveUsingDom = function() {
536
return new goog.dom.DomSavedTextRange_(this);
537
};
538
539
540
// RANGE MODIFICATION
541
542
543
/** @override */
544
goog.dom.TextRange.prototype.collapse = function(toAnchor) {
545
var toStart = this.isReversed() ? !toAnchor : toAnchor;
546
547
if (this.browserRangeWrapper_) {
548
this.browserRangeWrapper_.collapse(toStart);
549
}
550
551
if (toStart) {
552
this.endNode_ = this.startNode_;
553
this.endOffset_ = this.startOffset_;
554
} else {
555
this.startNode_ = this.endNode_;
556
this.startOffset_ = this.endOffset_;
557
}
558
559
// Collapsed ranges can't be reversed
560
this.isReversed_ = false;
561
};
562
563
564
// SAVED RANGE OBJECTS
565
566
567
568
/**
569
* A SavedRange implementation using DOM endpoints.
570
* @param {goog.dom.AbstractRange} range The range to save.
571
* @constructor
572
* @extends {goog.dom.SavedRange}
573
* @private
574
*/
575
goog.dom.DomSavedTextRange_ = function(range) {
576
goog.dom.DomSavedTextRange_.base(this, 'constructor');
577
578
/**
579
* The anchor node.
580
* @type {Node}
581
* @private
582
*/
583
this.anchorNode_ = range.getAnchorNode();
584
585
/**
586
* The anchor node offset.
587
* @type {number}
588
* @private
589
*/
590
this.anchorOffset_ = range.getAnchorOffset();
591
592
/**
593
* The focus node.
594
* @type {Node}
595
* @private
596
*/
597
this.focusNode_ = range.getFocusNode();
598
599
/**
600
* The focus node offset.
601
* @type {number}
602
* @private
603
*/
604
this.focusOffset_ = range.getFocusOffset();
605
};
606
goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
607
608
609
/**
610
* @return {!goog.dom.AbstractRange} The restored range.
611
* @override
612
*/
613
goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
614
return /** @suppress {missingRequire} */ (
615
goog.dom.Range.createFromNodes(
616
this.anchorNode_, this.anchorOffset_, this.focusNode_,
617
this.focusOffset_));
618
};
619
620
621
/** @override */
622
goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
623
goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
624
625
this.anchorNode_ = null;
626
this.focusNode_ = null;
627
};
628
629