Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/javascript/atoms/dom.js
3994 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 (element['contentEditable'] === undefined) {
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 && element['isContentEditable'] !== undefined) {
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 (value === undefined && typeof style.getPropertyValue === 'function') {
438
value = style.getPropertyValue(styleName);
439
}
440
441
if (value != 'inherit') {
442
return value !== undefined ? 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} displayedFn a function that's used
456
* to tell if the chain of ancestors or descendants are all shown.
457
* @return {boolean} Whether or not the element is visible.
458
* @private
459
*/
460
bot.dom.isShown_ = function (elem, ignoreOpacity, displayedFn) {
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, displayedFn);
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, displayedFn);
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 (!displayedFn(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
531
// Any element with hidden/collapsed visibility is not shown.
532
var visibility = bot.dom.getEffectiveStyle(e, 'visibility');
533
if (visibility == 'collapse' || visibility == 'hidden') {
534
return false;
535
}
536
537
if (!displayedFn(e)) {
538
return false;
539
}
540
// Zero-sized elements should still be considered to have positive size
541
// if they have a child element or text node with positive size, unless
542
// the element has an 'overflow' style of 'hidden'.
543
// Note: Text nodes containing only structural whitespace (with newlines
544
// or tabs) are ignored as they are likely just HTML formatting, not
545
// visible content.
546
return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' &&
547
goog.array.some(e.childNodes, function (n) {
548
if (n.nodeType == goog.dom.NodeType.TEXT) {
549
var text = n.nodeValue;
550
// Ignore text nodes that are purely structural whitespace
551
// (contain newlines or tabs and nothing else besides spaces)
552
if (/^[\s]*$/.test(text) && /[\n\r\t]/.test(text)) {
553
return false;
554
}
555
return true;
556
}
557
return bot.dom.isElement(n) && positiveSize(n);
558
});
559
}
560
if (!positiveSize(elem)) {
561
return false;
562
}
563
564
// Elements that are hidden by overflow are not shown.
565
function hiddenByOverflow(e) {
566
return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&
567
goog.array.every(e.childNodes, function (n) {
568
return !bot.dom.isElement(n) || hiddenByOverflow(n) ||
569
!positiveSize(n);
570
});
571
}
572
return !hiddenByOverflow(elem);
573
};
574
575
576
/**
577
* Determines whether an element is what a user would call "shown". This means
578
* that the element is shown in the viewport of the browser, and only has
579
* height and width greater than 0px, and that its visibility is not "hidden"
580
* and its display property is not "none".
581
* Options and Optgroup elements are treated as special cases: they are
582
* considered shown iff they have a enclosing select element that is shown.
583
*
584
* Elements in Shadow DOMs with younger shadow roots are not visible, and
585
* elements distributed into shadow DOMs check the visibility of the
586
* ancestors in the Composed DOM, rather than their ancestors in the logical
587
* DOM.
588
*
589
* @param {!Element} elem The element to consider.
590
* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity
591
* when determining whether it is shown; defaults to false.
592
* @return {boolean} Whether or not the element is visible.
593
*/
594
bot.dom.isShown = function (elem, opt_ignoreOpacity) {
595
/**
596
* Determines whether an element or its parents have `display: none` or similar CSS properties set
597
* @param {!Node} e the element
598
* @return {!boolean}
599
*/
600
function displayed(e) {
601
if (bot.dom.isElement(e)) {
602
var elem = /** @type {!Element} */ (e);
603
if ((bot.dom.getEffectiveStyle(elem, 'display') == 'none')
604
|| (bot.dom.getEffectiveStyle(elem, 'content-visibility') == 'hidden')) {
605
return false;
606
}
607
}
608
609
var parent = bot.dom.getParentNodeInComposedDom(e);
610
611
if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {
612
if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {
613
// There is a younger shadow root, which will take precedence over
614
// the shadow this element is in, thus this element won't be
615
// displayed.
616
return false;
617
} else {
618
parent = parent.host;
619
}
620
}
621
622
if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||
623
parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {
624
return true;
625
}
626
627
// Child of DETAILS element is not shown unless the DETAILS element is open
628
// or the child is a SUMMARY element.
629
if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) &&
630
!parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) {
631
return false;
632
}
633
634
return !!parent && displayed(parent);
635
}
636
637
return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);
638
};
639
640
641
/**
642
* The kind of overflow area in which an element may be located. NONE if it does
643
* not overflow any ancestor element; HIDDEN if it overflows and cannot be
644
* scrolled into view; SCROLL if it overflows but can be scrolled into view.
645
*
646
* @enum {string}
647
*/
648
bot.dom.OverflowState = {
649
NONE: 'none',
650
HIDDEN: 'hidden',
651
SCROLL: 'scroll'
652
};
653
654
655
/**
656
* Returns the overflow state of the given element.
657
*
658
* If an optional coordinate or rectangle region is provided, returns the
659
* overflow state of that region relative to the element. A coordinate is
660
* treated as a 1x1 rectangle whose top-left corner is the coordinate.
661
*
662
* @param {!Element} elem Element.
663
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
664
* Coordinate or rectangle relative to the top-left corner of the element.
665
* @return {bot.dom.OverflowState} Overflow state of the element.
666
*/
667
bot.dom.getOverflowState = function (elem, opt_region) {
668
var region = bot.dom.getClientRegion(elem, opt_region);
669
var ownerDoc = goog.dom.getOwnerDocument(elem);
670
var htmlElem = ownerDoc.documentElement;
671
var bodyElem = ownerDoc.body;
672
var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow');
673
var treatAsFixedPosition;
674
675
// Return the closest ancestor that the given element may overflow.
676
function getOverflowParent(e) {
677
var position = bot.dom.getEffectiveStyle(e, 'position');
678
if (position == 'fixed') {
679
treatAsFixedPosition = true;
680
// Fixed-position element may only overflow the viewport.
681
return e == htmlElem ? null : htmlElem;
682
} else {
683
var parent = bot.dom.getParentElement(e);
684
while (parent && !canBeOverflowed(parent)) {
685
parent = bot.dom.getParentElement(parent);
686
}
687
return parent;
688
}
689
690
function canBeOverflowed(container) {
691
// The HTML element can always be overflowed.
692
if (container == htmlElem) {
693
return true;
694
}
695
// An element cannot overflow an element with an inline or contents display style.
696
var containerDisplay = /** @type {string} */ (
697
bot.dom.getEffectiveStyle(container, 'display'));
698
if (goog.string.startsWith(containerDisplay, 'inline') ||
699
(containerDisplay == 'contents')) {
700
return false;
701
}
702
// An absolute-positioned element cannot overflow a static-positioned one.
703
if (position == 'absolute' &&
704
bot.dom.getEffectiveStyle(container, 'position') == 'static') {
705
return false;
706
}
707
return true;
708
}
709
}
710
711
// Return the x and y overflow styles for the given element.
712
function getOverflowStyles(e) {
713
// When the <html> element has an overflow style of 'visible', it assumes
714
// the overflow style of the body, and the body is really overflow:visible.
715
var overflowElem = e;
716
if (htmlOverflowStyle == 'visible') {
717
// Note: bodyElem will be null/undefined in SVG documents.
718
if (e == htmlElem && bodyElem) {
719
overflowElem = bodyElem;
720
} else if (e == bodyElem) {
721
return { x: 'visible', y: 'visible' };
722
}
723
}
724
var overflow = {
725
x: bot.dom.getEffectiveStyle(overflowElem, 'overflow-x'),
726
y: bot.dom.getEffectiveStyle(overflowElem, 'overflow-y')
727
};
728
// The <html> element cannot have a genuine 'visible' overflow style,
729
// because the viewport can't expand; 'visible' is really 'auto'.
730
if (e == htmlElem) {
731
overflow.x = overflow.x == 'visible' ? 'auto' : overflow.x;
732
overflow.y = overflow.y == 'visible' ? 'auto' : overflow.y;
733
}
734
return overflow;
735
}
736
737
// Returns the scroll offset of the given element.
738
function getScroll(e) {
739
if (e == htmlElem) {
740
return new goog.dom.DomHelper(ownerDoc).getDocumentScroll();
741
} else {
742
return new goog.math.Coordinate(e.scrollLeft, e.scrollTop);
743
}
744
}
745
746
// Check if the element overflows any ancestor element.
747
for (var container = getOverflowParent(elem);
748
!!container;
749
container = getOverflowParent(container)) {
750
var containerOverflow = getOverflowStyles(container);
751
752
// If the container has overflow:visible, the element cannot overflow it.
753
if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') {
754
continue;
755
}
756
757
var containerRect = bot.dom.getClientRect(container);
758
759
// Zero-sized containers without overflow:visible hide all descendants.
760
if (containerRect.width == 0 || containerRect.height == 0) {
761
return bot.dom.OverflowState.HIDDEN;
762
}
763
764
// Check "underflow": if an element is to the left or above the container
765
var underflowsX = region.right < containerRect.left;
766
var underflowsY = region.bottom < containerRect.top;
767
if ((underflowsX && containerOverflow.x == 'hidden') ||
768
(underflowsY && containerOverflow.y == 'hidden')) {
769
return bot.dom.OverflowState.HIDDEN;
770
} else if ((underflowsX && containerOverflow.x != 'visible') ||
771
(underflowsY && containerOverflow.y != 'visible')) {
772
// When the element is positioned to the left or above a container, we
773
// have to distinguish between the element being completely outside the
774
// container and merely scrolled out of view within the container.
775
var containerScroll = getScroll(container);
776
var unscrollableX = region.right < containerRect.left - containerScroll.x;
777
var unscrollableY = region.bottom < containerRect.top - containerScroll.y;
778
if ((unscrollableX && containerOverflow.x != 'visible') ||
779
(unscrollableY && containerOverflow.x != 'visible')) {
780
return bot.dom.OverflowState.HIDDEN;
781
}
782
var containerState = bot.dom.getOverflowState(container);
783
return containerState == bot.dom.OverflowState.HIDDEN ?
784
bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;
785
}
786
787
// Check "overflow": if an element is to the right or below a container
788
var overflowsX = region.left >= containerRect.left + containerRect.width;
789
var overflowsY = region.top >= containerRect.top + containerRect.height;
790
if ((overflowsX && containerOverflow.x == 'hidden') ||
791
(overflowsY && containerOverflow.y == 'hidden')) {
792
return bot.dom.OverflowState.HIDDEN;
793
} else if ((overflowsX && containerOverflow.x != 'visible') ||
794
(overflowsY && containerOverflow.y != 'visible')) {
795
// If the element has fixed position and falls outside the scrollable area
796
// of the document, then it is hidden.
797
if (treatAsFixedPosition) {
798
var docScroll = getScroll(container);
799
if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||
800
(region.right >= htmlElem.scrollHeight - docScroll.y)) {
801
return bot.dom.OverflowState.HIDDEN;
802
}
803
}
804
// If the element can be scrolled into view of the parent, it has a scroll
805
// state; unless the parent itself is entirely hidden by overflow, in
806
// which it is also hidden by overflow.
807
var containerState = bot.dom.getOverflowState(container);
808
return containerState == bot.dom.OverflowState.HIDDEN ?
809
bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;
810
}
811
}
812
813
// Does not overflow any ancestor.
814
return bot.dom.OverflowState.NONE;
815
};
816
817
818
/**
819
* A regular expression to match the CSS transform matrix syntax.
820
* @private {!RegExp}
821
* @const
822
*/
823
bot.dom.CSS_TRANSFORM_MATRIX_REGEX_ =
824
new RegExp('matrix\\(([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +
825
'([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +
826
'([\\d\\.\\-]+)(?:px)?, ([\\d\\.\\-]+)(?:px)?\\)');
827
828
829
/**
830
* Gets the client rectangle of the DOM element. It often returns the same value
831
* as Element.getBoundingClientRect, but is "fixed" for various scenarios:
832
* 1. Like goog.style.getClientPosition, it adjusts for the inset border in IE.
833
* 2. Gets a rect for <map>'s and <area>'s relative to the image using them.
834
* 3. Gets a rect for SVG elements representing their true bounding box.
835
* 4. Defines the client rect of the <html> element to be the window viewport.
836
*
837
* @param {!Element} elem The element to use.
838
* @return {!goog.math.Rect} The interaction box of the element.
839
*/
840
bot.dom.getClientRect = function (elem) {
841
var imageMap = bot.dom.maybeFindImageMap_(elem);
842
if (imageMap) {
843
return imageMap.rect;
844
} else if (bot.dom.isElement(elem, goog.dom.TagName.HTML)) {
845
// Define the client rect of the <html> element to be the viewport.
846
var doc = goog.dom.getOwnerDocument(elem);
847
var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(doc));
848
return new goog.math.Rect(0, 0, viewportSize.width, viewportSize.height);
849
} else {
850
var nativeRect;
851
try {
852
// TODO: in IE and Firefox, getBoundingClientRect includes stroke width,
853
// but getBBox does not.
854
nativeRect = elem.getBoundingClientRect();
855
} catch (e) {
856
// On IE < 9, calling getBoundingClientRect on an orphan element raises
857
// an "Unspecified Error". All other browsers return zeros.
858
return new goog.math.Rect(0, 0, 0, 0);
859
}
860
861
var rect = new goog.math.Rect(nativeRect.left, nativeRect.top,
862
nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);
863
864
// In IE, the element can additionally be offset by a border around the
865
// documentElement or body element that we have to subtract.
866
if (goog.userAgent.IE && elem.ownerDocument.body) {
867
var doc = goog.dom.getOwnerDocument(elem);
868
rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;
869
rect.top -= doc.documentElement.clientTop + doc.body.clientTop;
870
}
871
872
return rect;
873
}
874
};
875
876
877
/**
878
* If given a <map> or <area> element, finds the corresponding image and client
879
* rectangle of the element; otherwise returns null. The return value is an
880
* object with 'image' and 'rect' properties. When no image uses the given
881
* element, the returned rectangle is present but has zero size.
882
*
883
* @param {!Element} elem Element to test.
884
* @return {?{image: Element, rect: !goog.math.Rect}} Image and rectangle.
885
* @private
886
*/
887
bot.dom.maybeFindImageMap_ = function (elem) {
888
// If not a <map> or <area>, return null indicating so.
889
var isMap = bot.dom.isElement(elem, goog.dom.TagName.MAP);
890
if (!isMap && !bot.dom.isElement(elem, goog.dom.TagName.AREA)) {
891
return null;
892
}
893
894
// Get the <map> associated with this element, or null if none.
895
var map = isMap ? elem :
896
(bot.dom.isElement(elem.parentNode, goog.dom.TagName.MAP) ?
897
elem.parentNode : null);
898
899
var image = null, rect = null;
900
if (map && map.name) {
901
var mapDoc = goog.dom.getOwnerDocument(map);
902
903
// TODO: Restrict to applet, img, input:image, and object nodes.
904
var locator = '*[usemap="#' + map.name + '"]';
905
906
// TODO: Break dependency of bot.locators on bot.dom,
907
// so bot.locators.findElement can be called here instead.
908
image = bot.locators.css.single(locator, mapDoc);
909
910
if (image) {
911
rect = bot.dom.getClientRect(image);
912
if (!isMap && elem.shape.toLowerCase() != 'default') {
913
// Shift and crop the relative area rectangle to the map.
914
var relRect = bot.dom.getAreaRelativeRect_(elem);
915
var relX = Math.min(Math.max(relRect.left, 0), rect.width);
916
var relY = Math.min(Math.max(relRect.top, 0), rect.height);
917
var w = Math.min(relRect.width, rect.width - relX);
918
var h = Math.min(relRect.height, rect.height - relY);
919
rect = new goog.math.Rect(relX + rect.left, relY + rect.top, w, h);
920
}
921
}
922
}
923
924
return { image: image, rect: rect || new goog.math.Rect(0, 0, 0, 0) };
925
};
926
927
928
/**
929
* Returns the bounding box around an <area> element relative to its enclosing
930
* <map>. Does not apply to <area> elements with shape=='default'.
931
*
932
* @param {!Element} area Area element.
933
* @return {!goog.math.Rect} Bounding box of the area element.
934
* @private
935
*/
936
bot.dom.getAreaRelativeRect_ = function (area) {
937
var shape = area.shape.toLowerCase();
938
var coords = area.coords.split(',');
939
if (shape == 'rect' && coords.length == 4) {
940
var x = coords[0], y = coords[1];
941
return new goog.math.Rect(x, y, coords[2] - x, coords[3] - y);
942
} else if (shape == 'circle' && coords.length == 3) {
943
var centerX = coords[0], centerY = coords[1], radius = coords[2];
944
return new goog.math.Rect(centerX - radius, centerY - radius,
945
2 * radius, 2 * radius);
946
} else if (shape == 'poly' && coords.length > 2) {
947
var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;
948
for (var i = 2; i + 1 < coords.length; i += 2) {
949
minX = Math.min(minX, coords[i]);
950
maxX = Math.max(maxX, coords[i]);
951
minY = Math.min(minY, coords[i + 1]);
952
maxY = Math.max(maxY, coords[i + 1]);
953
}
954
return new goog.math.Rect(minX, minY, maxX - minX, maxY - minY);
955
}
956
return new goog.math.Rect(0, 0, 0, 0);
957
};
958
959
960
/**
961
* Gets the element's client rectangle as a box, optionally clipped to the
962
* given coordinate or rectangle relative to the client's position. A coordinate
963
* is treated as a 1x1 rectangle whose top-left corner is the coordinate.
964
*
965
* @param {!Element} elem The element.
966
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
967
* Coordinate or rectangle relative to the top-left corner of the element.
968
* @return {!goog.math.Box} The client region box.
969
*/
970
bot.dom.getClientRegion = function (elem, opt_region) {
971
var region = bot.dom.getClientRect(elem).toBox();
972
973
if (opt_region) {
974
var rect = opt_region instanceof goog.math.Rect ? opt_region :
975
new goog.math.Rect(opt_region.x, opt_region.y, 1, 1);
976
region.left = goog.math.clamp(
977
region.left + rect.left, region.left, region.right);
978
region.top = goog.math.clamp(
979
region.top + rect.top, region.top, region.bottom);
980
region.right = goog.math.clamp(
981
region.left + rect.width, region.left, region.right);
982
region.bottom = goog.math.clamp(
983
region.top + rect.height, region.top, region.bottom);
984
}
985
986
return region;
987
};
988
989
990
/**
991
* Trims leading and trailing whitespace from strings, leaving non-breaking
992
* space characters in place.
993
*
994
* @param {string} str The string to trim.
995
* @return {string} str without any leading or trailing whitespace characters
996
* except non-breaking spaces.
997
* @private
998
*/
999
bot.dom.trimExcludingNonBreakingSpaceCharacters_ = function (str) {
1000
return str.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, '');
1001
};
1002
1003
1004
/**
1005
* Helper function for getVisibleText[InDisplayedDom].
1006
* @param {!Array.<string>} lines Accumulated visible lines of text.
1007
* @return {string} cleaned up concatenated lines
1008
* @private
1009
*/
1010
bot.dom.concatenateCleanedLines_ = function (lines) {
1011
lines = goog.array.map(
1012
lines,
1013
bot.dom.trimExcludingNonBreakingSpaceCharacters_);
1014
var joined = lines.join('\n');
1015
var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined);
1016
1017
// Replace non-breakable spaces with regular ones.
1018
return trimmed.replace(/\xa0/g, ' ');
1019
};
1020
1021
1022
/**
1023
* @param {!Element} elem The element to consider.
1024
* @return {string} visible text.
1025
*/
1026
bot.dom.getVisibleText = function (elem) {
1027
var lines = [];
1028
1029
if (bot.dom.IS_SHADOW_DOM_ENABLED) {
1030
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines);
1031
} else {
1032
bot.dom.appendVisibleTextLinesFromElement_(elem, lines);
1033
}
1034
return bot.dom.concatenateCleanedLines_(lines);
1035
};
1036
1037
1038
/**
1039
* Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and
1040
* bot.dom.appendVisibleTextLinesFromElementInComposedDom_
1041
* @param {!Element} elem Element.
1042
* @param {!Array.<string>} lines Accumulated visible lines of text.
1043
* @param {function(!Element):boolean} isShownFn function to call to
1044
* tell if an element is shown
1045
* @param {function(!Node, !Array.<string>, boolean, ?string, ?string):void}
1046
* childNodeFn function to call to append lines from any child nodes
1047
* @private
1048
*/
1049
bot.dom.appendVisibleTextLinesFromElementCommon_ = function (
1050
elem, lines, isShownFn, childNodeFn) {
1051
function currLine() {
1052
return /** @type {string|undefined} */ (goog.array.peek(lines)) || '';
1053
}
1054
1055
// TODO: Add case here for textual form elements.
1056
if (bot.dom.isElement(elem, goog.dom.TagName.BR)) {
1057
lines.push('');
1058
} else {
1059
// TODO: properly handle display:run-in
1060
var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD);
1061
var display = bot.dom.getEffectiveStyle(elem, 'display');
1062
// On some browsers, table cells incorrectly show up with block styles.
1063
var isBlock = !isTD &&
1064
!goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display);
1065
1066
// Add a newline before block elems when there is text on the current line,
1067
// except when the previous sibling has a display: run-in.
1068
// Also, do not run-in the previous sibling if this element is floated.
1069
1070
var previousElementSibling = goog.dom.getPreviousElementSibling(elem);
1071
var prevDisplay = (previousElementSibling) ?
1072
bot.dom.getEffectiveStyle(previousElementSibling, 'display') : '';
1073
// TODO: getEffectiveStyle should mask this for us
1074
var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') ||
1075
bot.dom.getEffectiveStyle(elem, 'cssFloat') ||
1076
bot.dom.getEffectiveStyle(elem, 'styleFloat');
1077
var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none';
1078
if (isBlock && !runIntoThis &&
1079
!goog.string.isEmptyOrWhitespace(currLine())) {
1080
lines.push('');
1081
}
1082
1083
// This element may be considered unshown, but have a child that is
1084
// explicitly shown (e.g. this element has "visibility:hidden").
1085
// Nevertheless, any text nodes that are direct descendants of this
1086
// element will not contribute to the visible text.
1087
var shown = isShownFn(elem);
1088
1089
// All text nodes that are children of this element need to know the
1090
// effective "white-space" and "text-transform" styles to properly
1091
// compute their contribution to visible text. Compute these values once.
1092
var whitespace = null, textTransform = null;
1093
if (shown) {
1094
whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');
1095
textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');
1096
}
1097
1098
goog.array.forEach(elem.childNodes, function (node) {
1099
childNodeFn(node, lines, shown, whitespace, textTransform);
1100
});
1101
1102
var line = currLine();
1103
1104
// Here we differ from standard innerText implementations (if there were
1105
// such a thing). Usually, table cells are separated by a tab, but we
1106
// normalize tabs into single spaces.
1107
if ((isTD || display == 'table-cell') && line &&
1108
!goog.string.endsWith(line, ' ')) {
1109
lines[lines.length - 1] += ' ';
1110
}
1111
1112
// Add a newline after block elems when there is text on the current line,
1113
// and the current element isn't marked as run-in.
1114
if (isBlock && display != 'run-in' &&
1115
!goog.string.isEmptyOrWhitespace(line)) {
1116
lines.push('');
1117
}
1118
}
1119
};
1120
1121
1122
/**
1123
* @param {!Element} elem Element.
1124
* @param {!Array.<string>} lines Accumulated visible lines of text.
1125
* @private
1126
*/
1127
bot.dom.appendVisibleTextLinesFromElement_ = function (elem, lines) {
1128
bot.dom.appendVisibleTextLinesFromElementCommon_(
1129
elem, lines, bot.dom.isShown,
1130
function (node, lines, shown, whitespace, textTransform) {
1131
if (node.nodeType == goog.dom.NodeType.TEXT && shown) {
1132
var textNode = /** @type {!Text} */ (node);
1133
bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,
1134
whitespace, textTransform);
1135
} else if (bot.dom.isElement(node)) {
1136
var castElem = /** @type {!Element} */ (node);
1137
bot.dom.appendVisibleTextLinesFromElement_(castElem, lines);
1138
}
1139
});
1140
};
1141
1142
1143
/**
1144
* Elements with one of these effective "display" styles are treated as inline
1145
* display boxes and have their visible text appended to the current line.
1146
* @private {!Array.<string>}
1147
* @const
1148
*/
1149
bot.dom.INLINE_DISPLAY_BOXES_ = [
1150
'inline',
1151
'inline-block',
1152
'inline-table',
1153
'none',
1154
'table-cell',
1155
'table-column',
1156
'table-column-group'
1157
];
1158
1159
1160
/**
1161
* @param {!Text} textNode Text node.
1162
* @param {!Array.<string>} lines Accumulated visible lines of text.
1163
* @param {?string} whitespace Parent element's "white-space" style.
1164
* @param {?string} textTransform Parent element's "text-transform" style.
1165
* @private
1166
*/
1167
bot.dom.appendVisibleTextLinesFromTextNode_ = function (textNode, lines,
1168
whitespace, textTransform) {
1169
1170
// First, remove zero-width characters. Do this before regularizing spaces as
1171
// the zero-width space is both zero-width and a space, but we do not want to
1172
// make it visible by converting it to a regular space.
1173
// The replaced characters are:
1174
// U+200B: Zero-width space
1175
// U+200E: Left-to-right mark
1176
// U+200F: Right-to-left mark
1177
var text = textNode.nodeValue.replace(/[\u200b\u200e\u200f]/g, '');
1178
1179
// Canonicalize the new lines, and then collapse new lines
1180
// for the whitespace styles that collapse. See:
1181
// https://developer.mozilla.org/en/CSS/white-space
1182
text = goog.string.canonicalizeNewlines(text);
1183
if (whitespace == 'normal' || whitespace == 'nowrap') {
1184
text = text.replace(/\n/g, ' ');
1185
}
1186
1187
// For pre and pre-wrap whitespace styles, convert all breaking spaces to be
1188
// non-breaking, otherwise, collapse all breaking spaces. Breaking spaces are
1189
// converted to regular spaces by getVisibleText().
1190
if (whitespace == 'pre' || whitespace == 'pre-wrap') {
1191
text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0');
1192
} else {
1193
text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' ');
1194
}
1195
1196
if (textTransform == 'capitalize') {
1197
// 1) don't treat '_' as a separator (protects snake_case)
1198
var re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])/g;
1199
text = text.replace(re, function () {
1200
return arguments[1] + arguments[2].toUpperCase();
1201
});
1202
1203
// 2) capitalize after opening "_" or "*"
1204
// Preceded by start or a non-word (so it won't fire for snake_case)
1205
re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])([_*])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24D0-\u24E9])/g;
1206
text = text.replace(re, function () {
1207
return arguments[1] + arguments[2] + arguments[3].toUpperCase();
1208
});
1209
} else if (textTransform == 'uppercase') {
1210
text = text.toUpperCase();
1211
} else if (textTransform == 'lowercase') {
1212
text = text.toLowerCase();
1213
}
1214
1215
var currLine = lines.pop() || '';
1216
if (goog.string.endsWith(currLine, ' ') &&
1217
goog.string.startsWith(text, ' ')) {
1218
text = text.substr(1);
1219
}
1220
lines.push(currLine + text);
1221
};
1222
1223
1224
/**
1225
* Gets the opacity of a node (x-browser).
1226
* This gets the inline style opacity of the node and takes into account the
1227
* cascaded or the computed style for this node.
1228
*
1229
* @param {!Element} elem Element whose opacity has to be found.
1230
* @return {number} Opacity between 0 and 1.
1231
*/
1232
bot.dom.getOpacity = function (elem) {
1233
// TODO: Does this need to deal with rgba colors?
1234
if (!bot.userAgent.IE_DOC_PRE9) {
1235
return bot.dom.getOpacityNonIE_(elem);
1236
} else {
1237
if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') {
1238
// Filter does not apply to non positioned elements.
1239
return 1;
1240
}
1241
1242
var opacityStyle = bot.dom.getEffectiveStyle(elem, 'filter');
1243
var groups = opacityStyle.match(/^alpha\(opacity=(\d*)\)/) ||
1244
opacityStyle.match(
1245
/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/);
1246
1247
if (groups) {
1248
return Number(groups[1]) / 100;
1249
} else {
1250
return 1; // Opaque.
1251
}
1252
}
1253
};
1254
1255
1256
/**
1257
* Implementation of getOpacity for browsers that do support
1258
* the "opacity" style.
1259
*
1260
* @param {!Element} elem Element whose opacity has to be found.
1261
* @return {number} Opacity between 0 and 1.
1262
* @private
1263
*/
1264
bot.dom.getOpacityNonIE_ = function (elem) {
1265
// By default the element is opaque.
1266
var elemOpacity = 1;
1267
1268
var opacityStyle = bot.dom.getEffectiveStyle(elem, 'opacity');
1269
if (opacityStyle) {
1270
elemOpacity = Number(opacityStyle);
1271
}
1272
1273
// Let's apply the parent opacity to the element.
1274
var parentElement = bot.dom.getParentElement(elem);
1275
if (parentElement) {
1276
elemOpacity = elemOpacity * bot.dom.getOpacityNonIE_(parentElement);
1277
}
1278
return elemOpacity;
1279
};
1280
1281
1282
/**
1283
* Returns the display parent element of the given node, or null. This method
1284
* differs from bot.dom.getParentElement in the presence of ShadowDOM and
1285
* &lt;shadow&gt; or &lt;content&gt; tags. For example if
1286
* <ul>
1287
* <li>div A contains div B
1288
* <li>div B has a css class .C
1289
* <li>div A contains a Shadow DOM with a div D
1290
* <li>div D contains a contents tag selecting all items of class .C
1291
* </ul>
1292
* then calling bot.dom.getParentElement on B will return A, but calling
1293
* getDisplayParentElement on B will return D.
1294
*
1295
* @param {!Node} node The node whose parent is desired.
1296
* @return {Node} The parent node, if available, null otherwise.
1297
*/
1298
bot.dom.getParentNodeInComposedDom = function (node) {
1299
var /**@type {Node}*/ parent = node.parentNode;
1300
1301
// Shadow DOM v1
1302
if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {
1303
// Can be null on purpose, meaning it has no parent as
1304
// it hasn't yet been slotted
1305
return node.assignedSlot ? node.assignedSlot.parentNode : null;
1306
}
1307
1308
// Shadow DOM V0 (deprecated)
1309
if (node.getDestinationInsertionPoints) {
1310
var destinations = node.getDestinationInsertionPoints();
1311
if (destinations.length > 0) {
1312
return destinations[destinations.length - 1];
1313
}
1314
}
1315
1316
return parent;
1317
};
1318
1319
1320
/**
1321
* @param {!Node} node Node.
1322
* @param {!Array.<string>} lines Accumulated visible lines of text.
1323
* @param {boolean} shown whether the node is visible
1324
* @param {?string} whitespace the node's 'white-space' effectiveStyle
1325
* @param {?string} textTransform the node's 'text-transform' effectiveStyle
1326
* @private
1327
* @suppress {missingProperties}
1328
*/
1329
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_ = function (
1330
node, lines, shown, whitespace, textTransform) {
1331
1332
if (node.nodeType == goog.dom.NodeType.TEXT && shown) {
1333
var textNode = /** @type {!Text} */ (node);
1334
bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,
1335
whitespace, textTransform);
1336
} else if (bot.dom.isElement(node)) {
1337
var castElem = /** @type {!Element} */ (node);
1338
1339
if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) {
1340
var parentNode = node;
1341
while (parentNode.parentNode) {
1342
parentNode = parentNode.parentNode;
1343
}
1344
if (parentNode instanceof ShadowRoot) {
1345
// If the element is <content> and we're inside a shadow DOM then just
1346
// append the contents of the nodes that have been distributed into it.
1347
var contentElem = /** @type {!Object} */ (node);
1348
var shadowChildren;
1349
if (bot.dom.isElement(node, 'CONTENT')) {
1350
shadowChildren = contentElem.getDistributedNodes();
1351
} else {
1352
shadowChildren = contentElem.assignedNodes();
1353
}
1354
const childrenToTraverse =
1355
shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes;
1356
goog.array.forEach(childrenToTraverse, function (node) {
1357
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1358
node, lines, shown, whitespace, textTransform);
1359
});
1360
} else {
1361
// if we're not inside a shadow DOM, then we just treat <content>
1362
// as an unknown element and use anything inside the tag
1363
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(
1364
castElem, lines);
1365
}
1366
} else if (bot.dom.isElement(node, 'SHADOW')) {
1367
// if the element is <shadow> then find the owning shadowRoot
1368
var parentNode = node;
1369
while (parentNode.parentNode) {
1370
parentNode = parentNode.parentNode;
1371
}
1372
if (parentNode instanceof ShadowRoot) {
1373
var thisShadowRoot = /** @type {!ShadowRoot} */ (parentNode);
1374
if (thisShadowRoot) {
1375
// then go through the owning shadowRoots older siblings and append
1376
// their contents
1377
var olderShadowRoot = thisShadowRoot.olderShadowRoot;
1378
while (olderShadowRoot) {
1379
goog.array.forEach(
1380
olderShadowRoot.childNodes, function (childNode) {
1381
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1382
childNode, lines, shown, whitespace, textTransform);
1383
});
1384
olderShadowRoot = olderShadowRoot.olderShadowRoot;
1385
}
1386
}
1387
}
1388
} else {
1389
// otherwise append the contents of an element as per normal.
1390
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(
1391
castElem, lines);
1392
}
1393
}
1394
};
1395
1396
1397
/**
1398
* Determines whether a given node has been distributed into a ShadowDOM
1399
* element somewhere.
1400
* @param {!Node} node The node to check
1401
* @return {boolean} True if the node has been distributed.
1402
*/
1403
bot.dom.isNodeDistributedIntoShadowDom = function (node) {
1404
var elemOrText = null;
1405
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
1406
elemOrText = /** @type {!Element} */ (node);
1407
} else if (node.nodeType == goog.dom.NodeType.TEXT) {
1408
elemOrText = /** @type {!Text} */ (node);
1409
}
1410
return elemOrText != null &&
1411
(elemOrText.assignedSlot != null ||
1412
(elemOrText.getDestinationInsertionPoints &&
1413
elemOrText.getDestinationInsertionPoints().length > 0)
1414
);
1415
};
1416
1417
1418
/**
1419
* @param {!Element} elem Element.
1420
* @param {!Array.<string>} lines Accumulated visible lines of text.
1421
* @private
1422
*/
1423
bot.dom.appendVisibleTextLinesFromElementInComposedDom_ = function (
1424
elem, lines) {
1425
if (elem.shadowRoot) {
1426
// Get the effective styles from the shadow host element for text nodes in shadow DOM
1427
var whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');
1428
var textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');
1429
1430
goog.array.forEach(elem.shadowRoot.childNodes, function (node) {
1431
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1432
node, lines, true, whitespace, textTransform);
1433
});
1434
}
1435
1436
bot.dom.appendVisibleTextLinesFromElementCommon_(
1437
elem, lines, bot.dom.isShown,
1438
function (node, lines, shown, whitespace, textTransform) {
1439
// If the node has been distributed into a shadowDom element
1440
// to be displayed elsewhere, then we shouldn't append
1441
// its contents here).
1442
if (!bot.dom.isNodeDistributedIntoShadowDom(node)) {
1443
bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(
1444
node, lines, shown, whitespace, textTransform);
1445
}
1446
});
1447
};
1448
1449