-
Notifications
You must be signed in to change notification settings - Fork 792
/
Copy pathvirtual-node.js
194 lines (168 loc) · 5.87 KB
/
virtual-node.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import AbstractVirtualNode from './abstract-virtual-node';
import { isXHTML, validInputTypes } from '../../utils';
import { isFocusable, getTabbableElements } from '../../../commons/dom';
import cache from '../cache';
let nodeIndex = 0;
class VirtualNode extends AbstractVirtualNode {
/**
* Wrap the real node and provide list of the flattened children
* @param {Node} node the node in question
* @param {VirtualNode} parent The parent VirtualNode
* @param {String} shadowId the ID of the shadow DOM to which this node belongs
*/
constructor(node, parent, shadowId) {
super();
this.shadowId = shadowId;
this.children = [];
this.actualNode = node;
this.parent = parent;
if (!parent) {
nodeIndex = 0;
}
this.nodeIndex = nodeIndex++;
this._isHidden = null; // will be populated by axe.utils.isHidden
this._cache = {};
this._isXHTML = isXHTML(node.ownerDocument);
// we will normalize the type prop for inputs by looking strictly
// at the attribute and not what the browser resolves the type
// to be
if (node.nodeName.toLowerCase() === 'input') {
let type = node.getAttribute('type');
type = this._isXHTML ? type : (type || '').toLowerCase();
if (!validInputTypes().includes(type)) {
type = 'text';
}
this._type = type;
}
if (cache.get('nodeMap')) {
cache.get('nodeMap').set(node, this);
}
}
// abstract Node properties so we can run axe in DOM-less environments.
// add to the prototype so memory is shared across all virtual nodes
get props() {
if (!this._cache.hasOwnProperty('props')) {
const { nodeType, nodeName, id, nodeValue } = this.actualNode;
this._cache.props = {
nodeType,
nodeName: this._isXHTML ? nodeName : nodeName.toLowerCase(),
id,
type: this._type,
nodeValue
};
// We avoid reading these on node types where they won't be relevant
// to work around issues like #4316.
if (nodeType === 1) {
this._cache.props.multiple = this.actualNode.multiple;
this._cache.props.value = this.actualNode.value;
this._cache.props.selected = this.actualNode.selected;
this._cache.props.checked = this.actualNode.checked;
this._cache.props.indeterminate = this.actualNode.indeterminate;
}
}
return this._cache.props;
}
/**
* Get the value of the given attribute name.
* @param {String} attrName The name of the attribute.
* @return {String|null} The value of the attribute or null if the attribute does not exist
*/
attr(attrName) {
if (typeof this.actualNode.getAttribute !== 'function') {
return null;
}
return this.actualNode.getAttribute(attrName);
}
/**
* Determine if the element has the given attribute.
* @param {String} attrName The name of the attribute
* @return {Boolean} True if the element has the attribute, false otherwise.
*/
hasAttr(attrName) {
if (typeof this.actualNode.hasAttribute !== 'function') {
return false;
}
return this.actualNode.hasAttribute(attrName);
}
/**
* Return a list of attribute names for the element.
* @return {String[]}
*/
get attrNames() {
if (!this._cache.hasOwnProperty('attrNames')) {
let attrs;
// eslint-disable-next-line no-restricted-syntax
if (this.actualNode.attributes instanceof window.NamedNodeMap) {
// eslint-disable-next-line no-restricted-syntax
attrs = this.actualNode.attributes;
}
// if the attributes property is not of type NamedNodeMap
// then the DOM has been clobbered. E.g. <form><input name="attributes"></form>.
// We can clone the node to isolate it and then return
// the attributes
else {
attrs = this.actualNode.cloneNode(false).attributes;
}
this._cache.attrNames = Array.from(attrs).map(attr => attr.name);
}
return this._cache.attrNames;
}
/**
* Return a property of the computed style for this element and cache the result. This is much faster than called `getPropteryValue` every time.
* @see https://jsperf.com/get-property-value
* @return {String}
*/
getComputedStylePropertyValue(property) {
const key = 'computedStyle_' + property;
if (!this._cache.hasOwnProperty(key)) {
if (!this._cache.hasOwnProperty('computedStyle')) {
this._cache.computedStyle = window.getComputedStyle(this.actualNode);
}
this._cache[key] = this._cache.computedStyle.getPropertyValue(property);
}
return this._cache[key];
}
/**
* Determine if the element is focusable and cache the result.
* @return {Boolean} True if the element is focusable, false otherwise.
*/
get isFocusable() {
if (!this._cache.hasOwnProperty('isFocusable')) {
this._cache.isFocusable = isFocusable(this.actualNode);
}
return this._cache.isFocusable;
}
/**
* Return the list of tabbable elements for this element and cache the result.
* @return {VirtualNode[]}
*/
get tabbableElements() {
if (!this._cache.hasOwnProperty('tabbableElements')) {
this._cache.tabbableElements = getTabbableElements(this);
}
return this._cache.tabbableElements;
}
/**
* Return the client rects for this element and cache the result.
* @return {DOMRect[]}
*/
get clientRects() {
if (!this._cache.hasOwnProperty('clientRects')) {
this._cache.clientRects = Array.from(
this.actualNode.getClientRects()
).filter(rect => rect.width > 0);
}
return this._cache.clientRects;
}
/**
* Return the bounding rect for this element and cache the result.
* @return {DOMRect}
*/
get boundingClientRect() {
if (!this._cache.hasOwnProperty('boundingClientRect')) {
this._cache.boundingClientRect = this.actualNode.getBoundingClientRect();
}
return this._cache.boundingClientRect;
}
}
export default VirtualNode;