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