• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
32WebInspector.DOMNode = function(doc, payload) {
33    this.ownerDocument = doc;
34
35    this.id = payload.id;
36    this._nodeType = payload.nodeType;
37    this._nodeName = payload.nodeName;
38    this._localName = payload.localName;
39    this._nodeValue = payload.nodeValue;
40
41    this._attributes = [];
42    this._attributesMap = {};
43    if (payload.attributes)
44        this._setAttributesPayload(payload.attributes);
45
46    this._childNodeCount = payload.childNodeCount;
47    this.children = null;
48
49    this.nextSibling = null;
50    this.prevSibling = null;
51    this.firstChild = null;
52    this.lastChild = null;
53    this.parentNode = null;
54
55    if (payload.children)
56        this._setChildrenPayload(payload.children);
57
58    this._computedStyle = null;
59    this.style = null;
60    this._matchedCSSRules = [];
61
62    if (this._nodeType === Node.ELEMENT_NODE) {
63        // HTML and BODY from internal iframes should not overwrite top-level ones.
64        if (!this.ownerDocument.documentElement && this._nodeName === "HTML")
65            this.ownerDocument.documentElement = this;
66        if (!this.ownerDocument.body && this._nodeName === "BODY")
67            this.ownerDocument.body = this;
68        if (payload.documentURL)
69            this.documentURL = payload.documentURL;
70    } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) {
71        this.publicId = payload.publicId;
72        this.systemId = payload.systemId;
73        this.internalSubset = payload.internalSubset;
74    } else if (this._nodeType === Node.DOCUMENT_NODE) {
75        this.documentURL = payload.documentURL;
76    } else if (this._nodeType === Node.ATTRIBUTE_NODE) {
77        this.name = payload.name;
78        this.value = payload.value;
79    }
80}
81
82WebInspector.DOMNode.prototype = {
83    hasAttributes: function()
84    {
85        return this._attributes.length > 0;
86    },
87
88    hasChildNodes: function()
89    {
90        return this._childNodeCount > 0;
91    },
92
93    nodeType: function()
94    {
95        return this._nodeType;
96    },
97
98    nodeName: function()
99    {
100        return this._nodeName;
101    },
102
103    setNodeName: function(name, callback)
104    {
105        DOMAgent.setNodeName(this.id, name, callback);
106    },
107
108    localName: function()
109    {
110        return this._localName;
111    },
112
113    nodeValue: function()
114    {
115        return this._nodeValue;
116    },
117
118    setNodeValue: function(value, callback)
119    {
120        DOMAgent.setNodeValue(this.id, value, callback);
121    },
122
123    getAttribute: function(name)
124    {
125        var attr = this._attributesMap[name];
126        return attr ? attr.value : undefined;
127    },
128
129    setAttribute: function(name, value, callback)
130    {
131        function mycallback(error)
132        {
133            if (!error) {
134                var attr = this._attributesMap[name];
135                if (attr)
136                    attr.value = value;
137                else
138                    attr = this._addAttribute(name, value);
139            }
140
141            if (callback)
142                callback();
143        }
144        DOMAgent.setAttribute(this.id, name, value, mycallback.bind(this));
145    },
146
147    attributes: function()
148    {
149        return this._attributes;
150    },
151
152    removeAttribute: function(name, callback)
153    {
154        function mycallback(error, success)
155        {
156            if (!error) {
157                delete this._attributesMap[name];
158                for (var i = 0;  i < this._attributes.length; ++i) {
159                    if (this._attributes[i].name === name) {
160                        this._attributes.splice(i, 1);
161                        break;
162                    }
163                }
164            }
165
166            if (callback)
167                callback();
168        }
169        DOMAgent.removeAttribute(this.id, name, mycallback.bind(this));
170    },
171
172    getChildNodes: function(callback)
173    {
174        if (this.children) {
175            if (callback)
176                callback(this.children);
177            return;
178        }
179
180        function mycallback(error) {
181            if (!error && callback)
182                callback(this.children);
183        }
184        DOMAgent.getChildNodes(this.id, mycallback.bind(this));
185    },
186
187    getOuterHTML: function(callback)
188    {
189        DOMAgent.getOuterHTML(this.id, callback);
190    },
191
192    setOuterHTML: function(html, callback)
193    {
194        DOMAgent.setOuterHTML(this.id, html, callback);
195    },
196
197    removeNode: function(callback)
198    {
199        DOMAgent.removeNode(this.id, callback);
200    },
201
202    copyNode: function(callback)
203    {
204        DOMAgent.copyNode(this.id, callback);
205    },
206
207    eventListeners: function(callback)
208    {
209        DOMAgent.getEventListenersForNode(this.id, callback);
210    },
211
212    path: function()
213    {
214        var path = [];
215        var node = this;
216        while (node && "index" in node && node._nodeName.length) {
217            path.push([node.index, node._nodeName]);
218            node = node.parentNode;
219        }
220        path.reverse();
221        return path.join(",");
222    },
223
224    appropriateSelectorFor: function(justSelector)
225    {
226        var lowerCaseName = this.localName() || node.nodeName().toLowerCase();
227
228        var id = this.getAttribute("id");
229        if (id) {
230            var selector = "#" + id;
231            return (justSelector ? selector : lowerCaseName + selector);
232        }
233
234        var className = this.getAttribute("class");
235        if (className) {
236            var selector = "." + className.replace(/\s+/, ".");
237            return (justSelector ? selector : lowerCaseName + selector);
238        }
239
240        if (lowerCaseName === "input" && this.getAttribute("type"))
241            return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]";
242
243        return lowerCaseName;
244    },
245
246    _setAttributesPayload: function(attrs)
247    {
248        this._attributes = [];
249        this._attributesMap = {};
250        for (var i = 0; i < attrs.length; i += 2)
251            this._addAttribute(attrs[i], attrs[i + 1]);
252    },
253
254    _insertChild: function(prev, payload)
255    {
256        var node = new WebInspector.DOMNode(this.ownerDocument, payload);
257        if (!prev) {
258            if (!this.children) {
259                // First node
260                this.children = [ node ];
261            } else
262                this.children.unshift(node);
263        } else
264            this.children.splice(this.children.indexOf(prev) + 1, 0, node);
265        this._renumber();
266        return node;
267    },
268
269    removeChild_: function(node)
270    {
271        this.children.splice(this.children.indexOf(node), 1);
272        node.parentNode = null;
273        this._renumber();
274    },
275
276    _setChildrenPayload: function(payloads)
277    {
278        this.children = [];
279        for (var i = 0; i < payloads.length; ++i) {
280            var payload = payloads[i];
281            var node = new WebInspector.DOMNode(this.ownerDocument, payload);
282            this.children.push(node);
283        }
284        this._renumber();
285    },
286
287    _renumber: function()
288    {
289        this._childNodeCount = this.children.length;
290        if (this._childNodeCount == 0) {
291            this.firstChild = null;
292            this.lastChild = null;
293            return;
294        }
295        this.firstChild = this.children[0];
296        this.lastChild = this.children[this._childNodeCount - 1];
297        for (var i = 0; i < this._childNodeCount; ++i) {
298            var child = this.children[i];
299            child.index = i;
300            child.nextSibling = i + 1 < this._childNodeCount ? this.children[i + 1] : null;
301            child.prevSibling = i - 1 >= 0 ? this.children[i - 1] : null;
302            child.parentNode = this;
303        }
304    },
305
306    _addAttribute: function(name, value)
307    {
308        var attr = {
309            "name": name,
310            "value": value,
311            "_node": this
312        };
313        this._attributesMap[name] = attr;
314        this._attributes.push(attr);
315    },
316
317    ownerDocumentElement: function()
318    {
319        // document element is the child of the document / frame owner node that has documentURL property.
320        // FIXME: return document nodes as a part of the DOM tree structure.
321        var node = this;
322        while (node.parentNode && !node.parentNode.documentURL)
323            node = node.parentNode;
324        return node;
325    }
326}
327
328WebInspector.DOMDocument = function(domAgent, payload)
329{
330    WebInspector.DOMNode.call(this, this, payload);
331    this._listeners = {};
332    this._domAgent = domAgent;
333}
334
335WebInspector.DOMDocument.prototype.__proto__ = WebInspector.DOMNode.prototype;
336
337WebInspector.DOMAgent = function() {
338    this._idToDOMNode = null;
339    this._document = null;
340    InspectorBackend.registerDomainDispatcher("DOM", new WebInspector.DOMDispatcher(this));
341}
342
343WebInspector.DOMAgent.Events = {
344    AttrModified: "AttrModified",
345    CharacterDataModified: "CharacterDataModified",
346    NodeInserted: "NodeInserted",
347    NodeRemoved: "NodeRemoved",
348    DocumentUpdated: "DocumentUpdated",
349    ChildNodeCountUpdated: "ChildNodeCountUpdated"
350}
351
352WebInspector.DOMAgent.prototype = {
353    requestDocument: function(callback)
354    {
355        if (this._document) {
356            if (callback)
357                callback(this._document);
358            return;
359        }
360
361        if (this._pendingDocumentRequestCallbacks) {
362            this._pendingDocumentRequestCallbacks.push(callback);
363            return;
364        }
365
366        this._pendingDocumentRequestCallbacks = [callback];
367
368        function onDocumentAvailable(error, root)
369        {
370            if (!error)
371                this._setDocument(root);
372
373            for (var i = 0; i < this._pendingDocumentRequestCallbacks.length; ++i) {
374                var callback = this._pendingDocumentRequestCallbacks[i];
375                if (callback)
376                    callback(this._document);
377            }
378            delete this._pendingDocumentRequestCallbacks;
379        }
380
381        DOMAgent.getDocument(onDocumentAvailable.bind(this));
382    },
383
384    pushNodeToFrontend: function(objectId, callback)
385    {
386        this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeToFrontend.bind(DOMAgent), objectId, callback);
387    },
388
389    pushNodeByPathToFrontend: function(path, callback)
390    {
391        this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent), path, callback);
392    },
393
394    _wrapClientCallback: function(callback)
395    {
396        if (!callback)
397            return;
398        return function(error, result)
399        {
400            if (error)
401                console.error("Error during DOMAgent operation: " + error);
402            callback(error ? null : result);
403        }
404    },
405
406    _dispatchWhenDocumentAvailable: function(action)
407    {
408        var requestArguments = Array.prototype.slice.call(arguments, 1);
409        var callbackWrapper;
410
411        if (typeof requestArguments[requestArguments.length - 1] === "function") {
412            var callback = requestArguments.pop();
413            callbackWrapper = this._wrapClientCallback(callback);
414            requestArguments.push(callbackWrapper);
415        }
416        function onDocumentAvailable()
417        {
418            if (this._document)
419                action.apply(null, requestArguments);
420            else {
421                if (callbackWrapper)
422                    callbackWrapper("No document");
423            }
424        }
425        this.requestDocument(onDocumentAvailable.bind(this));
426    },
427
428    _attributesUpdated: function(nodeId, attrsArray)
429    {
430        var node = this._idToDOMNode[nodeId];
431        node._setAttributesPayload(attrsArray);
432        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, node);
433    },
434
435    _characterDataModified: function(nodeId, newValue)
436    {
437        var node = this._idToDOMNode[nodeId];
438        node._nodeValue = newValue;
439        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.CharacterDataModified, node);
440    },
441
442    nodeForId: function(nodeId)
443    {
444        return this._idToDOMNode[nodeId];
445    },
446
447    _documentUpdated: function()
448    {
449        this._setDocument(null);
450        this.requestDocument();
451    },
452
453    _setDocument: function(payload)
454    {
455        this._idToDOMNode = {};
456        if (payload && "id" in payload) {
457            this._document = new WebInspector.DOMDocument(this, payload);
458            this._idToDOMNode[payload.id] = this._document;
459            if (this._document.children)
460                this._bindNodes(this._document.children);
461        } else
462            this._document = null;
463        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.DocumentUpdated, this._document);
464    },
465
466    _setDetachedRoot: function(payload)
467    {
468        var root = new WebInspector.DOMNode(this._document, payload);
469        this._idToDOMNode[payload.id] = root;
470    },
471
472    _setChildNodes: function(parentId, payloads)
473    {
474        if (!parentId && payloads.length) {
475            this._setDetachedRoot(payloads[0]);
476            return;
477        }
478
479        var parent = this._idToDOMNode[parentId];
480        parent._setChildrenPayload(payloads);
481        this._bindNodes(parent.children);
482    },
483
484    _bindNodes: function(children)
485    {
486        for (var i = 0; i < children.length; ++i) {
487            var child = children[i];
488            this._idToDOMNode[child.id] = child;
489            if (child.children)
490                this._bindNodes(child.children);
491        }
492    },
493
494    _childNodeCountUpdated: function(nodeId, newValue)
495    {
496        var node = this._idToDOMNode[nodeId];
497        node._childNodeCount = newValue;
498        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, node);
499    },
500
501    _childNodeInserted: function(parentId, prevId, payload)
502    {
503        var parent = this._idToDOMNode[parentId];
504        var prev = this._idToDOMNode[prevId];
505        var node = parent._insertChild(prev, payload);
506        this._idToDOMNode[node.id] = node;
507        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeInserted, node);
508    },
509
510    _childNodeRemoved: function(parentId, nodeId)
511    {
512        var parent = this._idToDOMNode[parentId];
513        var node = this._idToDOMNode[nodeId];
514        parent.removeChild_(node);
515        this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeRemoved, {node:node, parent:parent});
516        delete this._idToDOMNode[nodeId];
517        if (Preferences.nativeInstrumentationEnabled)
518            WebInspector.panels.elements.sidebarPanes.domBreakpoints.nodeRemoved(node);
519    },
520
521    performSearch: function(query, searchResultCollector, searchSynchronously)
522    {
523        this._searchResultCollector = searchResultCollector;
524        DOMAgent.performSearch(query, !!searchSynchronously);
525    },
526
527    cancelSearch: function()
528    {
529        delete this._searchResultCollector;
530        DOMAgent.cancelSearch();
531    },
532
533    querySelector: function(nodeId, selectors, callback)
534    {
535        DOMAgent.querySelector(nodeId, selectors, this._wrapClientCallback(callback));
536    },
537
538    querySelectorAll: function(nodeId, selectors, callback)
539    {
540        DOMAgent.querySelectorAll(nodeId, selectors, this._wrapClientCallback(callback));
541    }
542}
543
544WebInspector.DOMAgent.prototype.__proto__ = WebInspector.Object.prototype;
545
546WebInspector.DOMDispatcher = function(domAgent)
547{
548    this._domAgent = domAgent;
549}
550
551WebInspector.DOMDispatcher.prototype = {
552    documentUpdated: function()
553    {
554        this._domAgent._documentUpdated();
555    },
556
557    attributesUpdated: function(nodeId, attrsArray)
558    {
559        this._domAgent._attributesUpdated(nodeId, attrsArray);
560    },
561
562    characterDataModified: function(nodeId, newValue)
563    {
564        this._domAgent._characterDataModified(nodeId, newValue);
565    },
566
567    setChildNodes: function(parentId, payloads)
568    {
569        this._domAgent._setChildNodes(parentId, payloads);
570    },
571
572    childNodeCountUpdated: function(nodeId, newValue)
573    {
574        this._domAgent._childNodeCountUpdated(nodeId, newValue);
575    },
576
577    childNodeInserted: function(parentId, prevId, payload)
578    {
579        this._domAgent._childNodeInserted(parentId, prevId, payload);
580    },
581
582    childNodeRemoved: function(parentId, nodeId)
583    {
584        this._domAgent._childNodeRemoved(parentId, nodeId);
585    },
586
587    inspectElementRequested: function(nodeId)
588    {
589        WebInspector.updateFocusedNode(nodeId);
590    },
591
592    searchResults: function(nodeIds)
593    {
594        if (this._domAgent._searchResultCollector)
595            this._domAgent._searchResultCollector(nodeIds);
596    }
597}
598