ea6917b787907b724b764128710b949de03ac89b
[WebKit-https.git] / Source / WebCore / inspector / front-end / DOMAgent.js
1 /*
2  * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
3  * Copyright (C) 2009 Joseph Pecoraro
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 /**
33  * @constructor
34  * @param {WebInspector.DOMAgent} domAgent
35  * @param {?WebInspector.DOMDocument} doc
36  * @param {boolean} isInShadowTree
37  * @param {DOMAgent.Node} payload
38  */
39 WebInspector.DOMNode = function(domAgent, doc, isInShadowTree, payload) {
40     this._domAgent = domAgent;
41     this.ownerDocument = doc;
42     this._isInShadowTree = isInShadowTree;
43
44     this.id = payload.nodeId;
45     domAgent._idToDOMNode[this.id] = this;
46     this._nodeType = payload.nodeType;
47     this._nodeName = payload.nodeName;
48     this._localName = payload.localName;
49     this._nodeValue = payload.nodeValue;
50
51     this._shadowRoots = [];
52
53     this._attributes = [];
54     this._attributesMap = {};
55     if (payload.attributes)
56         this._setAttributesPayload(payload.attributes);
57
58     this._userProperties = {};
59     this._descendantUserPropertyCounters = {};
60
61     this._childNodeCount = payload.childNodeCount;
62     this.children = null;
63
64     this.nextSibling = null;
65     this.previousSibling = null;
66     this.firstChild = null;
67     this.lastChild = null;
68     this.parentNode = null;
69
70     if (payload.shadowRoots && WebInspector.settings.showShadowDOM.get()) {
71         for (var i = 0; i < payload.shadowRoots.length; ++i) {
72             var root = payload.shadowRoots[i];
73             var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, true, root);
74             this._shadowRoots.push(node);
75         }
76     }
77
78     if (payload.children)
79         this._setChildrenPayload(payload.children);
80
81     if (payload.contentDocument) {
82         this._contentDocument = new WebInspector.DOMDocument(domAgent, payload.contentDocument);
83         this.children = [this._contentDocument];
84         this._renumber();
85     }
86
87     if (this._nodeType === Node.ELEMENT_NODE) {
88         // HTML and BODY from internal iframes should not overwrite top-level ones.
89         if (this.ownerDocument && !this.ownerDocument.documentElement && this._nodeName === "HTML")
90             this.ownerDocument.documentElement = this;
91         if (this.ownerDocument && !this.ownerDocument.body && this._nodeName === "BODY")
92             this.ownerDocument.body = this;
93     } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) {
94         this.publicId = payload.publicId;
95         this.systemId = payload.systemId;
96         this.internalSubset = payload.internalSubset;
97     } else if (this._nodeType === Node.ATTRIBUTE_NODE) {
98         this.name = payload.name;
99         this.value = payload.value;
100     }
101 }
102
103 /**
104  * @constructor
105  * @param {string} value
106  * @param {boolean} optimized
107  */
108 WebInspector.DOMNode.XPathStep = function(value, optimized)
109 {
110     this.value = value;
111     this.optimized = optimized;
112 }
113
114 WebInspector.DOMNode.XPathStep.prototype = {
115     toString: function()
116     {
117         return this.value;
118     }
119 }
120
121 WebInspector.DOMNode.prototype = {
122     /**
123      * @return {boolean}
124      */
125     hasAttributes: function()
126     {
127         return this._attributes.length > 0;
128     },
129
130     /**
131      * @return {boolean}
132      */
133     hasChildNodes: function()
134     {
135         return this._childNodeCount > 0 || !!this._shadowRoots.length;
136     },
137
138     /**
139      * @return {boolean}
140      */
141     hasShadowRoots: function()
142     {
143         return !!this._shadowRoots.length;
144     },
145
146     /**
147      * @return {number}
148      */
149     nodeType: function()
150     {
151         return this._nodeType;
152     },
153
154     /**
155      * @return {string}
156      */
157     nodeName: function()
158     {
159         return this._nodeName;
160     },
161
162     /**
163      * @return {boolean}
164      */
165     isInShadowTree: function()
166     {
167         return this._isInShadowTree;
168     },
169
170     /**
171      * @return {string}
172      */
173     nodeNameInCorrectCase: function()
174     {
175         return this.isXMLNode() ? this.nodeName() : this.nodeName().toLowerCase();
176     },
177
178     /**
179      * @param {string} name
180      * @param {function(?Protocol.Error)=} callback
181      */
182     setNodeName: function(name, callback)
183     {
184         DOMAgent.setNodeName(this.id, name, WebInspector.domAgent._markRevision(this, callback));
185     },
186
187     /**
188      * @return {string}
189      */
190     localName: function()
191     {
192         return this._localName;
193     },
194
195     /**
196      * @return {string}
197      */
198     nodeValue: function()
199     {
200         return this._nodeValue;
201     },
202
203     /**
204      * @param {string} value
205      * @param {function(?Protocol.Error)=} callback
206      */
207     setNodeValue: function(value, callback)
208     {
209         DOMAgent.setNodeValue(this.id, value, WebInspector.domAgent._markRevision(this, callback));
210     },
211
212     /**
213      * @param {string} name
214      * @return {string}
215      */
216     getAttribute: function(name)
217     {
218         var attr = this._attributesMap[name];
219         return attr ? attr.value : undefined;
220     },
221
222     /**
223      * @param {string} name
224      * @param {string} text
225      * @param {function(?Protocol.Error)=} callback
226      */
227     setAttribute: function(name, text, callback)
228     {
229         DOMAgent.setAttributesAsText(this.id, text, name, WebInspector.domAgent._markRevision(this, callback));
230     },
231
232     /**
233      * @param {string} name
234      * @param {string} value
235      * @param {function(?Protocol.Error)=} callback
236      */
237     setAttributeValue: function(name, value, callback)
238     {
239         DOMAgent.setAttributeValue(this.id, name, value, WebInspector.domAgent._markRevision(this, callback));
240     },
241
242     /**
243      * @return {Object}
244      */
245     attributes: function()
246     {
247         return this._attributes;
248     },
249
250     /**
251      * @param {string} name
252      * @param {function(?Protocol.Error)=} callback
253      */
254     removeAttribute: function(name, callback)
255     {
256         /**
257          *  @param {?Protocol.Error} error
258          */
259         function mycallback(error)
260         {
261             if (!error) {
262                 delete this._attributesMap[name];
263                 for (var i = 0;  i < this._attributes.length; ++i) {
264                     if (this._attributes[i].name === name) {
265                         this._attributes.splice(i, 1);
266                         break;
267                     }
268                 }
269             }
270
271             WebInspector.domAgent._markRevision(this, callback)(error);
272         }
273         DOMAgent.removeAttribute(this.id, name, mycallback.bind(this));
274     },
275
276     /**
277      * @param {function(Array.<WebInspector.DOMNode>)=} callback
278      */
279     getChildNodes: function(callback)
280     {
281         if (this.children) {
282             if (callback)
283                 callback(this.children);
284             return;
285         }
286
287         /**
288          * @this {WebInspector.DOMNode}
289          * @param {?Protocol.Error} error
290          */
291         function mycallback(error)
292         {
293             if (!error && callback)
294                 callback(this.children);
295         }
296
297         DOMAgent.requestChildNodes(this.id, mycallback.bind(this));
298     },
299
300     /**
301      * @param {function(?Protocol.Error)=} callback
302      */
303     getOuterHTML: function(callback)
304     {
305         DOMAgent.getOuterHTML(this.id, callback);
306     },
307
308     /**
309      * @param {string} html
310      * @param {function(?Protocol.Error)=} callback
311      */
312     setOuterHTML: function(html, callback)
313     {
314         DOMAgent.setOuterHTML(this.id, html, WebInspector.domAgent._markRevision(this, callback));
315     },
316
317     /**
318      * @param {function(?Protocol.Error)=} callback
319      */
320     removeNode: function(callback)
321     {
322         DOMAgent.removeNode(this.id, WebInspector.domAgent._markRevision(this, callback));
323     },
324
325     copyNode: function()
326     {
327         function copy(error, text)
328         {
329             if (!error)
330                 InspectorFrontendHost.copyText(text);
331         }
332         DOMAgent.getOuterHTML(this.id, copy);
333     },
334
335     /**
336      * @param {boolean} optimized
337      */
338     copyXPath: function(optimized)
339     {
340         InspectorFrontendHost.copyText(this.xPath(optimized));
341     },
342
343     /**
344      * @param {function(?Protocol.Error)=} callback
345      */
346     eventListeners: function(callback)
347     {
348         DOMAgent.getEventListenersForNode(this.id, callback);
349     },
350
351     /**
352      * @return {string}
353      */
354     path: function()
355     {
356         var path = [];
357         var node = this;
358         while (node && "index" in node && node._nodeName.length) {
359             path.push([node.index, node._nodeName]);
360             node = node.parentNode;
361         }
362         path.reverse();
363         return path.join(",");
364     },
365
366     /**
367      * @param {boolean} justSelector
368      * @return {string}
369      */
370     appropriateSelectorFor: function(justSelector)
371     {
372         var lowerCaseName = this.localName() || this.nodeName().toLowerCase();
373
374         var id = this.getAttribute("id");
375         if (id) {
376             var selector = "#" + id;
377             return (justSelector ? selector : lowerCaseName + selector);
378         }
379
380         var className = this.getAttribute("class");
381         if (className) {
382             var selector = "." + className.trim().replace(/\s+/g, ".");
383             return (justSelector ? selector : lowerCaseName + selector);
384         }
385
386         if (lowerCaseName === "input" && this.getAttribute("type"))
387             return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]";
388
389         return lowerCaseName;
390     },
391
392     /**
393      * @param {WebInspector.DOMNode} node
394      * @return {boolean}
395      */
396     isAncestor: function(node)
397     {
398         if (!node)
399             return false;
400
401         var currentNode = node.parentNode;
402         while (currentNode) {
403             if (this === currentNode)
404                 return true;
405             currentNode = currentNode.parentNode;
406         }
407         return false;
408     },
409
410     /**
411      * @param {WebInspector.DOMNode} descendant
412      * @return {boolean}
413      */
414     isDescendant: function(descendant)
415     {
416         return descendant !== null && descendant.isAncestor(this);
417     },
418
419     /**
420      * @param {Array.<string>} attrs
421      * @return {boolean}
422      */
423     _setAttributesPayload: function(attrs)
424     {
425         var attributesChanged = !this._attributes || attrs.length !== this._attributes.length * 2;
426         var oldAttributesMap = this._attributesMap || {};
427
428         this._attributes = [];
429         this._attributesMap = {};
430
431         for (var i = 0; i < attrs.length; i += 2) {
432             var name = attrs[i];
433             var value = attrs[i + 1];
434             this._addAttribute(name, value);
435
436             if (attributesChanged)
437                 continue;
438
439             if (!oldAttributesMap[name] || oldAttributesMap[name].value !== value)
440               attributesChanged = true;
441         }
442         return attributesChanged;
443     },
444
445     /**
446      * @param {WebInspector.DOMNode} prev
447      * @param {DOMAgent.Node} payload
448      * @return {WebInspector.DOMNode}
449      */
450     _insertChild: function(prev, payload)
451     {
452         var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, this._isInShadowTree, payload);
453         if (!prev) {
454             if (!this.children) {
455                 // First node
456                 this.children = this._shadowRoots.concat([ node ]);
457             } else
458                 this.children.unshift(node);
459         } else
460             this.children.splice(this.children.indexOf(prev) + 1, 0, node);
461         this._renumber();
462         return node;
463     },
464
465     /**
466      * @param {WebInspector.DOMNode} node
467      */
468     _removeChild: function(node)
469     {
470         this.children.splice(this.children.indexOf(node), 1);
471         node.parentNode = null;
472         node._updateChildUserPropertyCountsOnRemoval(this);
473         this._renumber();
474     },
475
476     /**
477      * @param {Array.<DOMAgent.Node>} payloads
478      */
479     _setChildrenPayload: function(payloads)
480     {
481         // We set children in the constructor.
482         if (this._contentDocument)
483             return;
484
485         this.children = this._shadowRoots.slice();
486         for (var i = 0; i < payloads.length; ++i) {
487             var payload = payloads[i];
488             var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, this._isInShadowTree, payload);
489             this.children.push(node);
490         }
491         this._renumber();
492     },
493
494     _renumber: function()
495     {
496         this._childNodeCount = this.children.length;
497         if (this._childNodeCount == 0) {
498             this.firstChild = null;
499             this.lastChild = null;
500             return;
501         }
502         this.firstChild = this.children[0];
503         this.lastChild = this.children[this._childNodeCount - 1];
504         for (var i = 0; i < this._childNodeCount; ++i) {
505             var child = this.children[i];
506             child.index = i;
507             child.nextSibling = i + 1 < this._childNodeCount ? this.children[i + 1] : null;
508             child.previousSibling = i - 1 >= 0 ? this.children[i - 1] : null;
509             child.parentNode = this;
510         }
511     },
512
513     /**
514      * @param {string} name
515      * @param {string} value
516      */
517     _addAttribute: function(name, value)
518     {
519         var attr = {
520             name: name,
521             value: value,
522             _node: this
523         };
524         this._attributesMap[name] = attr;
525         this._attributes.push(attr);
526     },
527
528     /**
529      * @param {string} name
530      * @param {string} value
531      */
532     _setAttribute: function(name, value)
533     {
534         var attr = this._attributesMap[name];
535         if (attr)
536             attr.value = value;
537         else
538             this._addAttribute(name, value);
539     },
540
541     /**
542      * @param {string} name
543      */
544     _removeAttribute: function(name)
545     {
546         var attr = this._attributesMap[name];
547         if (attr) {
548             this._attributes.remove(attr);
549             delete this._attributesMap[name];
550         }
551     },
552
553     /**
554      * @param {WebInspector.DOMNode} targetNode
555      * @param {?WebInspector.DOMNode} anchorNode
556      * @param {function(?Protocol.Error)=} callback
557      */
558     moveTo: function(targetNode, anchorNode, callback)
559     {
560         DOMAgent.moveTo(this.id, targetNode.id, anchorNode ? anchorNode.id : undefined, WebInspector.domAgent._markRevision(this, callback));
561     },
562
563     /**
564      * @return {boolean}
565      */
566     isXMLNode: function()
567     {
568         return !!this.ownerDocument && !!this.ownerDocument.xmlVersion;
569     },
570
571     /**
572      * @param {boolean} optimized
573      * @return {string}
574      */
575     xPath: function(optimized)
576     {
577         if (this._nodeType === Node.DOCUMENT_NODE)
578             return "/";
579
580         var steps = [];
581         var contextNode = this;
582         while (contextNode) {
583             var step = contextNode._xPathValue(optimized);
584             if (!step)
585                 break; // Error - bail out early.
586             steps.push(step);
587             if (step.optimized)
588                 break;
589             contextNode = contextNode.parentNode;
590         }
591
592         steps.reverse();
593         return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/");
594     },
595
596     /**
597      * @param {boolean} optimized
598      * @return {WebInspector.DOMNode.XPathStep}
599      */
600     _xPathValue: function(optimized)
601     {
602         var ownValue;
603         var ownIndex = this._xPathIndex();
604         if (ownIndex === -1)
605             return null; // Error.
606
607         switch (this._nodeType) {
608         case Node.ELEMENT_NODE:
609             if (optimized && this.getAttribute("id"))
610                 return new WebInspector.DOMNode.XPathStep("//*[@id=\"" + this.getAttribute("id") + "\"]", true);
611             ownValue = this._localName;
612             break;
613         case Node.ATTRIBUTE_NODE:
614             ownValue = "@" + this._nodeName;
615             break;
616         case Node.TEXT_NODE:
617         case Node.CDATA_SECTION_NODE:
618             ownValue = "text()";
619             break;
620         case Node.PROCESSING_INSTRUCTION_NODE:
621             ownValue = "processing-instruction()";
622             break;
623         case Node.COMMENT_NODE:
624             ownValue = "comment()";
625             break;
626         case Node.DOCUMENT_NODE:
627             ownValue = "";
628             break;
629         default:
630             ownValue = "";
631             break;
632         }
633
634         if (ownIndex > 0)
635             ownValue += "[" + ownIndex + "]";
636
637         return new WebInspector.DOMNode.XPathStep(ownValue, this._nodeType === Node.DOCUMENT_NODE);
638     },
639
640     /**
641      * @return {number}
642      */
643     _xPathIndex: function()
644     {
645         // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise.
646         function areNodesSimilar(left, right)
647         {
648             if (left === right)
649                 return true;
650
651             if (left._nodeType === Node.ELEMENT_NODE && right._nodeType === Node.ELEMENT_NODE)
652                 return left._localName === right._localName;
653
654             if (left._nodeType === right._nodeType)
655                 return true;
656
657             // XPath treats CDATA as text nodes.
658             var leftType = left._nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left._nodeType;
659             var rightType = right._nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right._nodeType;
660             return leftType === rightType;
661         }
662
663         var siblings = this.parentNode ? this.parentNode.children : null;
664         if (!siblings)
665             return 0; // Root node - no siblings.
666         var hasSameNamedElements;
667         for (var i = 0; i < siblings.length; ++i) {
668             if (areNodesSimilar(this, siblings[i]) && siblings[i] !== this) {
669                 hasSameNamedElements = true;
670                 break;
671             }
672         }
673         if (!hasSameNamedElements)
674             return 0;
675         var ownIndex = 1; // XPath indices start with 1.
676         for (var i = 0; i < siblings.length; ++i) {
677             if (areNodesSimilar(this, siblings[i])) {
678                 if (siblings[i] === this)
679                     return ownIndex;
680                 ++ownIndex;
681             }
682         }
683         return -1; // An error occurred: |this| not found in parent's children.
684     },
685
686     _updateChildUserPropertyCountsOnRemoval: function(parentNode)
687     {
688         var result = {};
689         if (this._userProperties) {
690             for (var name in this._userProperties)
691                 result[name] = (result[name] || 0) + 1;
692         }
693
694         if (this._descendantUserPropertyCounters) {
695             for (var name in this._descendantUserPropertyCounters) {
696                 var counter = this._descendantUserPropertyCounters[name];
697                 result[name] = (result[name] || 0) + counter;
698             }
699         }
700
701         for (var name in result)
702             parentNode._updateDescendantUserPropertyCount(name, -result[name]);
703     },
704
705     _updateDescendantUserPropertyCount: function(name, delta)
706     {
707         if (!this._descendantUserPropertyCounters.hasOwnProperty(name))
708             this._descendantUserPropertyCounters[name] = 0;
709         this._descendantUserPropertyCounters[name] += delta;
710         if (!this._descendantUserPropertyCounters[name])
711             delete this._descendantUserPropertyCounters[name];
712         if (this.parentNode)
713             this.parentNode._updateDescendantUserPropertyCount(name, delta);
714     },
715
716     setUserProperty: function(name, value)
717     {
718         if (value === null) {
719             this.removeUserProperty(name);
720             return;
721         }
722
723         if (this.parentNode && !this._userProperties.hasOwnProperty(name))
724             this.parentNode._updateDescendantUserPropertyCount(name, 1);
725
726         this._userProperties[name] = value;
727     },
728
729     removeUserProperty: function(name)
730     {
731         if (!this._userProperties.hasOwnProperty(name))
732             return;
733
734         delete this._userProperties[name];
735         if (this.parentNode)
736             this.parentNode._updateDescendantUserPropertyCount(name, -1);
737     },
738
739     getUserProperty: function(name)
740     {
741         return this._userProperties ? this._userProperties[name] : null;
742     },
743
744     descendantUserPropertyCount: function(name)
745     {
746         return this._descendantUserPropertyCounters && this._descendantUserPropertyCounters[name] ? this._descendantUserPropertyCounters[name] : 0;
747     },
748
749     /**
750      * @param {string} url
751      * @return {?string}
752      */
753     resolveURL: function(url)
754     {
755         if (!url)
756             return url;
757         for (var frameOwnerCandidate = this; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
758             if (frameOwnerCandidate.baseURL)
759                 return WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, url);
760         }
761         return null;
762     }
763 }
764
765 /**
766  * @extends {WebInspector.DOMNode}
767  * @constructor
768  * @param {WebInspector.DOMAgent} domAgent
769  * @param {DOMAgent.Node} payload
770  */
771 WebInspector.DOMDocument = function(domAgent, payload)
772 {
773     WebInspector.DOMNode.call(this, domAgent, this, false, payload);
774     this.documentURL = payload.documentURL || "";
775     this.baseURL = /** @type {string} */ (payload.baseURL);
776     console.assert(this.baseURL);
777     this.xmlVersion = payload.xmlVersion;
778     this._listeners = {};
779 }
780
781 WebInspector.DOMDocument.prototype = {
782     __proto__: WebInspector.DOMNode.prototype
783 }
784
785 /**
786  * @extends {WebInspector.Object}
787  * @constructor
788  */
789 WebInspector.DOMAgent = function() {
790     /** @type {Object|undefined} */
791     this._idToDOMNode = {};
792     this._document = null;
793     this._attributeLoadNodeIds = {};
794     InspectorBackend.registerDOMDispatcher(new WebInspector.DOMDispatcher(this));
795     if (WebInspector.settings.emulateTouchEvents.get())
796         this._emulateTouchEventsChanged();
797     WebInspector.settings.emulateTouchEvents.addChangeListener(this._emulateTouchEventsChanged, this);
798 }
799
800 WebInspector.DOMAgent.Events = {
801     AttrModified: "AttrModified",
802     AttrRemoved: "AttrRemoved",
803     CharacterDataModified: "CharacterDataModified",
804     NodeInserted: "NodeInserted",
805     NodeRemoved: "NodeRemoved",
806     DocumentUpdated: "DocumentUpdated",
807     ChildNodeCountUpdated: "ChildNodeCountUpdated",
808     InspectElementRequested: "InspectElementRequested",
809     UndoRedoRequested: "UndoRedoRequested",
810     UndoRedoCompleted: "UndoRedoCompleted"
811 }
812
813 WebInspector.DOMAgent.prototype = {
814     /**
815      * @param {function(WebInspector.DOMDocument)=} callback
816      */
817     requestDocument: function(callback)
818     {
819         if (this._document) {
820             if (callback)
821                 callback(this._document);
822             return;
823         }
824
825         if (this._pendingDocumentRequestCallbacks) {
826             this._pendingDocumentRequestCallbacks.push(callback);
827             return;
828         }
829
830         this._pendingDocumentRequestCallbacks = [callback];
831
832         /**
833          * @this {WebInspector.DOMAgent}
834          * @param {?Protocol.Error} error
835          * @param {DOMAgent.Node} root
836          */
837         function onDocumentAvailable(error, root)
838         {
839             if (!error)
840                 this._setDocument(root);
841
842             for (var i = 0; i < this._pendingDocumentRequestCallbacks.length; ++i) {
843                 var callback = this._pendingDocumentRequestCallbacks[i];
844                 if (callback)
845                     callback(this._document);
846             }
847             delete this._pendingDocumentRequestCallbacks;
848         }
849
850         DOMAgent.getDocument(onDocumentAvailable.bind(this));
851     },
852
853     /**
854      * @return {WebInspector.DOMDocument?}
855      */
856     existingDocument: function()
857     {
858         return this._document;
859     },
860
861     /**
862      * @param {RuntimeAgent.RemoteObjectId} objectId
863      * @param {function(?DOMAgent.NodeId)=} callback
864      */
865     pushNodeToFrontend: function(objectId, callback)
866     {
867         var callbackCast = /** @type {function(*)} */ callback;
868         this._dispatchWhenDocumentAvailable(DOMAgent.requestNode.bind(DOMAgent, objectId), callbackCast);
869     },
870
871     /**
872      * @param {string} path
873      * @param {function(?WebInspector.DOMNode)=} callback
874      */
875     pushNodeByPathToFrontend: function(path, callback)
876     {
877         var callbackCast = /** @type {function(*)} */ callback;
878         this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent, path), callbackCast);
879     },
880
881     /**
882      * @param {function(*)=} callback
883      * @return {function(?Protocol.Error,*=)|undefined}
884      */
885     _wrapClientCallback: function(callback)
886     {
887         if (!callback)
888             return;
889         /**
890          * @param {?Protocol.Error} error
891          * @param {*=} result
892          */
893         return function(error, result)
894         {
895             // Caller is responsible for handling the actual error.
896             callback(error ? null : result);
897         }
898     },
899
900     /**
901      * @param {function(function(?Protocol.Error, *=))} func
902      * @param {function(*)=} callback
903      */
904     _dispatchWhenDocumentAvailable: function(func, callback)
905     {
906         var callbackWrapper = /** @type {function(?Protocol.Error, *=)} */ this._wrapClientCallback(callback);
907
908         function onDocumentAvailable()
909         {
910             if (this._document)
911                 func(callbackWrapper);
912             else {
913                 if (callbackWrapper)
914                     callbackWrapper("No document");
915             }
916         }
917         this.requestDocument(onDocumentAvailable.bind(this));
918     },
919
920     /**
921      * @param {DOMAgent.NodeId} nodeId
922      * @param {string} name
923      * @param {string} value
924      */
925     _attributeModified: function(nodeId, name, value)
926     {
927         var node = this._idToDOMNode[nodeId];
928         if (!node)
929             return;
930
931         node._setAttribute(name, value);
932         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, { node: node, name: name });
933     },
934
935     /**
936      * @param {DOMAgent.NodeId} nodeId
937      * @param {string} name
938      */
939     _attributeRemoved: function(nodeId, name)
940     {
941         var node = this._idToDOMNode[nodeId];
942         if (!node)
943             return;
944         node._removeAttribute(name);
945         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrRemoved, { node: node, name: name });
946     },
947
948     /**
949      * @param {Array.<DOMAgent.NodeId>} nodeIds
950      */
951     _inlineStyleInvalidated: function(nodeIds)
952     {
953         for (var i = 0; i < nodeIds.length; ++i)
954             this._attributeLoadNodeIds[nodeIds[i]] = true;
955         if ("_loadNodeAttributesTimeout" in this)
956             return;
957         this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0);
958     },
959
960     _loadNodeAttributes: function()
961     {
962         /**
963          * @this {WebInspector.DOMAgent}
964          * @param {DOMAgent.NodeId} nodeId
965          * @param {?Protocol.Error} error
966          * @param {Array.<string>} attributes
967          */
968         function callback(nodeId, error, attributes)
969         {
970             if (error) {
971                 // We are calling _loadNodeAttributes asynchronously, it is ok if node is not found.
972                 return;
973             }
974             var node = this._idToDOMNode[nodeId];
975             if (node) {
976                 if (node._setAttributesPayload(attributes))
977                     this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, { node: node, name: "style" });
978             }
979         }
980
981         delete this._loadNodeAttributesTimeout;
982
983         for (var nodeId in this._attributeLoadNodeIds) {
984             var nodeIdAsNumber = parseInt(nodeId, 10);
985             DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber));
986         }
987         this._attributeLoadNodeIds = {};
988     },
989
990     /**
991      * @param {DOMAgent.NodeId} nodeId
992      * @param {string} newValue
993      */
994     _characterDataModified: function(nodeId, newValue)
995     {
996         var node = this._idToDOMNode[nodeId];
997         node._nodeValue = newValue;
998         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.CharacterDataModified, node);
999     },
1000
1001     /**
1002      * @param {DOMAgent.NodeId} nodeId
1003      * @return {WebInspector.DOMNode|undefined}
1004      */
1005     nodeForId: function(nodeId)
1006     {
1007         return this._idToDOMNode[nodeId];
1008     },
1009
1010     _documentUpdated: function()
1011     {
1012         this._setDocument(null);
1013     },
1014
1015     /**
1016      * @param {DOMAgent.Node} payload
1017      */
1018     _setDocument: function(payload)
1019     {
1020         this._idToDOMNode = {};
1021         if (payload && "nodeId" in payload)
1022             this._document = new WebInspector.DOMDocument(this, payload);
1023         else
1024             this._document = null;
1025         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.DocumentUpdated, this._document);
1026     },
1027
1028     /**
1029      * @param {DOMAgent.Node} payload
1030      */
1031     _setDetachedRoot: function(payload)
1032     {
1033         if (payload.nodeName === "#document")
1034             new WebInspector.DOMDocument(this, payload);
1035         else
1036             new WebInspector.DOMNode(this, null, false, payload);
1037     },
1038
1039     /**
1040      * @param {DOMAgent.NodeId} parentId
1041      * @param {Array.<DOMAgent.Node>} payloads
1042      */
1043     _setChildNodes: function(parentId, payloads)
1044     {
1045         if (!parentId && payloads.length) {
1046             this._setDetachedRoot(payloads[0]);
1047             return;
1048         }
1049
1050         var parent = this._idToDOMNode[parentId];
1051         parent._setChildrenPayload(payloads);
1052     },
1053
1054     /**
1055      * @param {DOMAgent.NodeId} nodeId
1056      * @param {number} newValue
1057      */
1058     _childNodeCountUpdated: function(nodeId, newValue)
1059     {
1060         var node = this._idToDOMNode[nodeId];
1061         node._childNodeCount = newValue;
1062         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, node);
1063     },
1064
1065     /**
1066      * @param {DOMAgent.NodeId} parentId
1067      * @param {DOMAgent.NodeId} prevId
1068      * @param {DOMAgent.Node} payload
1069      */
1070     _childNodeInserted: function(parentId, prevId, payload)
1071     {
1072         var parent = this._idToDOMNode[parentId];
1073         var prev = this._idToDOMNode[prevId];
1074         var node = parent._insertChild(prev, payload);
1075         this._idToDOMNode[node.id] = node;
1076         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeInserted, node);
1077     },
1078
1079     /**
1080      * @param {DOMAgent.NodeId} parentId
1081      * @param {DOMAgent.NodeId} nodeId
1082      */
1083     _childNodeRemoved: function(parentId, nodeId)
1084     {
1085         var parent = this._idToDOMNode[parentId];
1086         var node = this._idToDOMNode[nodeId];
1087         parent._removeChild(node);
1088         this._unbind(node);
1089         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeRemoved, {node: node, parent: parent});
1090     },
1091
1092     /**
1093      * @param {DOMAgent.NodeId} rootId
1094      */
1095     _shadowRootPopped: function(rootId)
1096     {
1097     },
1098
1099     /**
1100      * @param {WebInspector.DOMNode} node
1101      */
1102     _unbind: function(node)
1103     {
1104         delete this._idToDOMNode[node.id];
1105         for (var i = 0; node.children && i < node.children.length; ++i)
1106             this._unbind(node.children[i]);
1107     },
1108
1109     /**
1110      * @param {number} nodeId
1111      */
1112     inspectElement: function(nodeId)
1113     {
1114         var node = this._idToDOMNode[nodeId];
1115         if (node)
1116             this.dispatchEventToListeners(WebInspector.DOMAgent.Events.InspectElementRequested, node);
1117     },
1118
1119     /**
1120      * @param {string} query
1121      * @param {function(number)} searchCallback
1122      */
1123     performSearch: function(query, searchCallback)
1124     {
1125         this.cancelSearch();
1126
1127         /**
1128          * @param {?Protocol.Error} error
1129          * @param {string} searchId
1130          * @param {number} resultsCount
1131          */
1132         function callback(error, searchId, resultsCount)
1133         {
1134             this._searchId = searchId;
1135             searchCallback(resultsCount);
1136         }
1137         DOMAgent.performSearch(query, callback.bind(this));
1138     },
1139
1140     /**
1141      * @param {number} index
1142      * @param {?function(DOMAgent.Node)} callback
1143      */
1144     searchResult: function(index, callback)
1145     {
1146         if (this._searchId) {
1147             /**
1148              * @param {?Protocol.Error} error
1149              * @param {Array.<number>} nodeIds
1150              */
1151             function mycallback(error, nodeIds)
1152             {
1153                 if (error) {
1154                     console.error(error);
1155                     callback(null);
1156                     return;
1157                 }
1158                 if (nodeIds.length != 1)
1159                     return;
1160
1161                 callback(this._idToDOMNode[nodeIds[0]]);
1162             }
1163             DOMAgent.getSearchResults(this._searchId, index, index + 1, mycallback.bind(this));
1164         } else
1165             callback(null);
1166     },
1167
1168     cancelSearch: function()
1169     {
1170         if (this._searchId) {
1171             DOMAgent.discardSearchResults(this._searchId);
1172             delete this._searchId;
1173         }
1174     },
1175
1176     /**
1177      * @param {DOMAgent.NodeId} nodeId
1178      * @param {string} selectors
1179      * @param {function(?DOMAgent.NodeId)=} callback
1180      */
1181     querySelector: function(nodeId, selectors, callback)
1182     {
1183         var callbackCast = /** @type {function(*)|undefined} */callback;
1184         DOMAgent.querySelector(nodeId, selectors, this._wrapClientCallback(callbackCast));
1185     },
1186
1187     /**
1188      * @param {DOMAgent.NodeId} nodeId
1189      * @param {string} selectors
1190      * @param {function(?Array.<DOMAgent.NodeId>)=} callback
1191      */
1192     querySelectorAll: function(nodeId, selectors, callback)
1193     {
1194         var callbackCast = /** @type {function(*)|undefined} */callback;
1195         DOMAgent.querySelectorAll(nodeId, selectors, this._wrapClientCallback(callbackCast));
1196     },
1197
1198     /**
1199      * @param {?number} nodeId
1200      * @param {string=} mode
1201      */
1202     highlightDOMNode: function(nodeId, mode)
1203     {
1204         if (this._hideDOMNodeHighlightTimeout) {
1205             clearTimeout(this._hideDOMNodeHighlightTimeout);
1206             delete this._hideDOMNodeHighlightTimeout;
1207         }
1208
1209         this._highlightedDOMNodeId = nodeId;
1210         if (nodeId)
1211             DOMAgent.highlightNode(nodeId, this._buildHighlightConfig(mode));
1212         else
1213             DOMAgent.hideHighlight();
1214     },
1215
1216     hideDOMNodeHighlight: function()
1217     {
1218         this.highlightDOMNode(0);
1219     },
1220
1221     /**
1222      * @param {?DOMAgent.NodeId} nodeId
1223      */
1224     highlightDOMNodeForTwoSeconds: function(nodeId)
1225     {
1226         this.highlightDOMNode(nodeId);
1227         this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000);
1228     },
1229
1230     /**
1231      * @param {boolean} enabled
1232      * @param {function(?Protocol.Error)=} callback
1233      */
1234     setInspectModeEnabled: function(enabled, callback)
1235     {
1236         DOMAgent.setInspectModeEnabled(enabled, this._buildHighlightConfig(), callback);
1237     },
1238
1239     /**
1240      * @param {string=} mode
1241      */
1242     _buildHighlightConfig: function(mode)
1243     {
1244         mode = mode || "all";
1245         var highlightConfig = { showInfo: mode === "all", showRulers: WebInspector.settings.showMetricsRulers.get() };
1246         if (mode === "all" || mode === "content")
1247             highlightConfig.contentColor = WebInspector.Color.PageHighlight.Content.toProtocolRGBA();
1248
1249         if (mode === "all" || mode === "padding")
1250             highlightConfig.paddingColor = WebInspector.Color.PageHighlight.Padding.toProtocolRGBA();
1251
1252         if (mode === "all" || mode === "border")
1253             highlightConfig.borderColor = WebInspector.Color.PageHighlight.Border.toProtocolRGBA();
1254
1255         if (mode === "all" || mode === "margin")
1256             highlightConfig.marginColor = WebInspector.Color.PageHighlight.Margin.toProtocolRGBA();
1257
1258         return highlightConfig;
1259     },
1260
1261     /**
1262      * @param {WebInspector.DOMNode} node
1263      * @param {function(?Protocol.Error)=} callback
1264      * @return {function(?Protocol.Error)}
1265      */
1266     _markRevision: function(node, callback)
1267     {
1268         function wrapperFunction(error)
1269         {
1270             if (!error)
1271                 this.markUndoableState();
1272
1273             if (callback)
1274                 callback.apply(this, arguments);
1275         }
1276         return wrapperFunction.bind(this);
1277     },
1278
1279     _emulateTouchEventsChanged: function()
1280     {
1281         const injectedFunction = function() {
1282             const touchEvents = ["ontouchstart", "ontouchend", "ontouchmove", "ontouchcancel"];
1283             for (var i = 0; i < touchEvents.length; ++i) {
1284                 if (!(touchEvents[i] in window.__proto__))
1285                     Object.defineProperty(window.__proto__, touchEvents[i], { value: null, writable: true, configurable: true, enumerable: true });
1286                 if (!(touchEvents[i] in document.__proto__))
1287                     Object.defineProperty(document.__proto__, touchEvents[i], { value: null, writable: true, configurable: true, enumerable: true });
1288             }
1289         }
1290
1291         var emulationEnabled = WebInspector.settings.emulateTouchEvents.get();
1292         if (emulationEnabled && !this._addTouchEventsScriptInjecting) {
1293             this._addTouchEventsScriptInjecting = true;
1294             PageAgent.addScriptToEvaluateOnLoad("(" + injectedFunction.toString() + ")", scriptAddedCallback.bind(this));
1295         } else {
1296             if (typeof this._addTouchEventsScriptId !== "undefined") {
1297                 PageAgent.removeScriptToEvaluateOnLoad(this._addTouchEventsScriptId);
1298                 delete this._addTouchEventsScriptId;
1299             }
1300         }
1301
1302         function scriptAddedCallback(error, scriptId)
1303         {
1304             delete this._addTouchEventsScriptInjecting;
1305             if (error)
1306                 return;
1307             this._addTouchEventsScriptId = scriptId;
1308         }
1309
1310         PageAgent.setTouchEmulationEnabled(emulationEnabled);
1311     },
1312
1313     markUndoableState: function()
1314     {
1315         DOMAgent.markUndoableState();
1316     },
1317
1318     /**
1319      * @param {function(?Protocol.Error)=} callback
1320      */
1321     undo: function(callback)
1322     {
1323         function mycallback(error)
1324         {
1325             this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoCompleted);
1326             callback(error);
1327         }
1328
1329         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoRequested);
1330         DOMAgent.undo(callback);
1331     },
1332
1333     /**
1334      * @param {function(?Protocol.Error)=} callback
1335      */
1336     redo: function(callback)
1337     {
1338         function mycallback(error)
1339         {
1340             this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoCompleted);
1341             callback(error);
1342         }
1343
1344         this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoRequested);
1345         DOMAgent.redo(callback);
1346     },
1347
1348     __proto__: WebInspector.Object.prototype
1349 }
1350
1351 /**
1352  * @constructor
1353  * @implements {DOMAgent.Dispatcher}
1354  * @param {WebInspector.DOMAgent} domAgent
1355  */
1356 WebInspector.DOMDispatcher = function(domAgent)
1357 {
1358     this._domAgent = domAgent;
1359 }
1360
1361 WebInspector.DOMDispatcher.prototype = {
1362     documentUpdated: function()
1363     {
1364         this._domAgent._documentUpdated();
1365     },
1366
1367     /**
1368      * @param {DOMAgent.NodeId} nodeId
1369      * @param {string} name
1370      * @param {string} value
1371      */
1372     attributeModified: function(nodeId, name, value)
1373     {
1374         this._domAgent._attributeModified(nodeId, name, value);
1375     },
1376
1377     /**
1378      * @param {DOMAgent.NodeId} nodeId
1379      * @param {string} name
1380      */
1381     attributeRemoved: function(nodeId, name)
1382     {
1383         this._domAgent._attributeRemoved(nodeId, name);
1384     },
1385
1386     /**
1387      * @param {Array.<DOMAgent.NodeId>} nodeIds
1388      */
1389     inlineStyleInvalidated: function(nodeIds)
1390     {
1391         this._domAgent._inlineStyleInvalidated(nodeIds);
1392     },
1393
1394     /**
1395      * @param {DOMAgent.NodeId} nodeId
1396      * @param {string} characterData
1397      */
1398     characterDataModified: function(nodeId, characterData)
1399     {
1400         this._domAgent._characterDataModified(nodeId, characterData);
1401     },
1402
1403     /**
1404      * @param {DOMAgent.NodeId} parentId
1405      * @param {Array.<DOMAgent.Node>} payloads
1406      */
1407     setChildNodes: function(parentId, payloads)
1408     {
1409         this._domAgent._setChildNodes(parentId, payloads);
1410     },
1411
1412     /**
1413      * @param {DOMAgent.NodeId} nodeId
1414      * @param {number} childNodeCount
1415      */
1416     childNodeCountUpdated: function(nodeId, childNodeCount)
1417     {
1418         this._domAgent._childNodeCountUpdated(nodeId, childNodeCount);
1419     },
1420
1421     /**
1422      * @param {DOMAgent.NodeId} parentNodeId
1423      * @param {DOMAgent.NodeId} previousNodeId
1424      * @param {DOMAgent.Node} payload
1425      */
1426     childNodeInserted: function(parentNodeId, previousNodeId, payload)
1427     {
1428         this._domAgent._childNodeInserted(parentNodeId, previousNodeId, payload);
1429     },
1430
1431     /**
1432      * @param {DOMAgent.NodeId} parentNodeId
1433      * @param {DOMAgent.NodeId} nodeId
1434      */
1435     childNodeRemoved: function(parentNodeId, nodeId)
1436     {
1437         this._domAgent._childNodeRemoved(parentNodeId, nodeId);
1438     },
1439
1440     /**
1441      * @param {DOMAgent.NodeId} hostId
1442      * @param {DOMAgent.Node} root
1443      */
1444     shadowRootPushed: function(hostId, root)
1445     {
1446         this._domAgent._childNodeInserted(hostId, 0, root);
1447     },
1448
1449     /**
1450      * @param {DOMAgent.NodeId} hostId
1451      * @param {DOMAgent.NodeId} rootId
1452      */
1453     shadowRootPopped: function(hostId, rootId)
1454     {
1455         this._domAgent._childNodeRemoved(hostId, rootId);
1456     }
1457 }
1458
1459 /**
1460  * @type {?WebInspector.DOMAgent}
1461  */
1462 WebInspector.domAgent = null;