Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/dom.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview DOM manipulation and querying routines.
20
*/
21
22
goog.provide('bot.dom');
23
24
goog.require('bot');
25
goog.require('bot.color');
26
goog.require('bot.dom.core');
27
goog.require('bot.locators.css');
28
goog.require('bot.userAgent');
29
goog.require('goog.array');
30
goog.require('goog.dom');
31
goog.require('goog.dom.DomHelper');
32
goog.require('goog.dom.NodeType');
33
goog.require('goog.dom.TagName');
34
goog.require('goog.math');
35
goog.require('goog.math.Coordinate');
36
goog.require('goog.math.Rect');
37
goog.require('goog.string');
38
goog.require('goog.style');
39
goog.require('goog.userAgent');
40
41
42
/**
43
* Whether Shadow DOM operations are supported by the browser.
44
* @const {boolean}
45
*/
46
bot.dom.IS_SHADOW_DOM_ENABLED = (typeof ShadowRoot === 'function');
47
48
49
/**
50
* Retrieves the active element for a node's owner document.
51
* @param {(!Node|!Window)} nodeOrWindow The node whose owner document to get
52
* the active element for.
53
* @return {?Element} The active element, if any.
54
*/
55
bot.dom.getActiveElement = function (nodeOrWindow) {
56
var active = goog.dom.getActiveElement(
57
goog.dom.getOwnerDocument(nodeOrWindow));
58
// IE has the habit of returning an empty object from
59
// goog.dom.getActiveElement instead of null.
60
if (goog.userAgent.IE &&
61
active &&
62
typeof active.nodeType === 'undefined') {
63
return null;
64
}
65
return active;
66
};
67
68
69
/**
70
* @const
71
*/
72
bot.dom.isElement = bot.dom.core.isElement;
73
74
75
/**
76
* Returns whether an element is in an interactable state: whether it is shown
77
* to the user, ignoring its opacity, and whether it is enabled.
78
*
79
* @param {!Element} element The element to check.
80
* @return {boolean} Whether the element is interactable.
81
* @see bot.dom.isShown.
82
* @see bot.dom.isEnabled
83
*/
84
bot.dom.isInteractable = function (element) {
85
return bot.dom.isShown(element, /*ignoreOpacity=*/true) &&
86
bot.dom.isEnabled(element) &&
87
!bot.dom.hasPointerEventsDisabled_(element);
88
};
89
90
91
/**
92
* @param {!Element} element Element.
93
* @return {boolean} Whether element is set by the CSS pointer-events property
94
* not to be interactable.
95
* @private
96
*/
97
bot.dom.hasPointerEventsDisabled_ = function (element) {
98
if (goog.userAgent.IE ||
99
(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {
100
// Don't support pointer events
101
return false;
102
}
103
return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';
104
};
105
106
107
/**
108
* @const
109
*/
110
bot.dom.isSelectable = bot.dom.core.isSelectable;
111
112
113
/**
114
* @const
115
*/
116
bot.dom.isSelected = bot.dom.core.isSelected;
117
118
119
/**
120
* List of the focusable fields, according to
121
* http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus
122
* @private {!Array.<!goog.dom.TagName>}
123
* @const
124
*/
125
bot.dom.FOCUSABLE_FORM_FIELDS_ = [
126
goog.dom.TagName.A,
127
goog.dom.TagName.AREA,
128
goog.dom.TagName.BUTTON,
129
goog.dom.TagName.INPUT,
130
goog.dom.TagName.LABEL,
131
goog.dom.TagName.SELECT,
132
goog.dom.TagName.TEXTAREA
133
];
134
135
136
/**
137
* Returns whether a node is a focusable element. An element may receive focus
138
* if it is a form field, has a non-negative tabindex, or is editable.
139
* @param {!Element} element The node to test.
140
* @return {boolean} Whether the node is focusable.
141
*/
142
bot.dom.isFocusable = function (element) {
143
return goog.array.some(bot.dom.FOCUSABLE_FORM_FIELDS_, tagNameMatches) ||
144
(bot.dom.getAttribute(element, 'tabindex') != null &&
145
Number(bot.dom.getProperty(element, 'tabIndex')) >= 0) ||
146
bot.dom.isEditable(element);
147
148
function tagNameMatches(tagName) {
149
return bot.dom.isElement(element, tagName);
150
}
151
};
152
153
154
/**
155
* @const
156
*/
157
bot.dom.getProperty = bot.dom.core.getProperty;
158
159
160
/**
161
* @const
162
*/
163
bot.dom.getAttribute = bot.dom.core.getAttribute;
164
165
166
/**
167
* List of elements that support the "disabled" attribute, as defined by the
168
* HTML 4.01 specification.
169
* @private {!Array.<!goog.dom.TagName>}
170
* @const
171
* @see http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1
172
*/
173
bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_ = [
174
goog.dom.TagName.BUTTON,
175
goog.dom.TagName.INPUT,
176
goog.dom.TagName.OPTGROUP,
177
goog.dom.TagName.OPTION,
178
goog.dom.TagName.SELECT,
179
goog.dom.TagName.TEXTAREA
180
];
181
182
183
/**
184
* Determines if an element is enabled. An element is considered enabled if it
185
* does not support the "disabled" attribute, or if it is not disabled.
186
* @param {!Element} el The element to test.
187
* @return {boolean} Whether the element is enabled.
188
*/
189
bot.dom.isEnabled = function (el) {
190
var isSupported = goog.array.some(
191
bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_,
192
function (tagName) { return bot.dom.isElement(el, tagName); });
193
if (!isSupported) {
194
return true;
195
}
196
197
if (bot.dom.getProperty(el, 'disabled')) {
198
return false;
199
}
200
201
// The element is not explicitly disabled, but if it is an OPTION or OPTGROUP,
202
// we must test if it inherits its state from a parent.
203
if (el.parentNode &&
204
el.parentNode.nodeType == goog.dom.NodeType.ELEMENT &&
205
bot.dom.isElement(el, goog.dom.TagName.OPTGROUP) ||
206
bot.dom.isElement(el, goog.dom.TagName.OPTION)) {
207
return bot.dom.isEnabled(/**@type{!Element}*/(el.parentNode));
208
}
209
210
// Is there an ancestor of the current element that is a disabled fieldset
211
// and whose child is also an ancestor-or-self of the current element but is
212
// not the first legend child of the fieldset. If so then the element is
213
// disabled.
214
return !goog.dom.getAncestor(el, function (e) {
215
var parent = e.parentNode;
216
217
if (parent &&
218
bot.dom.isElement(parent, goog.dom.TagName.FIELDSET) &&
219
bot.dom.getProperty(/** @type {!Element} */(parent), 'disabled')) {
220
if (!bot.dom.isElement(e, goog.dom.TagName.LEGEND)) {
221
return true;
222
}
223
224
var sibling = e;
225
// Are there any previous legend siblings? If so then we are not the
226
// first and the element is disabled
227
while (sibling = goog.dom.getPreviousElementSibling(sibling)) {
228
if (bot.dom.isElement(sibling, goog.dom.TagName.LEGEND)) {
229
return true;
230
}
231
}
232
}
233
return false;
234
}, true);
235
};
236
237
238
/**
239
* List of input types that create text fields.
240
* @private {!Array.<string>}
241
* @const
242
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#attr-input-type
243
*/
244
bot.dom.TEXTUAL_INPUT_TYPES_ = [
245
'text',
246
'search',
247
'tel',
248
'url',
249
'email',
250
'password',
251
'number'
252
];
253
254
255
/**
256
* TODO: Add support for designMode elements.
257
*
258
* @param {!Element} element The element to check.
259
* @return {boolean} Whether the element accepts user-typed text.
260
*/
261
bot.dom.isTextual = function (element) {
262
if (bot.dom.isElement(element, goog.dom.TagName.TEXTAREA)) {
263
return true;
264
}
265
266
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
267
var type = element.type.toLowerCase();
268
return goog.array.contains(bot.dom.TEXTUAL_INPUT_TYPES_, type);
269
}
270
271
if (bot.dom.isContentEditable(element)) {
272
return true;
273
}
274
275
return false;
276
};
277
278
279
/**
280
* @param {!Element} element The element to check.
281
* @return {boolean} Whether the element is a file input.
282
*/
283
bot.dom.isFileInput = function (element) {
284
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
285
var type = element.type.toLowerCase();
286
return type == 'file';
287
}
288
289
return false;
290
};
291
292
293
/**
294
* @param {!Element} element The element to check.
295
* @param {string} inputType The type of input to check.
296
* @return {boolean} Whether the element is an input with specified type.
297
*/
298
bot.dom.isInputType = function (element, inputType) {
299
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
300
var type = element.type.toLowerCase();
301
return type == inputType;
302
}
303
304
return false;
305
};
306
307
308
/**
309
* @param {!Element} element The element to check.
310
* @return {boolean} Whether the element is contentEditable.
311
*/
312
bot.dom.isContentEditable = function (element) {
313
// Check if browser supports contentEditable.
314
if (!goog.isDef(element['contentEditable'])) {
315
return false;
316
}
317
318
// Checking the element's isContentEditable property is preferred except for
319
// IE where that property is not reliable on IE versions 7, 8, and 9.
320
if (!goog.userAgent.IE && goog.isDef(element['isContentEditable'])) {
321
return element.isContentEditable;
322
}
323
324
// For IE and for browsers where contentEditable is supported but
325
// isContentEditable is not, traverse up the ancestors:
326
function legacyIsContentEditable(e) {
327
if (e.contentEditable == 'inherit') {
328
var parent = bot.dom.getParentElement(e);
329
return parent ? legacyIsContentEditable(parent) : false;
330
} else {
331
return e.contentEditable == 'true';
332
}
333
}
334
return legacyIsContentEditable(element);
335
};
336
337
338
/**
339
* TODO: Merge isTextual into this function and move to bot.dom.
340
* For Puppet, requires adding support to getVisibleText for grabbing
341
* text from all textual elements.
342
*
343
* Whether the element may contain text the user can edit.
344
*
345
* @param {!Element} element The element to check.
346
* @return {boolean} Whether the element accepts user-typed text.
347
*/
348
bot.dom.isEditable = function (element) {
349
return (bot.dom.isTextual(element) ||
350
bot.dom.isFileInput(element) ||
351
bot.dom.isInputType(element, 'range') ||
352
bot.dom.isInputType(element, 'date') ||
353
bot.dom.isInputType(element, 'month') ||
354
bot.dom.isInputType(element, 'week') ||
355
bot.dom.isInputType(element, 'time') ||
356
bot.dom.isInputType(element, 'datetime-local') ||
357
bot.dom.isInputType(element, 'color')) &&
358
!bot.dom.getProperty(element, 'readOnly');
359
};
360
361
362
/**
363
* Returns the parent element of the given node, or null. This is required
364
* because the parent node may not be another element.
365
*
366
* @param {!Node} node The node who's parent is desired.
367
* @return {Element} The parent element, if available, null otherwise.
368
*/
369
bot.dom.getParentElement = function (node) {
370
var elem = node.parentNode;
371
372
while (elem &&
373
elem.nodeType != goog.dom.NodeType.ELEMENT &&
374
elem.nodeType != goog.dom.NodeType.DOCUMENT &&
375
elem.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
376
elem = elem.parentNode;
377
}
378
return /** @type {Element} */ (bot.dom.isElement(elem) ? elem : null);
379
};
380
381
382
/**
383
* Retrieves an explicitly-set, inline style value of an element. This returns
384
* '' if there isn't a style attribute on the element or if this style property
385
* has not been explicitly set in script.
386
*
387
* @param {!Element} elem Element to get the style value from.
388
* @param {string} styleName Name of the style property in selector-case.
389
* @return {string} The value of the style property.
390
*/
391
bot.dom.getInlineStyle = function (elem, styleName) {
392
return goog.style.getStyle(elem, styleName);
393
};
394
395
396
/**
397
* Retrieves the implicitly-set, effective style of an element, or null if it is
398
* unknown. It returns the computed style where available; otherwise it looks
399
* up the DOM tree for the first style value not equal to 'inherit,' using the
400
* IE currentStyle of each node if available, and otherwise the inline style.
401
* Since the computed, current, and inline styles can be different, the return
402
* value of this function is not always consistent across browsers. See:
403
* http://code.google.com/p/doctype/wiki/ArticleComputedStyleVsCascadedStyle
404
*
405
* @param {!Element} elem Element to get the style value from.
406
* @param {string} propertyName Name of the CSS property.
407
* @return {?string} The value of the style property, or null.
408
*/
409
bot.dom.getEffectiveStyle = function (elem, propertyName) {
410
var styleName = goog.string.toCamelCase(propertyName);
411
if (styleName == 'float' ||
412
styleName == 'cssFloat' ||
413
styleName == 'styleFloat') {
414
styleName = bot.userAgent.IE_DOC_PRE9 ? 'styleFloat' : 'cssFloat';
415
}
416
var style = goog.style.getComputedStyle(elem, styleName) ||
417
bot.dom.getCascadedStyle_(elem, styleName);
418
if (style === null) {
419
return null;
420
}
421
return bot.color.standardizeColor(styleName, style);
422
};
423
424
425
/**
426
* Looks up the DOM tree for the first style value not equal to 'inherit,' using
427
* the currentStyle of each node if available, and otherwise the inline style.
428
*
429
* @param {!Element} elem Element to get the style value from.
430
* @param {string} styleName CSS style property in camelCase.
431
* @return {?string} The value of the style property, or null.
432
* @private
433
*/
434
bot.dom.getCascadedStyle_ = function (elem, styleName) {
435
var style = elem.currentStyle || elem.style;
436
var value = style[styleName];
437
if (!goog.isDef(value) && goog.isFunction(style.getPropertyValue)) {
438
value = style.getPropertyValue(styleName);
439
}
440
441
if (value != 'inherit') {
442
return goog.isDef(value) ? value : null;
443
}
444
var parent = bot.dom.getParentElement(elem);
445
return parent ? bot.dom.getCascadedStyle_(parent, styleName) : null;
446
};
447
448
449
/**
450
* Extracted code from bot.dom.isShown.
451
*
452
* @param {!Element} elem The element to consider.
453
* @param {boolean} ignoreOpacity Whether to ignore the element's opacity
454
* when determining whether it is shown.
455
* @param {function(!Element):boolean} parentsDisplayedFn a function that's used
456
* to tell if the chain of ancestors are all shown.
457
* @return {boolean} Whether or not the element is visible.
458
* @private
459
*/
460
bot.dom.isShown_ = function (elem, ignoreOpacity, parentsDisplayedFn) {
461
if (!bot.dom.isElement(elem)) {
462
throw new Error('Argument to isShown must be of type Element');
463
}
464
465
// By convention, BODY element is always shown: BODY represents the document
466
// and even if there's nothing rendered in there, user can always see there's
467
// the document.
468
if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) {
469
return true;
470
}
471
472
// Option or optgroup is shown iff enclosing select is shown (ignoring the
473
// select's opacity).
474
if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) ||
475
bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) {
476
var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function (e) {
477
return bot.dom.isElement(e, goog.dom.TagName.SELECT);
478
}));
479
return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn);
480
}
481
482
// Image map elements are shown if image that uses it is shown, and
483
// the area of the element is positive.
484
var imageMap = bot.dom.maybeFindImageMap_(elem);
485
if (imageMap) {
486
return !!imageMap.image &&
487
imageMap.rect.width > 0 && imageMap.rect.height > 0 &&
488
bot.dom.isShown_(
489
imageMap.image, ignoreOpacity, parentsDisplayedFn);
490
}
491
492
// Any hidden input is not shown.
493
if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) &&
494
elem.type.toLowerCase() == 'hidden') {
495
return false;
496
}
497
498
// Any NOSCRIPT element is not shown.
499
if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) {
500
return false;
501
}
502
503
// Any element with hidden/collapsed visibility is not shown.
504
var visibility = bot.dom.getEffectiveStyle(elem, 'visibility');
505
if (visibility == 'collapse' || visibility == 'hidden') {
506
return false;
507
}
508
509
if (!parentsDisplayedFn(elem)) {
510
return false;
511
}
512
513
// Any transparent element is not shown.
514
if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) {
515
return false;
516
}
517
518
// Any element without positive size dimensions is not shown.
519
function positiveSize(e) {
520
var rect = bot.dom.getClientRect(e);
521
if (rect.height > 0 && rect.width > 0) {
522
return true;
523
}
524
// A vertical or horizontal SVG Path element will report zero width or
525
// height but is "shown" if it has a positive stroke-width.
526
if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) {
527
var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');
528
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
529
}
530
// Zero-sized elements should still be considered to have positive size
531
// if they have a child element or text node with positive size, unless
532
// the element has an 'overflow' style of 'hidden'.
533
return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' &&
534
goog.array.some(e.childNodes, function (n) {
535
return n.nodeType == goog.dom.NodeType.TEXT ||
536
(bot.dom.isElement(n) && positiveSize(n));
537
});
538
}
539
if (!positiveSize(elem)) {
540
return false;
541
}
542
543
// Elements that are hidden by overflow are not shown.
544
function hiddenByOverflow(e) {
545
return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&
546
goog.array.every(e.childNodes, function (n) {
547
return !bot.dom.isElement(n) || hiddenByOverflow(n) ||
548
!positiveSize(n);
549
});
550
}
551
return !hiddenByOverflow(elem);
552
};
553
554
555
/**
556
* Determines whether an element is what a user would call "shown". This means
557
* that the element is shown in the viewport of the browser, and only has
558
* height and width greater than 0px, and that its visibility is not "hidden"
559
* and its display property is not "none".
560
* Options and Optgroup elements are treated as special cases: they are
561
* considered shown iff they have a enclosing select element that is shown.
562
*
563
* Elements in Shadow DOMs with younger shadow roots are not visible, and
564
* elements distributed into shadow DOMs check the visibility of the
565
* ancestors in the Composed DOM, rather than their ancestors in the logical
566
* DOM.
567
*
568
* @param {!Element} elem The element to consider.
569
* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity
570
* when determining whether it is shown; defaults to false.
571
* @return {boolean} Whether or not the element is visible.
572
*/
573
bot.dom.isShown = function (elem, opt_ignoreOpacity) {
574
/**
575
* Determines whether an element or its parents have `display: none` set
576
* @param {!Node} e the element
577
* @return {!boolean}
578
*/
579
function displayed(e) {
580
if (bot.dom.isElement(e)) {
581
var elem = /** @type {!Element} */ (e);
582
if ((bot.dom.getEffectiveStyle(elem, 'display') == 'none')
583
|| (bot.dom.getEffectiveStyle(elem, 'content-visibility') == 'hidden')) {
584
return false;
585
}
586
}
587
588
var parent = bot.dom.getParentNodeInComposedDom(e);
589
590
if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {
591
if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {
592
// There is a younger shadow root, which will take precedence over
593
// the shadow this element is in, thus this element won't be
594
// displayed.
595
return false;
596
} else {
597
parent = parent.host;
598
}
599
}
600
601
if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||
602
parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {
603
return true;
604
}
605
606
// Child of DETAILS element is not shown unless the DETAILS element is open
607
// or the child is a SUMMARY element.
608
if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) &&
609
!parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) {
610
return false;
611
}
612
613
return !!parent && displayed(parent);
614
}
615
616
return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);
617
};
618
619
620
/**
621
* The kind of overflow area in which an element may be located. NONE if it does
622
* not overflow any ancestor element; HIDDEN if it overflows and cannot be
623
* scrolled into view; SCROLL if it overflows but can be scrolled into view.
624
*
625
* @enum {string}
626
*/
627
bot.dom.OverflowState = {
628
NONE: 'none',
629
HIDDEN: 'hidden',
630
SCROLL: 'scroll'
631
};
632
633
634
/**
635
* Returns the overflow state of the given element.
636
*
637
* If an optional coordinate or rectangle region is provided, returns the
638
* overflow state of that region relative to the element. A coordinate is
639
* treated as a 1x1 rectangle whose top-left corner is the coordinate.
640
*
641
* @param {!Element} elem Element.
642
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
643
* Coordinate or rectangle relative to the top-left corner of the element.
644
* @return {bot.dom.OverflowState} Overflow state of the element.
645
*/
646
bot.dom.getOverflowState = function (elem, opt_region) {
647
var region = bot.dom.getClientRegion(elem, opt_region);
648
var ownerDoc = goog.dom.getOwnerDocument(elem);
649
var htmlElem = ownerDoc.documentElement;
650
var bodyElem = ownerDoc.body;
651
var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow');
652
var treatAsFixedPosition;
653
654
// Return the closest ancestor that the given element may overflow.
655
function getOverflowParent(e) {
656
var position = bot.dom.getEffectiveStyle(e, 'position');
657
if (position == 'fixed') {
658
treatAsFixedPosition = true;
659
// Fixed-position element may only overflow the viewport.
660
return e == htmlElem ? null : htmlElem;
661
} else {
662
var parent = bot.dom.getParentElement(e);
663
while (parent && !canBeOverflowed(parent)) {
664
parent = bot.dom.getParentElement(parent);
665
}
666
return parent;
667
}
668
669
function canBeOverflowed(container) {
670
// The HTML element can always be overflowed.
671
if (container == htmlElem) {
672
return true;
673
}
674
// An element cannot overflow an element with an inline or contents display style.
675
var containerDisplay = /** @type {string} */ (
676
bot.dom.getEffectiveStyle(container, 'display'));
677
if (goog.string.startsWith(containerDisplay, 'inline') ||
678
(containerDisplay == 'contents')) {
679
return false;
680
}
681
// An absolute-positioned element cannot overflow a static-positioned one.
682
if (position == 'absolute' &&
683
bot.dom.getEffectiveStyle(container, 'position') == 'static') {
684
return false;
685
}
686
return true;
687
}
688
}
689
690
// Return the x and y overflow styles for the given element.
691
function getOverflowStyles(e) {
692
// When the <html> element has an overflow style of 'visible', it assumes
693
// the overflow style of the body, and the body is really overflow:visible.
694
var overflowElem = e;
695
if (htmlOverflowStyle == 'visible') {
696
// Note: bodyElem will be null/undefined in SVG documents.
697
if (e == htmlElem && bodyElem) {
698
overflowElem = bodyElem;
699
} else if (e == bodyElem) {
700
return { x: 'visible', y: 'visible' };
701
}
702
}
703
var overflow = {
704
x: bot.dom.getEffectiveStyle(overflowElem, 'overflow-x'),
705
y: bot.dom.getEffectiveStyle(overflowElem, 'overflow-y')
706
};
707
// The <html> element cannot have a genuine 'visible' overflow style,
708
// because the viewport can't expand; 'visible' is really 'auto'.
709
if (e == htmlElem) {
710
overflow.x = overflow.x == 'visible' ? 'auto' : overflow.x;
711
overflow.y = overflow.y == 'visible' ? 'auto' : overflow.y;
712
}
713
return overflow;
714
}
715
716
// Returns the scroll offset of the given element.
717
function getScroll(e) {
718
if (e == htmlElem) {
719
return new goog.dom.DomHelper(ownerDoc).getDocumentScroll();
720
} else {
721
return new goog.math.Coordinate(e.scrollLeft, e.scrollTop);
722
}
723
}
724
725
// Check if the element overflows any ancestor element.
726
for (var container = getOverflowParent(elem);
727
!!container;
728
container = getOverflowParent(container)) {
729
var containerOverflow = getOverflowStyles(container);
730
731
// If the container has overflow:visible, the element cannot overflow it.
732
if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') {
733
continue;
734
}
735
736
var containerRect = bot.dom.getClientRect(container);
737
738
// Zero-sized containers without overflow:visible hide all descendants.
739
if (containerRect.width == 0 || containerRect.height == 0) {
740
return bot.dom.OverflowState.HIDDEN;
741
}
742
743
// Check "underflow": if an element is to the left or above the container
744
var underflowsX = region.right < containerRect.left;
745
var underflowsY = region.bottom < containerRect.top;
746
if ((underflowsX && containerOverflow.x == 'hidden') ||
747
(underflowsY && containerOverflow.y == 'hidden')) {
748
return bot.dom.OverflowState.HIDDEN;
749
} else if ((underflowsX && containerOverflow.x != 'visible') ||
750
(underflowsY && containerOverflow.y != 'visible')) {
751
// When the element is positioned to the left or above a container, we
752
// have to distinguish between the element being completely outside the
753
// container and merely scrolled out of view within the container.
754
var containerScroll = getScroll(container);
755
var unscrollableX = region.right < containerRect.left - containerScroll.x;
756
var unscrollableY = region.bottom < containerRect.top - containerScroll.y;
757
if ((unscrollableX && containerOverflow.x != 'visible') ||
758
(unscrollableY && containerOverflow.x != 'visible')) {
759
return bot.dom.OverflowState.HIDDEN;
760
}
761
var containerState = bot.dom.getOverflowState(container);
762
return containerState == bot.dom.OverflowState.HIDDEN ?
763
bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;
764
}
765
766
// Check "overflow": if an element is to the right or below a container
767
var overflowsX = region.left >= containerRect.left + containerRect.width;
768
var overflowsY = region.top >= containerRect.top + containerRect.height;
769
if ((overflowsX && containerOverflow.x == 'hidden') ||
770
(overflowsY && containerOverflow.y == 'hidden')) {
771
return bot.dom.OverflowState.HIDDEN;
772
} else if ((overflowsX && containerOverflow.x != 'visible') ||
773
(overflowsY && containerOverflow.y != 'visible')) {
774
// If the element has fixed position and falls outside the scrollable area
775
// of the document, then it is hidden.
776
if (treatAsFixedPosition) {
777
var docScroll = getScroll(container);
778
if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||
779
(region.right >= htmlElem.scrollHeight - docScroll.y)) {
780
return bot.dom.OverflowState.HIDDEN;
781
}
782
}
783
// If the element can be scrolled into view of the parent, it has a scroll
784
// state; unless the parent itself is entirely hidden by overflow, in
785
// which it is also hidden by overflow.
786
var containerState = bot.dom.getOverflowState(container);
787
return containerState == bot.dom.OverflowState.HIDDEN ?
788
bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;
789
}
790
}
791
792
// Does not overflow any ancestor.
793
return bot.dom.OverflowState.NONE;
794
};
795
796
797
/**
798
* A regular expression to match the CSS transform matrix syntax.
799
* @private {!RegExp}
800
* @const
801
*/
802
bot.dom.CSS_TRANSFORM_MATRIX_REGEX_ =
803
new RegExp('matrix\\(([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +
804
'([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +
805
'([\\d\\.\\-]+)(?:px)?, ([\\d\\.\\-]+)(?:px)?\\)');
806
807
808
/**
809
* Gets the client rectangle of the DOM element. It often returns the same value
810
* as Element.getBoundingClientRect, but is "fixed" for various scenarios:
811
* 1. Like goog.style.getClientPosition, it adjusts for the inset border in IE.
812
* 2. Gets a rect for <map>'s and <area>'s relative to the image using them.
813
* 3. Gets a rect for SVG elements representing their true bounding box.
814
* 4. Defines the client rect of the <html> element to be the window viewport.
815
*
816
* @param {!Element} elem The element to use.
817
* @return {!goog.math.Rect} The interaction box of the element.
818
*/
819
bot.dom.getClientRect = function (elem) {
820
var imageMap = bot.dom.maybeFindImageMap_(elem);
821
if (imageMap) {
822
return imageMap.rect;
823
} else if (bot.dom.isElement(elem, goog.dom.TagName.HTML)) {
824
// Define the client rect of the <html> element to be the viewport.
825
var doc = goog.dom.getOwnerDocument(elem);
826
var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(doc));
827
return new goog.math.Rect(0, 0, viewportSize.width, viewportSize.height);
828
} else {
829
var nativeRect;
830
try {
831
// TODO: in IE and Firefox, getBoundingClientRect includes stroke width,
832
// but getBBox does not.
833
nativeRect = elem.getBoundingClientRect();
834
} catch (e) {
835
// On IE < 9, calling getBoundingClientRect on an orphan element raises
836
// an "Unspecified Error". All other browsers return zeros.
837
return new goog.math.Rect(0, 0, 0, 0);
838
}
839
840
var rect = new goog.math.Rect(nativeRect.left, nativeRect.top,
841
nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);
842
843
// In IE, the element can additionally be offset by a border around the
844
// documentElement or body element that we have to subtract.
845
if (goog.userAgent.IE && elem.ownerDocument.body) {
846
var doc = goog.dom.getOwnerDocument(elem);
847
rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;
848
rect.top -= doc.documentElement.clientTop + doc.body.clientTop;
849
}
850
851
return rect;
852
}
853
};
854
855
856
/**
857
* If given a <map> or <area> element, finds the corresponding image and client
858
* rectangle of the element; otherwise returns null. The return value is an
859
* object with 'image' and 'rect' properties. When no image uses the given
860
* element, the returned rectangle is present but has zero size.
861
*
862
* @param {!Element} elem Element to test.
863
* @return {?{image: Element, rect: !goog.math.Rect}} Image and rectangle.
864
* @private
865
*/
866
bot.dom.maybeFindImageMap_ = function (elem) {
867
// If not a <map> or <area>, return null indicating so.
868
var isMap = bot.dom.isElement(elem, goog.dom.TagName.MAP);
869
if (!isMap && !bot.dom.isElement(elem, goog.dom.TagName.AREA)) {
870
return null;
871
}
872
873
// Get the <map> associated with this element, or null if none.
874
var map = isMap ? elem :
875
(bot.dom.isElement(elem.parentNode, goog.dom.TagName.MAP) ?
876
elem.parentNode : null);
877
878
var image = null, rect = null;
879
if (map && map.name) {
880
var mapDoc = goog.dom.getOwnerDocument(map);
881
882
// TODO: Restrict to applet, img, input:image, and object nodes.
883
var locator = '*[usemap="#' + map.name + '"]';
884
885
// TODO: Break dependency of bot.locators on bot.dom,
886
// so bot.locators.findElement can be called here instead.
887
image = bot.locators.css.single(locator, mapDoc);
888
889
if (image) {
890
rect = bot.dom.getClientRect(image);
891
if (!isMap && elem.shape.toLowerCase() != 'default') {
892
// Shift and crop the relative area rectangle to the map.
893
var relRect = bot.dom.getAreaRelativeRect_(elem);
894
var relX = Math.min(Math.max(relRect.left, 0), rect.width);
895
var relY = Math.min(Math.max(relRect.top, 0), rect.height);
896
var w = Math.min(relRect.width, rect.width - relX);
897
var h = Math.min(relRect.height, rect.height - relY);
898
rect = new goog.math.Rect(relX + rect.left, relY + rect.top, w, h);
899
}
900
}
901
}
902
903
return { image: image, rect: rect || new goog.math.Rect(0, 0, 0, 0) };
904
};
905
906
907
/**
908
* Returns the bounding box around an <area> element relative to its enclosing
909
* <map>. Does not apply to <area> elements with shape=='default'.
910
*
911
* @param {!Element} area Area element.
912
* @return {!goog.math.Rect} Bounding box of the area element.
913
* @private
914
*/
915
bot.dom.getAreaRelativeRect_ = function (area) {
916
var shape = area.shape.toLowerCase();
917
var coords = area.coords.split(',');
918
if (shape == 'rect' && coords.length == 4) {
919
var x = coords[0], y = coords[1];
920
return new goog.math.Rect(x, y, coords[2] - x, coords[3] - y);
921
} else if (shape == 'circle' && coords.length == 3) {
922
var centerX = coords[0], centerY = coords[1], radius = coords[2];
923
return new goog.math.Rect(centerX - radius, centerY - radius,
924
2 * radius, 2 * radius);
925
} else if (shape == 'poly' && coords.length > 2) {
926
var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;
927
for (var i = 2; i + 1 < coords.length; i += 2) {
928
minX = Math.min(minX, coords[i]);
929
maxX = Math.max(maxX, coords[i]);
930
minY = Math.min(minY, coords[i + 1]);
931
maxY = Math.max(maxY, coords[i + 1]);
932
}
933
return new goog.math.Rect(minX, minY, maxX - minX, maxY - minY);
934
}
935
return new goog.math.Rect(0, 0, 0, 0);
936
};
937
938
939
/**
940
* Gets the element's client rectangle as a box, optionally clipped to the
941
* given coordinate or rectangle relative to the client's position. A coordinate
942
* is treated as a 1x1 rectangle whose top-left corner is the coordinate.
943
*
944
* @param {!Element} elem The element.
945
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
946
* Coordinate or rectangle relative to the top-left corner of the element.
947
* @return {!goog.math.Box} The client region box.
948
*/
949
bot.dom.getClientRegion = function (elem, opt_region) {
950
var region = bot.dom.getClientRect(elem).toBox();
951
952
if (opt_region) {
953
var rect = opt_region instanceof goog.math.Rect ? opt_region :
954
new goog.math.Rect(opt_region.x, opt_region.y, 1, 1);
955
region.left = goog.math.clamp(
956
region.left + rect.left, region.left, region.right);
957
region.top = goog.math.clamp(
958
region.top + rect.top, region.top, region.bottom);
959
region.right = goog.math.clamp(
960
region.left + rect.width, region.left, region.right);
961
region.bottom = goog.math.clamp(
962
region.top + rect.height, region.top, region.bottom);
963
}
964
965
return region;
966
};
967
968
969
/**
970
* Trims leading and trailing whitespace from strings, leaving non-breaking
971
* space characters in place.
972
*
973
* @param {string} str The string to trim.
974
* @return {string} str without any leading or trailing whitespace characters
975
* except non-breaking spaces.
976
* @private
977
*/
978
bot.dom.trimExcludingNonBreakingSpaceCharacters_ = function (str) {
979
return str.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, '');
980
};
981
982
983
/**
984
* Helper function for getVisibleText[InDisplayedDom].
985
* @param {!Array.<string>} lines Accumulated visible lines of text.
986
* @return {string} cleaned up concatenated lines
987
* @private
988
*/
989
bot.dom.concatenateCleanedLines_ = function (lines) {
990
lines = goog.array.map(
991
lines,
992
bot.dom.trimExcludingNonBreakingSpaceCharacters_);
993
var joined = lines.join('\n');
994
var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined);
995
996
// Replace non-breakable spaces with regular ones.
997
return trimmed.replace(/\xa0/g, ' ');
998
};
999
1000
1001
/**
1002
* @param {!Element} elem The element to consider.
1003
* @return {string} visible text.
1004
*/
1005
bot.dom.getVisibleText = function (elem) {
1006
var lines = [];
1007
1008
if (bot.dom.IS_SHADOW_DOM_ENABLED) {
1009
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines);
1010
} else {
1011
bot.dom.appendVisibleTextLinesFromElement_(elem, lines);
1012
}
1013
return bot.dom.concatenateCleanedLines_(lines);
1014
};
1015
1016
1017
/**
1018
* Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and
1019
* bot.dom.appendVisibleTextLinesFromElementInComposedDom_
1020
* @param {!Element} elem Element.
1021
* @param {!Array.<string>} lines Accumulated visible lines of text.
1022
* @param {function(!Element):boolean} isShownFn function to call to
1023
* tell if an element is shown
1024
* @param {function(!Node, !Array.<string>, boolean, ?string, ?string):void}
1025
* childNodeFn function to call to append lines from any child nodes
1026
* @private
1027
*/
1028
bot.dom.appendVisibleTextLinesFromElementCommon_ = function (
1029
elem, lines, isShownFn, childNodeFn) {
1030
function currLine() {
1031
return /** @type {string|undefined} */ (goog.array.peek(lines)) || '';
1032
}
1033
1034
// TODO: Add case here for textual form elements.
1035
if (bot.dom.isElement(elem, goog.dom.TagName.BR)) {
1036
lines.push('');
1037
} else {
1038
// TODO: properly handle display:run-in
1039
var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD);
1040
var display = bot.dom.getEffectiveStyle(elem, 'display');
1041
// On some browsers, table cells incorrectly show up with block styles.
1042
var isBlock = !isTD &&
1043
!goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display);
1044
1045
// Add a newline before block elems when there is text on the current line,
1046
// except when the previous sibling has a display: run-in.
1047
// Also, do not run-in the previous sibling if this element is floated.
1048
1049
var previousElementSibling = goog.dom.getPreviousElementSibling(elem);
1050
var prevDisplay = (previousElementSibling) ?
1051
bot.dom.getEffectiveStyle(previousElementSibling, 'display') : '';
1052
// TODO: getEffectiveStyle should mask this for us
1053
var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') ||
1054
bot.dom.getEffectiveStyle(elem, 'cssFloat') ||
1055
bot.dom.getEffectiveStyle(elem, 'styleFloat');
1056
var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none';
1057
if (isBlock && !runIntoThis &&
1058
!goog.string.isEmptyOrWhitespace(currLine())) {
1059
lines.push('');
1060
}
1061
1062
// This element may be considered unshown, but have a child that is
1063
// explicitly shown (e.g. this element has "visibility:hidden").
1064
// Nevertheless, any text nodes that are direct descendants of this
1065
// element will not contribute to the visible text.
1066
var shown = isShownFn(elem);
1067
1068
// All text nodes that are children of this element need to know the
1069
// effective "white-space" and "text-transform" styles to properly
1070
// compute their contribution to visible text. Compute these values once.
1071
var whitespace = null, textTransform = null;
1072
if (shown) {
1073
whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');
1074
textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');
1075
}
1076
1077
goog.array.forEach(elem.childNodes, function (node) {
1078
childNodeFn(node, lines, shown, whitespace, textTransform);
1079
});
1080
1081
var line = currLine();
1082
1083
// Here we differ from standard innerText implementations (if there were
1084
// such a thing). Usually, table cells are separated by a tab, but we
1085
// normalize tabs into single spaces.
1086
if ((isTD || display == 'table-cell') && line &&
1087
!goog.string.endsWith(line, ' ')) {
1088
lines[lines.length - 1] += ' ';
1089
}
1090
1091
// Add a newline after block elems when there is text on the current line,
1092
// and the current element isn't marked as run-in.
1093
if (isBlock && display != 'run-in' &&
1094
!goog.string.isEmptyOrWhitespace(line)) {
1095
lines.push('');
1096
}
1097
}
1098
};
1099
1100
1101
/**
1102
* @param {!Element} elem Element.
1103
* @param {!Array.<string>} lines Accumulated visible lines of text.
1104
* @private
1105
*/
1106
bot.dom.appendVisibleTextLinesFromElement_ = function (elem, lines) {
1107
bot.dom.appendVisibleTextLinesFromElementCommon_(
1108
elem, lines, bot.dom.isShown,
1109
function (node, lines, shown, whitespace, textTransform) {
1110
if (node.nodeType == goog.dom.NodeType.TEXT && shown) {
1111
var textNode = /** @type {!Text} */ (node);
1112
bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,
1113
whitespace, textTransform);
1114
} else if (bot.dom.isElement(node)) {
1115
var castElem = /** @type {!Element} */ (node);
1116
bot.dom.appendVisibleTextLinesFromElement_(castElem, lines);
1117
}
1118
});
1119
};
1120
1121
1122
/**
1123
* Elements with one of these effective "display" styles are treated as inline
1124
* display boxes and have their visible text appended to the current line.
1125
* @private {!Array.<string>}
1126
* @const
1127
*/
1128
bot.dom.INLINE_DISPLAY_BOXES_ = [
1129
'inline',
1130
'inline-block',
1131
'inline-table',
1132
'none',
1133
'table-cell',
1134
'table-column',
1135
'table-column-group'
1136
];
1137
1138
1139
/**
1140
* @param {!Text} textNode Text node.
1141
* @param {!Array.<string>} lines Accumulated visible lines of text.
1142
* @param {?string} whitespace Parent element's "white-space" style.
1143
* @param {?string} textTransform Parent element's "text-transform" style.
1144
* @private
1145
*/
1146
bot.dom.appendVisibleTextLinesFromTextNode_ = function (textNode, lines,
1147
whitespace, textTransform) {
1148
1149
// First, remove zero-width characters. Do this before regularizing spaces as
1150
// the zero-width space is both zero-width and a space, but we do not want to
1151
// make it visible by converting it to a regular space.
1152
// The replaced characters are:
1153
// U+200B: Zero-width space
1154
// U+200E: Left-to-right mark
1155
// U+200F: Right-to-left mark
1156
var text = textNode.nodeValue.replace(/[\u200b\u200e\u200f]/g, '');
1157
1158
// Canonicalize the new lines, and then collapse new lines
1159
// for the whitespace styles that collapse. See:
1160
// https://developer.mozilla.org/en/CSS/white-space
1161
text = goog.string.canonicalizeNewlines(text);
1162
if (whitespace == 'normal' || whitespace == 'nowrap') {
1163
text = text.replace(/\n/g, ' ');
1164
}
1165
1166
// For pre and pre-wrap whitespace styles, convert all breaking spaces to be
1167
// non-breaking, otherwise, collapse all breaking spaces. Breaking spaces are
1168
// converted to regular spaces by getVisibleText().
1169
if (whitespace == 'pre' || whitespace == 'pre-wrap') {
1170
text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0');
1171
} else {
1172
text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' ');
1173
}
1174
1175
if (textTransform == 'capitalize') {
1176
// the unicode regex ending with /gu does not work in IE
1177
var re = goog.userAgent.IE ? /(^|\s|\b)(\S)/g : /(^|\s|\b)(\S)/gu;
1178
text = text.replace(re, function () {
1179
return arguments[1] + arguments[2].toUpperCase();
1180
});
1181
} else if (textTransform == 'uppercase') {
1182
text = text.toUpperCase();
1183
} else if (textTransform == 'lowercase') {
1184
text = text.toLowerCase();
1185
}
1186
1187
var currLine = lines.pop() || '';
1188
if (goog.string.endsWith(currLine, ' ') &&
1189
goog.string.startsWith(text, ' ')) {
1190
text = text.substr(1);
1191
}
1192
lines.push(currLine + text);
1193
};
1194
1195
1196
/**
1197
* Gets the opacity of a node (x-browser).
1198
* This gets the inline style opacity of the node and takes into account the
1199
* cascaded or the computed style for this node.
1200
*
1201
* @param {!Element} elem Element whose opacity has to be found.
1202
* @return {number} Opacity between 0 and 1.
1203
*/
1204
bot.dom.getOpacity = function (elem) {
1205
// TODO: Does this need to deal with rgba colors?
1206
if (!bot.userAgent.IE_DOC_PRE9) {
1207
return bot.dom.getOpacityNonIE_(elem);
1208
} else {
1209
if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') {
1210
// Filter does not apply to non positioned elements.
1211
return 1;
1212
}
1213
1214
var opacityStyle = bot.dom.getEffectiveStyle(elem, 'filter');
1215
var groups = opacityStyle.match(/^alpha\(opacity=(\d*)\)/) ||
1216
opacityStyle.match(
1217
/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/);
1218
1219
if (groups) {
1220
return Number(groups[1]) / 100;
1221
} else {
1222
return 1; // Opaque.
1223
}
1224
}
1225
};
1226
1227
1228
/**
1229
* Implementation of getOpacity for browsers that do support
1230
* the "opacity" style.
1231
*
1232
* @param {!Element} elem Element whose opacity has to be found.
1233
* @return {number} Opacity between 0 and 1.
1234
* @private
1235
*/
1236
bot.dom.getOpacityNonIE_ = function (elem) {
1237
// By default the element is opaque.
1238
var elemOpacity = 1;
1239
1240
var opacityStyle = bot.dom.getEffectiveStyle(elem, 'opacity');
1241
if (opacityStyle) {
1242
elemOpacity = Number(opacityStyle);
1243
}
1244
1245
// Let's apply the parent opacity to the element.
1246
var parentElement = bot.dom.getParentElement(elem);
1247
if (parentElement) {
1248
elemOpacity = elemOpacity * bot.dom.getOpacityNonIE_(parentElement);
1249
}
1250
return elemOpacity;
1251
};
1252
1253
1254
/**
1255
* Returns the display parent element of the given node, or null. This method
1256
* differs from bot.dom.getParentElement in the presence of ShadowDOM and
1257
* &lt;shadow&gt; or &lt;content&gt; tags. For example if
1258
* <ul>
1259
* <li>div A contains div B
1260
* <li>div B has a css class .C
1261
* <li>div A contains a Shadow DOM with a div D
1262
* <li>div D contains a contents tag selecting all items of class .C
1263
* </ul>
1264
* then calling bot.dom.getParentElement on B will return A, but calling
1265
* getDisplayParentElement on B will return D.
1266
*
1267
* @param {!Node} node The node whose parent is desired.
1268
* @return {Node} The parent node, if available, null otherwise.
1269
*/
1270
bot.dom.getParentNodeInComposedDom = function (node) {
1271
var /**@type {Node}*/ parent = node.parentNode;
1272
1273
// Shadow DOM v1
1274
if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {
1275
// Can be null on purpose, meaning it has no parent as
1276
// it hasn't yet been slotted
1277
return node.assignedSlot ? node.assignedSlot.parentNode : null;
1278
}
1279
1280
// Shadow DOM V0 (deprecated)
1281
if (node.getDestinationInsertionPoints) {
1282
var destinations = node.getDestinationInsertionPoints();
1283
if (destinations.length > 0) {
1284
return destinations[destinations.length - 1];
1285
}
1286
}
1287
1288
return parent;
1289
};
1290
1291
1292
/**
1293
* @param {!Node} node Node.
1294
* @param {!Array.<string>} lines Accumulated visible lines of text.
1295
* @param {boolean} shown whether the node is visible
1296
* @param {?string} whitespace the node's 'white-space' effectiveStyle
1297
* @param {?string} textTransform the node's 'text-transform' effectiveStyle
1298
* @private
1299
* @suppress {missingProperties}
1300
*/
1301
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_ = function (
1302
node, lines, shown, whitespace, textTransform) {
1303
1304
if (node.nodeType == goog.dom.NodeType.TEXT && shown) {
1305
var textNode = /** @type {!Text} */ (node);
1306
bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,
1307
whitespace, textTransform);
1308
} else if (bot.dom.isElement(node)) {
1309
var castElem = /** @type {!Element} */ (node);
1310
1311
if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) {
1312
var parentNode = node;
1313
while (parentNode.parentNode) {
1314
parentNode = parentNode.parentNode;
1315
}
1316
if (parentNode instanceof ShadowRoot) {
1317
// If the element is <content> and we're inside a shadow DOM then just
1318
// append the contents of the nodes that have been distributed into it.
1319
var contentElem = /** @type {!Object} */ (node);
1320
var shadowChildren;
1321
if (bot.dom.isElement(node, 'CONTENT')) {
1322
shadowChildren = contentElem.getDistributedNodes();
1323
} else {
1324
shadowChildren = contentElem.assignedNodes();
1325
}
1326
const childrenToTraverse =
1327
shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes;
1328
goog.array.forEach(childrenToTraverse, function (node) {
1329
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1330
node, lines, shown, whitespace, textTransform);
1331
});
1332
} else {
1333
// if we're not inside a shadow DOM, then we just treat <content>
1334
// as an unknown element and use anything inside the tag
1335
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(
1336
castElem, lines);
1337
}
1338
} else if (bot.dom.isElement(node, 'SHADOW')) {
1339
// if the element is <shadow> then find the owning shadowRoot
1340
var parentNode = node;
1341
while (parentNode.parentNode) {
1342
parentNode = parentNode.parentNode;
1343
}
1344
if (parentNode instanceof ShadowRoot) {
1345
var thisShadowRoot = /** @type {!ShadowRoot} */ (parentNode);
1346
if (thisShadowRoot) {
1347
// then go through the owning shadowRoots older siblings and append
1348
// their contents
1349
var olderShadowRoot = thisShadowRoot.olderShadowRoot;
1350
while (olderShadowRoot) {
1351
goog.array.forEach(
1352
olderShadowRoot.childNodes, function (childNode) {
1353
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1354
childNode, lines, shown, whitespace, textTransform);
1355
});
1356
olderShadowRoot = olderShadowRoot.olderShadowRoot;
1357
}
1358
}
1359
}
1360
} else {
1361
// otherwise append the contents of an element as per normal.
1362
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(
1363
castElem, lines);
1364
}
1365
}
1366
};
1367
1368
1369
/**
1370
* Determines whether a given node has been distributed into a ShadowDOM
1371
* element somewhere.
1372
* @param {!Node} node The node to check
1373
* @return {boolean} True if the node has been distributed.
1374
*/
1375
bot.dom.isNodeDistributedIntoShadowDom = function (node) {
1376
var elemOrText = null;
1377
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
1378
elemOrText = /** @type {!Element} */ (node);
1379
} else if (node.nodeType == goog.dom.NodeType.TEXT) {
1380
elemOrText = /** @type {!Text} */ (node);
1381
}
1382
return elemOrText != null &&
1383
(elemOrText.assignedSlot != null ||
1384
(elemOrText.getDestinationInsertionPoints &&
1385
elemOrText.getDestinationInsertionPoints().length > 0)
1386
);
1387
};
1388
1389
1390
/**
1391
* @param {!Element} elem Element.
1392
* @param {!Array.<string>} lines Accumulated visible lines of text.
1393
* @private
1394
*/
1395
bot.dom.appendVisibleTextLinesFromElementInComposedDom_ = function (
1396
elem, lines) {
1397
if (elem.shadowRoot) {
1398
goog.array.forEach(elem.shadowRoot.childNodes, function (node) {
1399
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1400
node, lines, true, null, null);
1401
});
1402
}
1403
1404
bot.dom.appendVisibleTextLinesFromElementCommon_(
1405
elem, lines, bot.dom.isShown,
1406
function (node, lines, shown, whitespace, textTransform) {
1407
// If the node has been distributed into a shadowDom element
1408
// to be displayed elsewhere, then we shouldn't append
1409
// its contents here).
1410
if (!bot.dom.isNodeDistributedIntoShadowDom(node)) {
1411
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1412
node, lines, shown, whitespace, textTransform);
1413
}
1414
});
1415
};
1416
1417