Web Inspector: Move a few remaining global WI settings to WI.settings
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / DOMNode.js
1 /*
2  * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
3  * Copyright (C) 2009 Joseph Pecoraro
4  * Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are
8  * met:
9  *
10  *     * Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  *     * Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following disclaimer
14  * in the documentation and/or other materials provided with the
15  * distribution.
16  *     * Neither the name of Google Inc. nor the names of its
17  * contributors may be used to endorse or promote products derived from
18  * this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32
33 WI.DOMNode = class DOMNode extends WI.Object
34 {
35     constructor(domManager, doc, isInShadowTree, payload)
36     {
37         super();
38
39         this._domManager = domManager;
40         this._isInShadowTree = isInShadowTree;
41
42         this.id = payload.nodeId;
43         this._domManager._idToDOMNode[this.id] = this;
44
45         this._nodeType = payload.nodeType;
46         this._nodeName = payload.nodeName;
47         this._localName = payload.localName;
48         this._nodeValue = payload.nodeValue;
49         this._pseudoType = payload.pseudoType;
50         this._shadowRootType = payload.shadowRootType;
51         this._computedRole = payload.role;
52         this._contentSecurityPolicyHash = payload.contentSecurityPolicyHash;
53
54         if (this._nodeType === Node.DOCUMENT_NODE)
55             this.ownerDocument = this;
56         else
57             this.ownerDocument = doc;
58
59         this._attributes = [];
60         this._attributesMap = new Map;
61         if (payload.attributes)
62             this._setAttributesPayload(payload.attributes);
63
64         this._childNodeCount = payload.childNodeCount;
65         this._children = null;
66         this._filteredChildren = null;
67         this._filteredChildrenNeedsUpdating = true;
68
69         this._nextSibling = null;
70         this._previousSibling = null;
71         this.parentNode = null;
72
73         this._enabledPseudoClasses = [];
74
75         // FIXME: The logic around this._shadowRoots and this._children is very confusing.
76         // We eventually include shadow roots at the start of _children. However we might
77         // not have our actual children yet. So we try to defer initializing _children until
78         // we have both shadowRoots and child nodes.
79         this._shadowRoots = [];
80         if (payload.shadowRoots) {
81             for (var i = 0; i < payload.shadowRoots.length; ++i) {
82                 var root = payload.shadowRoots[i];
83                 var node = new WI.DOMNode(this._domManager, this.ownerDocument, true, root);
84                 node.parentNode = this;
85                 this._shadowRoots.push(node);
86             }
87         }
88
89         if (payload.children)
90             this._setChildrenPayload(payload.children);
91         else if (this._shadowRoots.length && !this._childNodeCount)
92             this._children = this._shadowRoots.slice();
93
94         if (this._nodeType === Node.ELEMENT_NODE)
95             this._customElementState = payload.customElementState || WI.DOMNode.CustomElementState.Builtin;
96         else
97             this._customElementState = null;
98
99         if (payload.templateContent) {
100             this._templateContent = new WI.DOMNode(this._domManager, this.ownerDocument, false, payload.templateContent);
101             this._templateContent.parentNode = this;
102         }
103
104         this._pseudoElements = new Map;
105         if (payload.pseudoElements) {
106             for (var i = 0; i < payload.pseudoElements.length; ++i) {
107                 var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload.pseudoElements[i]);
108                 node.parentNode = this;
109                 this._pseudoElements.set(node.pseudoType(), node);
110             }
111         }
112
113         if (payload.contentDocument) {
114             this._contentDocument = new WI.DOMNode(this._domManager, null, false, payload.contentDocument);
115             this._children = [this._contentDocument];
116             this._renumber();
117         }
118
119         if (payload.frameId)
120             this._frameIdentifier = payload.frameId;
121
122         if (this._nodeType === Node.ELEMENT_NODE) {
123             // HTML and BODY from internal iframes should not overwrite top-level ones.
124             if (this.ownerDocument && !this.ownerDocument.documentElement && this._nodeName === "HTML")
125                 this.ownerDocument.documentElement = this;
126             if (this.ownerDocument && !this.ownerDocument.body && this._nodeName === "BODY")
127                 this.ownerDocument.body = this;
128             if (payload.documentURL)
129                 this.documentURL = payload.documentURL;
130         } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) {
131             this.publicId = payload.publicId;
132             this.systemId = payload.systemId;
133         } else if (this._nodeType === Node.DOCUMENT_NODE) {
134             this.documentURL = payload.documentURL;
135             this.xmlVersion = payload.xmlVersion;
136         } else if (this._nodeType === Node.ATTRIBUTE_NODE) {
137             this.name = payload.name;
138             this.value = payload.value;
139         }
140
141         this._domEvents = [];
142
143         if (this._shouldListenForEventListeners())
144             WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
145     }
146
147     // Static
148
149     static getFullscreenDOMEvents(domEvents)
150     {
151         return domEvents.reduce((accumulator, current) => {
152             if (current.eventName === "webkitfullscreenchange" && current.data && (!accumulator.length || accumulator.lastValue.data.enabled !== current.data.enabled))
153                 accumulator.push(current);
154             return accumulator;
155         }, []);
156     }
157
158     // Public
159
160     get domEvents() { return this._domEvents; }
161
162     get frameIdentifier()
163     {
164         return this._frameIdentifier || this.ownerDocument.frameIdentifier;
165     }
166
167     get frame()
168     {
169         if (!this._frame)
170             this._frame = WI.networkManager.frameForIdentifier(this.frameIdentifier);
171         return this._frame;
172     }
173
174     get children()
175     {
176         if (!this._children)
177             return null;
178
179         if (WI.settings.showShadowDOM.value)
180             return this._children;
181
182         if (this._filteredChildrenNeedsUpdating) {
183             this._filteredChildrenNeedsUpdating = false;
184             this._filteredChildren = this._children.filter(function(node) {
185                 return !node._isInShadowTree;
186             });
187         }
188
189         return this._filteredChildren;
190     }
191
192     get firstChild()
193     {
194         var children = this.children;
195
196         if (children && children.length > 0)
197             return children[0];
198
199         return null;
200     }
201
202     get lastChild()
203     {
204         var children = this.children;
205
206         if (children && children.length > 0)
207             return children.lastValue;
208
209         return null;
210     }
211
212     get nextSibling()
213     {
214         if (WI.settings.showShadowDOM.value)
215             return this._nextSibling;
216
217         var node = this._nextSibling;
218         while (node) {
219             if (!node._isInShadowTree)
220                 return node;
221             node = node._nextSibling;
222         }
223         return null;
224     }
225
226     get previousSibling()
227     {
228         if (WI.settings.showShadowDOM.value)
229             return this._previousSibling;
230
231         var node = this._previousSibling;
232         while (node) {
233             if (!node._isInShadowTree)
234                 return node;
235             node = node._previousSibling;
236         }
237         return null;
238     }
239
240     get childNodeCount()
241     {
242         var children = this.children;
243         if (children)
244             return children.length;
245
246         if (WI.settings.showShadowDOM.value)
247             return this._childNodeCount + this._shadowRoots.length;
248
249         return this._childNodeCount;
250     }
251
252     set childNodeCount(count)
253     {
254         this._childNodeCount = count;
255     }
256
257     computedRole()
258     {
259         return this._computedRole;
260     }
261
262     contentSecurityPolicyHash()
263     {
264         return this._contentSecurityPolicyHash;
265     }
266
267     hasAttributes()
268     {
269         return this._attributes.length > 0;
270     }
271
272     hasChildNodes()
273     {
274         return this.childNodeCount > 0;
275     }
276
277     hasShadowRoots()
278     {
279         return !!this._shadowRoots.length;
280     }
281
282     isInShadowTree()
283     {
284         return this._isInShadowTree;
285     }
286
287     isInUserAgentShadowTree()
288     {
289         return this._isInShadowTree && this.ancestorShadowRoot().isUserAgentShadowRoot();
290     }
291
292     isCustomElement()
293     {
294         return this._customElementState === WI.DOMNode.CustomElementState.Custom;
295     }
296
297     customElementState()
298     {
299         return this._customElementState;
300     }
301
302     isShadowRoot()
303     {
304         return !!this._shadowRootType;
305     }
306
307     isUserAgentShadowRoot()
308     {
309         return this._shadowRootType === WI.DOMNode.ShadowRootType.UserAgent;
310     }
311
312     ancestorShadowRoot()
313     {
314         if (!this._isInShadowTree)
315             return null;
316
317         let node = this;
318         while (node && !node.isShadowRoot())
319             node = node.parentNode;
320         return node;
321     }
322
323     ancestorShadowHost()
324     {
325         let shadowRoot = this.ancestorShadowRoot();
326         return shadowRoot ? shadowRoot.parentNode : null;
327     }
328
329     isPseudoElement()
330     {
331         return this._pseudoType !== undefined;
332     }
333
334     nodeType()
335     {
336         return this._nodeType;
337     }
338
339     nodeName()
340     {
341         return this._nodeName;
342     }
343
344     nodeNameInCorrectCase()
345     {
346         return this.isXMLNode() ? this.nodeName() : this.nodeName().toLowerCase();
347     }
348
349     setNodeName(name, callback)
350     {
351         DOMAgent.setNodeName(this.id, name, this._makeUndoableCallback(callback));
352     }
353
354     localName()
355     {
356         return this._localName;
357     }
358
359     templateContent()
360     {
361         return this._templateContent || null;
362     }
363
364     pseudoType()
365     {
366         return this._pseudoType;
367     }
368
369     hasPseudoElements()
370     {
371         return this._pseudoElements.size > 0;
372     }
373
374     pseudoElements()
375     {
376         return this._pseudoElements;
377     }
378
379     beforePseudoElement()
380     {
381         return this._pseudoElements.get(WI.DOMNode.PseudoElementType.Before) || null;
382     }
383
384     afterPseudoElement()
385     {
386         return this._pseudoElements.get(WI.DOMNode.PseudoElementType.After) || null;
387     }
388
389     shadowRoots()
390     {
391         return this._shadowRoots;
392     }
393
394     shadowRootType()
395     {
396         return this._shadowRootType;
397     }
398
399     nodeValue()
400     {
401         return this._nodeValue;
402     }
403
404     setNodeValue(value, callback)
405     {
406         DOMAgent.setNodeValue(this.id, value, this._makeUndoableCallback(callback));
407     }
408
409     getAttribute(name)
410     {
411         let attr = this._attributesMap.get(name);
412         return attr ? attr.value : undefined;
413     }
414
415     setAttribute(name, text, callback)
416     {
417         DOMAgent.setAttributesAsText(this.id, text, name, this._makeUndoableCallback(callback));
418     }
419
420     setAttributeValue(name, value, callback)
421     {
422         DOMAgent.setAttributeValue(this.id, name, value, this._makeUndoableCallback(callback));
423     }
424
425     attributes()
426     {
427         return this._attributes;
428     }
429
430     removeAttribute(name, callback)
431     {
432         function mycallback(error, success)
433         {
434             if (!error) {
435                 this._attributesMap.delete(name);
436                 for (var i = 0; i < this._attributes.length; ++i) {
437                     if (this._attributes[i].name === name) {
438                         this._attributes.splice(i, 1);
439                         break;
440                     }
441                 }
442             }
443
444             this._makeUndoableCallback(callback)(error);
445         }
446         DOMAgent.removeAttribute(this.id, name, mycallback.bind(this));
447     }
448
449     toggleClass(className, flag)
450     {
451         if (!className || !className.length)
452             return;
453
454         if (this.isPseudoElement()) {
455             this.parentNode.toggleClass(className, flag);
456             return;
457         }
458
459         if (this.nodeType() !== Node.ELEMENT_NODE)
460             return;
461
462         WI.RemoteObject.resolveNode(this).then((object) => {
463             function inspectedPage_node_toggleClass(className, flag) {
464                 this.classList.toggle(className, flag);
465             }
466
467             object.callFunction(inspectedPage_node_toggleClass, [className, flag]);
468             object.release();
469         });
470     }
471
472     scrollIntoView()
473     {
474         WI.RemoteObject.resolveNode(this).then((object) => {
475             function inspectedPage_node_scrollIntoView() {
476                 this.scrollIntoViewIfNeeded(true);
477             }
478
479             object.callFunction(inspectedPage_node_scrollIntoView);
480             object.release();
481         });
482     }
483
484     getChildNodes(callback)
485     {
486         if (this.children) {
487             if (callback)
488                 callback(this.children);
489             return;
490         }
491
492         function mycallback(error) {
493             if (!error && callback)
494                 callback(this.children);
495         }
496
497         DOMAgent.requestChildNodes(this.id, mycallback.bind(this));
498     }
499
500     getSubtree(depth, callback)
501     {
502         function mycallback(error)
503         {
504             if (callback)
505                 callback(error ? null : this.children);
506         }
507
508         DOMAgent.requestChildNodes(this.id, depth, mycallback.bind(this));
509     }
510
511     getOuterHTML(callback)
512     {
513         DOMAgent.getOuterHTML(this.id, callback);
514     }
515
516     setOuterHTML(html, callback)
517     {
518         DOMAgent.setOuterHTML(this.id, html, this._makeUndoableCallback(callback));
519     }
520
521     insertAdjacentHTML(position, html)
522     {
523         if (this.nodeType() !== Node.ELEMENT_NODE)
524             return;
525
526         // COMPATIBILITY (iOS 11.0): DOM.insertAdjacentHTML did not exist.
527         if (!DOMAgent.insertAdjacentHTML) {
528             WI.RemoteObject.resolveNode(this).then((object) => {
529                 function inspectedPage_node_insertAdjacentHTML(position, html) {
530                     this.insertAdjacentHTML(position, html);
531                 }
532
533                 object.callFunction(inspectedPage_node_insertAdjacentHTML, [position, html]);
534                 object.release();
535             });
536             return;
537         }
538
539         DOMAgent.insertAdjacentHTML(this.id, position, html, this._makeUndoableCallback());
540     }
541
542     removeNode(callback)
543     {
544         DOMAgent.removeNode(this.id, this._makeUndoableCallback(callback));
545     }
546
547     copyNode()
548     {
549         function copy(error, text)
550         {
551             if (!error)
552                 InspectorFrontendHost.copyText(text);
553         }
554         DOMAgent.getOuterHTML(this.id, copy);
555     }
556
557     getEventListeners(callback)
558     {
559         DOMAgent.getEventListenersForNode(this.id, callback);
560     }
561
562     accessibilityProperties(callback)
563     {
564         function accessibilityPropertiesCallback(error, accessibilityProperties)
565         {
566             if (!error && callback && accessibilityProperties) {
567                 callback({
568                     activeDescendantNodeId: accessibilityProperties.activeDescendantNodeId,
569                     busy: accessibilityProperties.busy,
570                     checked: accessibilityProperties.checked,
571                     childNodeIds: accessibilityProperties.childNodeIds,
572                     controlledNodeIds: accessibilityProperties.controlledNodeIds,
573                     current: accessibilityProperties.current,
574                     disabled: accessibilityProperties.disabled,
575                     exists: accessibilityProperties.exists,
576                     expanded: accessibilityProperties.expanded,
577                     flowedNodeIds: accessibilityProperties.flowedNodeIds,
578                     focused: accessibilityProperties.focused,
579                     ignored: accessibilityProperties.ignored,
580                     ignoredByDefault: accessibilityProperties.ignoredByDefault,
581                     invalid: accessibilityProperties.invalid,
582                     isPopupButton: accessibilityProperties.isPopUpButton,
583                     headingLevel: accessibilityProperties.headingLevel,
584                     hierarchyLevel: accessibilityProperties.hierarchyLevel,
585                     hidden: accessibilityProperties.hidden,
586                     label: accessibilityProperties.label,
587                     liveRegionAtomic: accessibilityProperties.liveRegionAtomic,
588                     liveRegionRelevant: accessibilityProperties.liveRegionRelevant,
589                     liveRegionStatus: accessibilityProperties.liveRegionStatus,
590                     mouseEventNodeId: accessibilityProperties.mouseEventNodeId,
591                     nodeId: accessibilityProperties.nodeId,
592                     ownedNodeIds: accessibilityProperties.ownedNodeIds,
593                     parentNodeId: accessibilityProperties.parentNodeId,
594                     pressed: accessibilityProperties.pressed,
595                     readonly: accessibilityProperties.readonly,
596                     required: accessibilityProperties.required,
597                     role: accessibilityProperties.role,
598                     selected: accessibilityProperties.selected,
599                     selectedChildNodeIds: accessibilityProperties.selectedChildNodeIds
600                 });
601             }
602         }
603         DOMAgent.getAccessibilityPropertiesForNode(this.id, accessibilityPropertiesCallback.bind(this));
604     }
605
606     path()
607     {
608         var path = [];
609         var node = this;
610         while (node && "index" in node && node._nodeName.length) {
611             path.push([node.index, node._nodeName]);
612             node = node.parentNode;
613         }
614         path.reverse();
615         return path.join(",");
616     }
617
618     get escapedIdSelector()
619     {
620         let id = this.getAttribute("id");
621         if (!id)
622             return "";
623
624         id = id.trim();
625         if (!id.length)
626             return "";
627
628         id = CSS.escape(id);
629         if (/[\s'"]/.test(id))
630             return `[id="${id}"]`;
631
632         return `#${id}`;
633     }
634
635     get escapedClassSelector()
636     {
637         let classes = this.getAttribute("class");
638         if (!classes)
639             return "";
640
641         classes = classes.trim();
642         if (!classes.length)
643             return "";
644
645         let foundClasses = new Set;
646         return classes.split(/\s+/).reduce((selector, className) => {
647             if (!className.length || foundClasses.has(className))
648                 return selector;
649
650             foundClasses.add(className);
651             return `${selector}.${CSS.escape(className)}`;
652         }, "");
653     }
654
655     get displayName()
656     {
657         if (this.isPseudoElement())
658             return "::" + this._pseudoType;
659         return this.nodeNameInCorrectCase() + this.escapedIdSelector + this.escapedClassSelector;
660     }
661
662     appropriateSelectorFor(justSelector)
663     {
664         if (this.isPseudoElement())
665             return this.parentNode.appropriateSelectorFor() + "::" + this._pseudoType;
666
667         let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
668
669         let id = this.escapedIdSelector;
670         if (id.length)
671             return justSelector ? id : lowerCaseName + id;
672
673         let classes = this.escapedClassSelector;
674         if (classes.length)
675             return justSelector ? classes : lowerCaseName + classes;
676
677         if (lowerCaseName === "input" && this.getAttribute("type"))
678             return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]";
679
680         return lowerCaseName;
681     }
682
683     isAncestor(node)
684     {
685         if (!node)
686             return false;
687
688         var currentNode = node.parentNode;
689         while (currentNode) {
690             if (this === currentNode)
691                 return true;
692             currentNode = currentNode.parentNode;
693         }
694         return false;
695     }
696
697     isDescendant(descendant)
698     {
699         return descendant !== null && descendant.isAncestor(this);
700     }
701
702     get ownerSVGElement()
703     {
704         if (this._nodeName === "svg")
705             return this;
706
707         if (!this.parentNode)
708             return null;
709
710         return this.parentNode.ownerSVGElement;
711     }
712
713     isSVGElement()
714     {
715         return !!this.ownerSVGElement;
716     }
717
718     didFireEvent(eventName, timestamp, data)
719     {
720         // Called from WI.DOMManager.
721
722         this._addDOMEvent({
723             eventName,
724             timestamp: WI.timelineManager.computeElapsedTime(timestamp),
725             data,
726         });
727     }
728
729     _handleDOMNodeDidFireEvent(event)
730     {
731         if (event.target === this || !event.target.isAncestor(this))
732             return;
733
734         let domEvent = Object.shallowCopy(event.data.domEvent);
735         domEvent.originator = event.target;
736
737         this._addDOMEvent(domEvent);
738     }
739
740     _addDOMEvent(domEvent)
741     {
742         this._domEvents.push(domEvent);
743
744         this.dispatchEventToListeners(WI.DOMNode.Event.DidFireEvent, {domEvent});
745     }
746
747     _shouldListenForEventListeners()
748     {
749         let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
750         return lowerCaseName === "video" || lowerCaseName === "audio";
751     }
752
753     _setAttributesPayload(attrs)
754     {
755         this._attributes = [];
756         this._attributesMap = new Map;
757         for (var i = 0; i < attrs.length; i += 2)
758             this._addAttribute(attrs[i], attrs[i + 1]);
759     }
760
761     _insertChild(prev, payload)
762     {
763         var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload);
764         if (!prev) {
765             if (!this._children) {
766                 // First node
767                 this._children = this._shadowRoots.concat([node]);
768             } else
769                 this._children.unshift(node);
770         } else
771             this._children.splice(this._children.indexOf(prev) + 1, 0, node);
772         this._renumber();
773         return node;
774     }
775
776     _removeChild(node)
777     {
778         // FIXME: Handle removal if this is a shadow root.
779         if (node.isPseudoElement()) {
780             this._pseudoElements.delete(node.pseudoType());
781             node.parentNode = null;
782         } else {
783             this._children.splice(this._children.indexOf(node), 1);
784             node.parentNode = null;
785             this._renumber();
786         }
787     }
788
789     _setChildrenPayload(payloads)
790     {
791         // We set children in the constructor.
792         if (this._contentDocument)
793             return;
794
795         this._children = this._shadowRoots.slice();
796         for (var i = 0; i < payloads.length; ++i) {
797             var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payloads[i]);
798             this._children.push(node);
799         }
800         this._renumber();
801     }
802
803     _renumber()
804     {
805         this._filteredChildrenNeedsUpdating = true;
806
807         var childNodeCount = this._children.length;
808         if (childNodeCount === 0)
809             return;
810
811         for (var i = 0; i < childNodeCount; ++i) {
812             var child = this._children[i];
813             child.index = i;
814             child._nextSibling = i + 1 < childNodeCount ? this._children[i + 1] : null;
815             child._previousSibling = i - 1 >= 0 ? this._children[i - 1] : null;
816             child.parentNode = this;
817         }
818     }
819
820     _addAttribute(name, value)
821     {
822         let attr = {name, value, _node: this};
823         this._attributesMap.set(name, attr);
824         this._attributes.push(attr);
825     }
826
827     _setAttribute(name, value)
828     {
829         let attr = this._attributesMap.get(name);
830         if (attr)
831             attr.value = value;
832         else
833             this._addAttribute(name, value);
834     }
835
836     _removeAttribute(name)
837     {
838         let attr = this._attributesMap.get(name);
839         if (attr) {
840             this._attributes.remove(attr);
841             this._attributesMap.delete(name);
842         }
843     }
844
845     moveTo(targetNode, anchorNode, callback)
846     {
847         DOMAgent.moveTo(this.id, targetNode.id, anchorNode ? anchorNode.id : undefined, this._makeUndoableCallback(callback));
848     }
849
850     isXMLNode()
851     {
852         return !!this.ownerDocument && !!this.ownerDocument.xmlVersion;
853     }
854
855     get enabledPseudoClasses()
856     {
857         return this._enabledPseudoClasses;
858     }
859
860     setPseudoClassEnabled(pseudoClass, enabled)
861     {
862         var pseudoClasses = this._enabledPseudoClasses;
863         if (enabled) {
864             if (pseudoClasses.includes(pseudoClass))
865                 return;
866             pseudoClasses.push(pseudoClass);
867         } else {
868             if (!pseudoClasses.includes(pseudoClass))
869                 return;
870             pseudoClasses.remove(pseudoClass);
871         }
872
873         function changed(error)
874         {
875             if (!error)
876                 this.dispatchEventToListeners(WI.DOMNode.Event.EnabledPseudoClassesChanged);
877         }
878
879         CSSAgent.forcePseudoState(this.id, pseudoClasses, changed.bind(this));
880     }
881
882     _makeUndoableCallback(callback)
883     {
884         return function(error)
885         {
886             if (!error)
887                 DOMAgent.markUndoableState();
888
889             if (callback)
890                 callback.apply(null, arguments);
891         };
892     }
893 };
894
895 WI.DOMNode.Event = {
896     EnabledPseudoClassesChanged: "dom-node-enabled-pseudo-classes-did-change",
897     AttributeModified: "dom-node-attribute-modified",
898     AttributeRemoved: "dom-node-attribute-removed",
899     EventListenersChanged: "dom-node-event-listeners-changed",
900     DidFireEvent: "dom-node-did-fire-event",
901 };
902
903 WI.DOMNode.PseudoElementType = {
904     Before: "before",
905     After: "after",
906 };
907
908 WI.DOMNode.ShadowRootType = {
909     UserAgent: "user-agent",
910     Closed: "closed",
911     Open: "open",
912 };
913
914 WI.DOMNode.CustomElementState = {
915     Builtin: "builtin",
916     Custom: "custom",
917     Waiting: "waiting",
918     Failed: "failed",
919 };