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