Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/range.js
2868 views
1
// Copyright 2008 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 Utilties for working with ranges.
17
*
18
* @author [email protected] (Nick Santos)
19
*/
20
21
goog.provide('goog.editor.range');
22
goog.provide('goog.editor.range.Point');
23
24
goog.require('goog.array');
25
goog.require('goog.dom');
26
goog.require('goog.dom.NodeType');
27
goog.require('goog.dom.Range');
28
goog.require('goog.dom.RangeEndpoint');
29
goog.require('goog.dom.SavedCaretRange');
30
goog.require('goog.editor.node');
31
goog.require('goog.editor.style');
32
goog.require('goog.iter');
33
goog.require('goog.userAgent');
34
35
36
/**
37
* Given a range and an element, create a narrower range that is limited to the
38
* boundaries of the element. If the range starts (or ends) outside the
39
* element, the narrowed range's start point (or end point) will be the
40
* leftmost (or rightmost) leaf of the element.
41
* @param {goog.dom.AbstractRange} range The range.
42
* @param {Element} el The element to limit the range to.
43
* @return {goog.dom.AbstractRange} A new narrowed range, or null if the
44
* element does not contain any part of the given range.
45
*/
46
goog.editor.range.narrow = function(range, el) {
47
var startContainer = range.getStartNode();
48
var endContainer = range.getEndNode();
49
50
if (startContainer && endContainer) {
51
var isElement = function(node) { return node == el; };
52
var hasStart = goog.dom.getAncestor(startContainer, isElement, true);
53
var hasEnd = goog.dom.getAncestor(endContainer, isElement, true);
54
55
if (hasStart && hasEnd) {
56
// The range is contained entirely within this element.
57
return range.clone();
58
} else if (hasStart) {
59
// The range starts inside the element, but ends outside it.
60
var leaf = goog.editor.node.getRightMostLeaf(el);
61
return goog.dom.Range.createFromNodes(
62
range.getStartNode(), range.getStartOffset(), leaf,
63
goog.editor.node.getLength(leaf));
64
} else if (hasEnd) {
65
// The range starts outside the element, but ends inside it.
66
return goog.dom.Range.createFromNodes(
67
goog.editor.node.getLeftMostLeaf(el), 0, range.getEndNode(),
68
range.getEndOffset());
69
}
70
}
71
72
// The selection starts and ends outside the element.
73
return null;
74
};
75
76
77
/**
78
* Given a range, expand the range to include outer tags if the full contents of
79
* those tags are entirely selected. This essentially changes the dom position,
80
* but not the visible position of the range.
81
* Ex. <code><li>foo</li></code> if "foo" is selected, instead of returning
82
* start and end nodes as the foo text node, return the li.
83
* @param {goog.dom.AbstractRange} range The range.
84
* @param {Node=} opt_stopNode Optional node to stop expanding past.
85
* @return {!goog.dom.AbstractRange} The expanded range.
86
*/
87
goog.editor.range.expand = function(range, opt_stopNode) {
88
// Expand the start out to the common container.
89
var expandedRange = goog.editor.range.expandEndPointToContainer_(
90
range, goog.dom.RangeEndpoint.START, opt_stopNode);
91
// Expand the end out to the common container.
92
expandedRange = goog.editor.range.expandEndPointToContainer_(
93
expandedRange, goog.dom.RangeEndpoint.END, opt_stopNode);
94
95
var startNode = expandedRange.getStartNode();
96
var endNode = expandedRange.getEndNode();
97
var startOffset = expandedRange.getStartOffset();
98
var endOffset = expandedRange.getEndOffset();
99
100
// If we have reached a common container, now expand out.
101
if (startNode == endNode) {
102
while (endNode != opt_stopNode && startOffset == 0 &&
103
endOffset == goog.editor.node.getLength(endNode)) {
104
// Select the parent instead.
105
var parentNode = endNode.parentNode;
106
startOffset = goog.array.indexOf(parentNode.childNodes, endNode);
107
endOffset = startOffset + 1;
108
endNode = parentNode;
109
}
110
startNode = endNode;
111
}
112
113
return goog.dom.Range.createFromNodes(
114
startNode, startOffset, endNode, endOffset);
115
};
116
117
118
/**
119
* Given a range, expands the start or end points as far out towards the
120
* range's common container (or stopNode, if provided) as possible, while
121
* perserving the same visible position.
122
*
123
* @param {goog.dom.AbstractRange} range The range to expand.
124
* @param {goog.dom.RangeEndpoint} endpoint The endpoint to expand.
125
* @param {Node=} opt_stopNode Optional node to stop expanding past.
126
* @return {!goog.dom.AbstractRange} The expanded range.
127
* @private
128
*/
129
goog.editor.range.expandEndPointToContainer_ = function(
130
range, endpoint, opt_stopNode) {
131
var expandStart = endpoint == goog.dom.RangeEndpoint.START;
132
var node = expandStart ? range.getStartNode() : range.getEndNode();
133
var offset = expandStart ? range.getStartOffset() : range.getEndOffset();
134
var container = range.getContainerElement();
135
136
// Expand the node out until we reach the container or the stop node.
137
while (node != container && node != opt_stopNode) {
138
// It is only valid to expand the start if we are at the start of a node
139
// (offset 0) or expand the end if we are at the end of a node
140
// (offset length).
141
if (expandStart && offset != 0 ||
142
!expandStart && offset != goog.editor.node.getLength(node)) {
143
break;
144
}
145
146
var parentNode = node.parentNode;
147
var index = goog.array.indexOf(parentNode.childNodes, node);
148
offset = expandStart ? index : index + 1;
149
node = parentNode;
150
}
151
152
return goog.dom.Range.createFromNodes(
153
expandStart ? node : range.getStartNode(),
154
expandStart ? offset : range.getStartOffset(),
155
expandStart ? range.getEndNode() : node,
156
expandStart ? range.getEndOffset() : offset);
157
};
158
159
160
/**
161
* Cause the window's selection to be the start of this node.
162
* @param {Node} node The node to select the start of.
163
*/
164
goog.editor.range.selectNodeStart = function(node) {
165
goog.dom.Range.createCaret(goog.editor.node.getLeftMostLeaf(node), 0)
166
.select();
167
};
168
169
170
/**
171
* Position the cursor immediately to the left or right of "node".
172
* In Firefox, the selection parent is outside of "node", so the cursor can
173
* effectively be moved to the end of a link node, without being considered
174
* inside of it.
175
* Note: This does not always work in WebKit. In particular, if you try to
176
* place a cursor to the right of a link, typing still puts you in the link.
177
* Bug: http://bugs.webkit.org/show_bug.cgi?id=17697
178
* @param {Node} node The node to position the cursor relative to.
179
* @param {boolean} toLeft True to place it to the left, false to the right.
180
* @return {!goog.dom.AbstractRange} The newly selected range.
181
*/
182
goog.editor.range.placeCursorNextTo = function(node, toLeft) {
183
var parent = node.parentNode;
184
var offset = goog.array.indexOf(parent.childNodes, node) + (toLeft ? 0 : 1);
185
var point =
186
goog.editor.range.Point.createDeepestPoint(parent, offset, toLeft, true);
187
var range = goog.dom.Range.createCaret(point.node, point.offset);
188
range.select();
189
return range;
190
};
191
192
193
/**
194
* Normalizes the node, preserving the selection of the document.
195
*
196
* May also normalize things outside the node, if it is more efficient to do so.
197
*
198
* @param {Node} node The node to normalize.
199
*/
200
goog.editor.range.selectionPreservingNormalize = function(node) {
201
var doc = goog.dom.getOwnerDocument(node);
202
var selection = goog.dom.Range.createFromWindow(goog.dom.getWindow(doc));
203
var normalizedRange =
204
goog.editor.range.rangePreservingNormalize(node, selection);
205
if (normalizedRange) {
206
normalizedRange.select();
207
}
208
};
209
210
211
/**
212
* Manually normalizes the node in IE, since native normalize in IE causes
213
* transient problems.
214
* @param {Node} node The node to normalize.
215
* @private
216
*/
217
goog.editor.range.normalizeNodeIe_ = function(node) {
218
var lastText = null;
219
var child = node.firstChild;
220
while (child) {
221
var next = child.nextSibling;
222
if (child.nodeType == goog.dom.NodeType.TEXT) {
223
if (child.nodeValue == '') {
224
node.removeChild(child);
225
} else if (lastText) {
226
lastText.nodeValue += child.nodeValue;
227
node.removeChild(child);
228
} else {
229
lastText = child;
230
}
231
} else {
232
goog.editor.range.normalizeNodeIe_(child);
233
lastText = null;
234
}
235
child = next;
236
}
237
};
238
239
240
/**
241
* Normalizes the given node.
242
* @param {Node} node The node to normalize.
243
*/
244
goog.editor.range.normalizeNode = function(node) {
245
if (goog.userAgent.IE) {
246
goog.editor.range.normalizeNodeIe_(node);
247
} else {
248
node.normalize();
249
}
250
};
251
252
253
/**
254
* Normalizes the node, preserving a range of the document.
255
*
256
* May also normalize things outside the node, if it is more efficient to do so.
257
*
258
* @param {Node} node The node to normalize.
259
* @param {goog.dom.AbstractRange?} range The range to normalize.
260
* @return {goog.dom.AbstractRange?} The range, adjusted for normalization.
261
*/
262
goog.editor.range.rangePreservingNormalize = function(node, range) {
263
if (range) {
264
var rangeFactory = goog.editor.range.normalize(range);
265
// WebKit has broken selection affinity, so carets tend to jump out of the
266
// beginning of inline elements. This means that if we're doing the
267
// normalize as the result of a range that will later become the selection,
268
// we might not normalize something in the range after it is read back from
269
// the selection. We can't just normalize the parentNode here because WebKit
270
// can move the selection range out of multiple inline parents.
271
var container = goog.editor.style.getContainer(range.getContainerElement());
272
}
273
274
if (container) {
275
goog.editor.range.normalizeNode(
276
goog.dom.findCommonAncestor(container, node));
277
} else if (node) {
278
goog.editor.range.normalizeNode(node);
279
}
280
281
if (rangeFactory) {
282
return rangeFactory();
283
} else {
284
return null;
285
}
286
};
287
288
289
/**
290
* Get the deepest point in the DOM that's equivalent to the endpoint of the
291
* given range.
292
*
293
* @param {goog.dom.AbstractRange} range A range.
294
* @param {boolean} atStart True for the start point, false for the end point.
295
* @return {!goog.editor.range.Point} The end point, expressed as a node
296
* and an offset.
297
*/
298
goog.editor.range.getDeepEndPoint = function(range, atStart) {
299
return atStart ?
300
goog.editor.range.Point.createDeepestPoint(
301
range.getStartNode(), range.getStartOffset()) :
302
goog.editor.range.Point.createDeepestPoint(
303
range.getEndNode(), range.getEndOffset());
304
};
305
306
307
/**
308
* Given a range in the current DOM, create a factory for a range that
309
* represents the same selection in a normalized DOM. The factory function
310
* should be invoked after the DOM is normalized.
311
*
312
* All browsers do a bad job preserving ranges across DOM normalization.
313
* The issue is best described in this 5-year-old bug report:
314
* https://bugzilla.mozilla.org/show_bug.cgi?id=191864
315
* For most applications, this isn't a problem. The browsers do a good job
316
* handling un-normalized text, so there's usually no reason to normalize.
317
*
318
* The exception to this rule is the rich text editing commands
319
* execCommand and queryCommandValue, which will fail often if there are
320
* un-normalized text nodes.
321
*
322
* The factory function creates new ranges so that we can normalize the DOM
323
* without problems. It must be created before any normalization happens,
324
* and invoked after normalization happens.
325
*
326
* @param {goog.dom.AbstractRange} range The range to normalize. It may
327
* become invalid after body.normalize() is called.
328
* @return {function(): goog.dom.AbstractRange} A factory for a normalized
329
* range. Should be called after body.normalize() is called.
330
*/
331
goog.editor.range.normalize = function(range) {
332
var isReversed = range.isReversed();
333
var anchorPoint = goog.editor.range.normalizePoint_(
334
goog.editor.range.getDeepEndPoint(range, !isReversed));
335
var anchorParent = anchorPoint.getParentPoint();
336
var anchorPreviousSibling = anchorPoint.node.previousSibling;
337
if (anchorPoint.node.nodeType == goog.dom.NodeType.TEXT) {
338
anchorPoint.node = null;
339
}
340
341
var focusPoint = goog.editor.range.normalizePoint_(
342
goog.editor.range.getDeepEndPoint(range, isReversed));
343
var focusParent = focusPoint.getParentPoint();
344
var focusPreviousSibling = focusPoint.node.previousSibling;
345
if (focusPoint.node.nodeType == goog.dom.NodeType.TEXT) {
346
focusPoint.node = null;
347
}
348
349
return function() {
350
if (!anchorPoint.node && anchorPreviousSibling) {
351
// If anchorPoint.node was previously an empty text node with no siblings,
352
// anchorPreviousSibling may not have a nextSibling since that node will
353
// no longer exist. Do our best and point to the end of the previous
354
// element.
355
anchorPoint.node = anchorPreviousSibling.nextSibling;
356
if (!anchorPoint.node) {
357
anchorPoint =
358
goog.editor.range.Point.getPointAtEndOfNode(anchorPreviousSibling);
359
}
360
}
361
362
if (!focusPoint.node && focusPreviousSibling) {
363
// If focusPoint.node was previously an empty text node with no siblings,
364
// focusPreviousSibling may not have a nextSibling since that node will no
365
// longer exist. Do our best and point to the end of the previous
366
// element.
367
focusPoint.node = focusPreviousSibling.nextSibling;
368
if (!focusPoint.node) {
369
focusPoint =
370
goog.editor.range.Point.getPointAtEndOfNode(focusPreviousSibling);
371
}
372
}
373
374
return goog.dom.Range.createFromNodes(
375
anchorPoint.node || anchorParent.node.firstChild || anchorParent.node,
376
anchorPoint.offset,
377
focusPoint.node || focusParent.node.firstChild || focusParent.node,
378
focusPoint.offset);
379
};
380
};
381
382
383
/**
384
* Given a point in the current DOM, adjust it to represent the same point in
385
* a normalized DOM.
386
*
387
* See the comments on goog.editor.range.normalize for more context.
388
*
389
* @param {goog.editor.range.Point} point A point in the document.
390
* @return {!goog.editor.range.Point} The same point, for easy chaining.
391
* @private
392
*/
393
goog.editor.range.normalizePoint_ = function(point) {
394
var previous;
395
if (point.node.nodeType == goog.dom.NodeType.TEXT) {
396
// If the cursor position is in a text node,
397
// look at all the previous text siblings of the text node,
398
// and set the offset relative to the earliest text sibling.
399
for (var current = point.node.previousSibling;
400
current && current.nodeType == goog.dom.NodeType.TEXT;
401
current = current.previousSibling) {
402
point.offset += goog.editor.node.getLength(current);
403
}
404
405
previous = current;
406
} else {
407
previous = point.node.previousSibling;
408
}
409
410
var parent = point.node.parentNode;
411
point.node = previous ? previous.nextSibling : parent.firstChild;
412
return point;
413
};
414
415
416
/**
417
* Checks if a range is completely inside an editable region.
418
* @param {goog.dom.AbstractRange} range The range to test.
419
* @return {boolean} Whether the range is completely inside an editable region.
420
*/
421
goog.editor.range.isEditable = function(range) {
422
var rangeContainer = range.getContainerElement();
423
424
// Closure's implementation of getContainerElement() is a little too
425
// smart in IE when exactly one element is contained in the range.
426
// It assumes that there's a user whose intent was actually to select
427
// all that element's children, so it returns the element itself as its
428
// own containing element.
429
// This little sanity check detects this condition so we can account for it.
430
var rangeContainerIsOutsideRange =
431
range.getStartNode() != rangeContainer.parentElement;
432
433
return (rangeContainerIsOutsideRange &&
434
goog.editor.node.isEditableContainer(rangeContainer)) ||
435
goog.editor.node.isEditable(rangeContainer);
436
};
437
438
439
/**
440
* Returns whether the given range intersects with any instance of the given
441
* tag.
442
* @param {goog.dom.AbstractRange} range The range to check.
443
* @param {!goog.dom.TagName} tagName The name of the tag.
444
* @return {boolean} Whether the given range intersects with any instance of
445
* the given tag.
446
*/
447
goog.editor.range.intersectsTag = function(range, tagName) {
448
if (goog.dom.getAncestorByTagNameAndClass(
449
range.getContainerElement(), tagName)) {
450
return true;
451
}
452
453
return goog.iter.some(
454
range, function(node) { return node.tagName == tagName; });
455
};
456
457
458
459
/**
460
* One endpoint of a range, represented as a Node and and offset.
461
* @param {Node} node The node containing the point.
462
* @param {number} offset The offset of the point into the node.
463
* @constructor
464
* @final
465
*/
466
goog.editor.range.Point = function(node, offset) {
467
/**
468
* The node containing the point.
469
* @type {Node}
470
*/
471
this.node = node;
472
473
/**
474
* The offset of the point into the node.
475
* @type {number}
476
*/
477
this.offset = offset;
478
};
479
480
481
/**
482
* Gets the point of this point's node in the DOM.
483
* @return {!goog.editor.range.Point} The node's point.
484
*/
485
goog.editor.range.Point.prototype.getParentPoint = function() {
486
var parent = this.node.parentNode;
487
return new goog.editor.range.Point(
488
parent, goog.array.indexOf(parent.childNodes, this.node));
489
};
490
491
492
/**
493
* Construct the deepest possible point in the DOM that's equivalent
494
* to the given point, expressed as a node and an offset.
495
* @param {Node} node The node containing the point.
496
* @param {number} offset The offset of the point from the node.
497
* @param {boolean=} opt_trendLeft Notice that a (node, offset) pair may be
498
* equivalent to more than one descendent (node, offset) pair in the DOM.
499
* By default, we trend rightward. If this parameter is true, then we
500
* trend leftward. The tendency to fall rightward by default is for
501
* consistency with other range APIs (like placeCursorNextTo).
502
* @param {boolean=} opt_stopOnChildlessElement If true, and we encounter
503
* a Node which is an Element that cannot have children, we return a Point
504
* based on its parent rather than that Node itself.
505
* @return {!goog.editor.range.Point} A new point.
506
*/
507
goog.editor.range.Point.createDeepestPoint = function(
508
node, offset, opt_trendLeft, opt_stopOnChildlessElement) {
509
while (node.nodeType == goog.dom.NodeType.ELEMENT) {
510
var child = node.childNodes[offset];
511
if (!child && !node.lastChild) {
512
break;
513
} else if (child) {
514
var prevSibling = child.previousSibling;
515
if (opt_trendLeft && prevSibling) {
516
if (opt_stopOnChildlessElement &&
517
goog.editor.range.Point.isTerminalElement_(prevSibling)) {
518
break;
519
}
520
node = prevSibling;
521
offset = goog.editor.node.getLength(node);
522
} else {
523
if (opt_stopOnChildlessElement &&
524
goog.editor.range.Point.isTerminalElement_(child)) {
525
break;
526
}
527
node = child;
528
offset = 0;
529
}
530
} else {
531
if (opt_stopOnChildlessElement &&
532
goog.editor.range.Point.isTerminalElement_(node.lastChild)) {
533
break;
534
}
535
node = node.lastChild;
536
offset = goog.editor.node.getLength(node);
537
}
538
}
539
540
return new goog.editor.range.Point(node, offset);
541
};
542
543
544
/**
545
* Return true if the specified node is an Element that is not expected to have
546
* children. The createDeepestPoint() method should not traverse into
547
* such elements.
548
* @param {Node} node .
549
* @return {boolean} True if the node is an Element that does not contain
550
* child nodes (e.g. BR, IMG).
551
* @private
552
*/
553
goog.editor.range.Point.isTerminalElement_ = function(node) {
554
return (
555
node.nodeType == goog.dom.NodeType.ELEMENT &&
556
!goog.dom.canHaveChildren(node));
557
};
558
559
560
/**
561
* Construct a point at the very end of the given node.
562
* @param {Node} node The node to create a point for.
563
* @return {!goog.editor.range.Point} A new point.
564
*/
565
goog.editor.range.Point.getPointAtEndOfNode = function(node) {
566
return new goog.editor.range.Point(node, goog.editor.node.getLength(node));
567
};
568
569
570
/**
571
* Saves the range by inserting carets into the HTML.
572
*
573
* Unlike the regular saveUsingCarets, this SavedRange normalizes text nodes.
574
* Browsers have other bugs where they don't handle split text nodes in
575
* contentEditable regions right.
576
*
577
* @param {goog.dom.AbstractRange} range The abstract range object.
578
* @return {!goog.dom.SavedCaretRange} A saved caret range that normalizes
579
* text nodes.
580
*/
581
goog.editor.range.saveUsingNormalizedCarets = function(range) {
582
return new goog.editor.range.NormalizedCaretRange_(range);
583
};
584
585
586
587
/**
588
* Saves the range using carets, but normalizes text nodes when carets
589
* are removed.
590
* @see goog.editor.range.saveUsingNormalizedCarets
591
* @param {goog.dom.AbstractRange} range The range being saved.
592
* @constructor
593
* @extends {goog.dom.SavedCaretRange}
594
* @private
595
*/
596
goog.editor.range.NormalizedCaretRange_ = function(range) {
597
goog.dom.SavedCaretRange.call(this, range);
598
};
599
goog.inherits(
600
goog.editor.range.NormalizedCaretRange_, goog.dom.SavedCaretRange);
601
602
603
/**
604
* Normalizes text nodes whenever carets are removed from the document.
605
* @param {goog.dom.AbstractRange=} opt_range A range whose offsets have already
606
* been adjusted for caret removal; it will be adjusted and returned if it
607
* is also affected by post-removal operations, such as text node
608
* normalization.
609
* @return {goog.dom.AbstractRange|undefined} The adjusted range, if opt_range
610
* was provided.
611
* @override
612
*/
613
goog.editor.range.NormalizedCaretRange_.prototype.removeCarets = function(
614
opt_range) {
615
var startCaret = this.getCaret(true);
616
var endCaret = this.getCaret(false);
617
var node = startCaret && endCaret ?
618
goog.dom.findCommonAncestor(startCaret, endCaret) :
619
startCaret || endCaret;
620
621
goog.editor.range.NormalizedCaretRange_.superClass_.removeCarets.call(this);
622
623
if (opt_range) {
624
return goog.editor.range.rangePreservingNormalize(node, opt_range);
625
} else if (node) {
626
goog.editor.range.selectionPreservingNormalize(node);
627
}
628
};
629
630