2010-02-10 Alexander Pavlov <apavlov@chromium.org>
[WebKit-https.git] / WebCore / inspector / front-end / DOMAgent.js
1 /*
2  * Copyright (C) 2009 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.id;
36     // injectedScriptId is a node is for DOM nodes which should be converted
37     // to corresponding InjectedScript by the inspector backend. We indicate
38     // this by making injectedScriptId negative.
39     this.injectedScriptId = -payload.id;
40     this.nodeType = payload.nodeType;
41     this.nodeName = payload.nodeName;
42     this.localName = payload.localName;
43     this._nodeValue = payload.nodeValue;
44     this.textContent = this.nodeValue;
45
46     this.attributes = [];
47     this._attributesMap = {};
48     if (payload.attributes)
49         this._setAttributesPayload(payload.attributes);
50
51     this._childNodeCount = payload.childNodeCount;
52     this.children = null;
53
54     this.nextSibling = null;
55     this.prevSibling = null;
56     this.firstChild = null;
57     this.lastChild = null;
58     this.parentNode = null;
59
60     if (payload.children)
61         this._setChildrenPayload(payload.children);
62
63     this._computedStyle = null;
64     this.style = null;
65     this._matchedCSSRules = [];
66
67     if (this.nodeType === Node.ELEMENT_NODE) {
68         // HTML and BODY from internal iframes should not overwrite top-level ones.
69         if (!this.ownerDocument.documentElement && this.nodeName === "HTML")
70             this.ownerDocument.documentElement = this;
71         if (!this.ownerDocument.body && this.nodeName === "BODY")
72             this.ownerDocument.body = this;
73         if (payload.documentURL)
74             this.documentURL = payload.documentURL;
75     } else if (this.nodeType === Node.DOCUMENT_TYPE_NODE) {
76         this.publicId = payload.publicId;
77         this.systemId = payload.systemId;
78         this.internalSubset = payload.internalSubset;
79     } else if (this.nodeType === Node.DOCUMENT_NODE)
80         this.documentURL = payload.documentURL;
81 }
82
83 WebInspector.DOMNode.prototype = {
84     hasAttributes: function()
85     {
86         return this.attributes.length > 0;
87     },
88
89     hasChildNodes: function()  {
90         return this._childNodeCount > 0;
91     },
92
93     get nodeValue() {
94         return this._nodeValue;
95     },
96
97     set nodeValue(value) {
98         if (this.nodeType != Node.TEXT_NODE)
99             return;
100         var self = this;
101         var callback = function()
102         {
103             self._nodeValue = value;
104             self.textContent = value;
105         };
106         this.ownerDocument._domAgent.setTextNodeValueAsync(this, value, callback);
107     },
108
109     getAttribute: function(name)
110     {
111         var attr = this._attributesMap[name];
112         return attr ? attr.value : undefined;
113     },
114
115     setAttribute: function(name, value)
116     {
117         var self = this;
118         var callback = function()
119         {
120             var attr = self._attributesMap[name];
121             if (attr)
122                 attr.value = value;
123             else
124                 attr = self._addAttribute(name, value);
125         };
126         this.ownerDocument._domAgent.setAttributeAsync(this, name, value, callback);
127     },
128
129     removeAttribute: function(name)
130     {
131         var self = this;
132         var callback = function()
133         {
134             delete self._attributesMap[name];
135             for (var i = 0;  i < self.attributes.length; ++i) {
136                 if (self.attributes[i].name == name) {
137                     self.attributes.splice(i, 1);
138                     break;
139                 }
140             }
141         };
142         this.ownerDocument._domAgent.removeAttributeAsync(this, name, callback);
143     },
144
145     _setAttributesPayload: function(attrs)
146     {
147         this.attributes = [];
148         this._attributesMap = {};
149         for (var i = 0; i < attrs.length; i += 2)
150             this._addAttribute(attrs[i], attrs[i + 1]);
151     },
152
153     _insertChild: function(prev, payload)
154     {
155         var node = new WebInspector.DOMNode(this.ownerDocument, payload);
156         if (!prev) {
157             if (!this.children) {
158                 // First node
159                 this.children = [ node ];
160             } else
161                 this.children.unshift(node);
162         } else
163             this.children.splice(this.children.indexOf(prev) + 1, 0, node);
164         this._renumber();
165         return node;
166     },
167
168     removeChild_: function(node)
169     {
170         this.children.splice(this.children.indexOf(node), 1);
171         node.parentNode = null;
172         this._renumber();
173     },
174
175     _setChildrenPayload: function(payloads)
176     {
177         this.children = [];
178         for (var i = 0; i < payloads.length; ++i) {
179             var payload = payloads[i];
180             var node = new WebInspector.DOMNode(this.ownerDocument, payload);
181             this.children.push(node);
182         }
183         this._renumber();
184     },
185
186     _renumber: function()
187     {
188         this._childNodeCount = this.children.length;
189         if (this._childNodeCount == 0) {
190             this.firstChild = null;
191             this.lastChild = null;
192             return;
193         }
194         this.firstChild = this.children[0];
195         this.lastChild = this.children[this._childNodeCount - 1];
196         for (var i = 0; i < this._childNodeCount; ++i) {
197             var child = this.children[i];
198             child.index = i;
199             child.nextSibling = i + 1 < this._childNodeCount ? this.children[i + 1] : null;
200             child.prevSibling = i - 1 >= 0 ? this.children[i - 1] : null;
201             child.parentNode = this;
202         }
203     },
204
205     _addAttribute: function(name, value)
206     {
207         var attr = {
208             "name": name,
209             "value": value,
210             "_node": this
211         };
212         this._attributesMap[name] = attr;
213         this.attributes.push(attr);
214     },
215
216     _setStyles: function(computedStyle, inlineStyle, styleAttributes, matchedCSSRules)
217     {
218         this._computedStyle = new WebInspector.CSSStyleDeclaration(computedStyle);
219         this.style = new WebInspector.CSSStyleDeclaration(inlineStyle);
220
221         for (var name in styleAttributes) {
222             if (this._attributesMap[name])
223                 this._attributesMap[name].style = new WebInspector.CSSStyleDeclaration(styleAttributes[name]);
224         }
225
226         this._matchedCSSRules = [];
227         for (var i = 0; i < matchedCSSRules.length; i++)
228             this._matchedCSSRules.push(WebInspector.CSSStyleDeclaration.parseRule(matchedCSSRules[i]));
229     },
230
231     _clearStyles: function()
232     {
233         this.computedStyle = null;
234         this.style = null;
235         for (var name in this._attributesMap)
236             this._attributesMap[name].style = null;
237         this._matchedCSSRules = null;
238     }
239 }
240
241 WebInspector.DOMDocument = function(domAgent, defaultView, payload)
242 {
243     WebInspector.DOMNode.call(this, this, payload);
244     this._listeners = {};
245     this._domAgent = domAgent;
246     this.defaultView = defaultView;
247 }
248
249 WebInspector.DOMDocument.prototype = {
250
251     addEventListener: function(name, callback)
252     {
253         var listeners = this._listeners[name];
254         if (!listeners) {
255             listeners = [];
256             this._listeners[name] = listeners;
257         }
258         listeners.push(callback);
259     },
260
261     removeEventListener: function(name, callback)
262     {
263         var listeners = this._listeners[name];
264         if (!listeners)
265             return;
266
267         var index = listeners.indexOf(callback);
268         if (index != -1)
269             listeners.splice(index, 1);
270     },
271
272     _fireDomEvent: function(name, event)
273     {
274         var listeners = this._listeners[name];
275         if (!listeners)
276             return;
277
278         for (var i = 0; i < listeners.length; ++i) {
279             var listener = listeners[i];
280             listener.call(this, event);
281         }
282     }
283 }
284
285 WebInspector.DOMDocument.prototype.__proto__ = WebInspector.DOMNode.prototype;
286
287
288 WebInspector.DOMWindow = function(domAgent)
289 {
290     this._domAgent = domAgent;
291 }
292
293 WebInspector.DOMWindow.prototype = {
294     get document()
295     {
296         return this._domAgent.document;
297     },
298
299     get Node()
300     {
301         return WebInspector.DOMNode;
302     },
303
304     get Element()
305     {
306         return WebInspector.DOMNode;
307     },
308
309     Object: function()
310     {
311     },
312
313     getComputedStyle: function(node)
314     {
315         return node._computedStyle;
316     },
317
318     getMatchedCSSRules: function(node, pseudoElement, authorOnly)
319     {
320         return node._matchedCSSRules;
321     }
322 }
323
324 WebInspector.DOMAgent = function() {
325     this._window = new WebInspector.DOMWindow(this);
326     this._idToDOMNode = null;
327     this.document = null;
328 }
329
330 WebInspector.DOMAgent.prototype = {
331     get domWindow()
332     {
333         return this._window;
334     },
335
336     getChildNodesAsync: function(parent, callback)
337     {
338         var children = parent.children;
339         if (children) {
340             callback(children);
341             return;
342         }
343         function mycallback() {
344             callback(parent.children);
345         }
346         var callId = WebInspector.Callback.wrap(mycallback);
347         InspectorBackend.getChildNodes(callId, parent.id);
348     },
349
350     setAttributeAsync: function(node, name, value, callback)
351     {
352         var mycallback = this._didApplyDomChange.bind(this, node, callback);
353         InspectorBackend.setAttribute(WebInspector.Callback.wrap(mycallback), node.id, name, value);
354     },
355
356     removeAttributeAsync: function(node, name, callback)
357     {
358         var mycallback = this._didApplyDomChange.bind(this, node, callback);
359         InspectorBackend.removeAttribute(WebInspector.Callback.wrap(mycallback), node.id, name);
360     },
361
362     setTextNodeValueAsync: function(node, text, callback)
363     {
364         var mycallback = this._didApplyDomChange.bind(this, node, callback);
365         InspectorBackend.setTextNodeValue(WebInspector.Callback.wrap(mycallback), node.id, text);
366     },
367
368     _didApplyDomChange: function(node, callback, success)
369     {
370         if (!success)
371             return;
372         callback();
373         // TODO(pfeldman): Fix this hack.
374         var elem = WebInspector.panels.elements.treeOutline.findTreeElement(node);
375         if (elem)
376             elem.updateTitle();
377     },
378
379     _attributesUpdated: function(nodeId, attrsArray)
380     {
381         var node = this._idToDOMNode[nodeId];
382         node._setAttributesPayload(attrsArray);
383         var event = {target: node};
384         this.document._fireDomEvent("DOMAttrModified", event);
385     },
386
387     nodeForId: function(nodeId) {
388         return this._idToDOMNode[nodeId];
389     },
390
391     _setDocument: function(payload)
392     {
393         this._idToDOMNode = {};
394         if (payload && "id" in payload) {
395             this.document = new WebInspector.DOMDocument(this, this._window, payload);
396             this._idToDOMNode[payload.id] = this.document;
397             this._bindNodes(this.document.children);
398         } else
399             this.document = null;
400         WebInspector.panels.elements.setDocument(this.document);
401     },
402
403     _setDetachedRoot: function(payload)
404     {
405         var root = new WebInspector.DOMNode(this.document, payload);
406         this._idToDOMNode[payload.id] = root;
407     },
408
409     _setChildNodes: function(parentId, payloads)
410     {
411         var parent = this._idToDOMNode[parentId];
412         parent._setChildrenPayload(payloads);
413         this._bindNodes(parent.children);
414     },
415
416     _bindNodes: function(children)
417     {
418         for (var i = 0; i < children.length; ++i) {
419             var child = children[i];
420             this._idToDOMNode[child.id] = child;
421             if (child.children)
422                 this._bindNodes(child.children);
423         }
424     },
425
426     _childNodeCountUpdated: function(nodeId, newValue)
427     {
428         var node = this._idToDOMNode[nodeId];
429         node._childNodeCount = newValue;
430         var outline = WebInspector.panels.elements.treeOutline;
431         var treeElement = outline.findTreeElement(node);
432         if (treeElement)
433             treeElement.hasChildren = newValue;
434     },
435
436     _childNodeInserted: function(parentId, prevId, payload)
437     {
438         var parent = this._idToDOMNode[parentId];
439         var prev = this._idToDOMNode[prevId];
440         var node = parent._insertChild(prev, payload);
441         this._idToDOMNode[node.id] = node;
442         var event = { target : node, relatedNode : parent };
443         this.document._fireDomEvent("DOMNodeInserted", event);
444     },
445
446     _childNodeRemoved: function(parentId, nodeId)
447     {
448         var parent = this._idToDOMNode[parentId];
449         var node = this._idToDOMNode[nodeId];
450         parent.removeChild_(node);
451         var event = { target : node, relatedNode : parent };
452         this.document._fireDomEvent("DOMNodeRemoved", event);
453         delete this._idToDOMNode[nodeId];
454     }
455 }
456
457 WebInspector.Cookies = {}
458
459 WebInspector.Cookies.getCookiesAsync = function(callback)
460 {
461     function mycallback(cookies, cookiesString) {
462         if (cookiesString)
463             callback(WebInspector.Cookies.buildCookiesFromString(cookiesString), false);
464         else
465             callback(cookies, true);
466     }
467     var callId = WebInspector.Callback.wrap(mycallback);
468     InspectorBackend.getCookies(callId);
469 }
470
471 WebInspector.Cookies.buildCookiesFromString = function(rawCookieString)
472 {
473     var rawCookies = rawCookieString.split(/;\s*/);
474     var cookies = [];
475
476     if (!(/^\s*$/.test(rawCookieString))) {
477         for (var i = 0; i < rawCookies.length; ++i) {
478             var cookie = rawCookies[i];
479             var delimIndex = cookie.indexOf("=");
480             var name = cookie.substring(0, delimIndex);
481             var value = cookie.substring(delimIndex + 1);
482             var size = name.length + value.length;
483             cookies.push({ name: name, value: value, size: size });
484         }
485     }
486
487     return cookies;
488 }
489
490 WebInspector.Cookies.cookieMatchesResourceURL = function(cookie, resourceURL)
491 {
492     var match = resourceURL.match(WebInspector.URLRegExp);
493     if (!match)
494         return false;
495     // See WebInspector.URLRegExp for definitions of the group index constants.
496     if (!this.cookieDomainMatchesResourceDomain(cookie.domain, match[2]))
497         return false;
498     var resourcePort = match[3] ? match[3] : undefined;
499     var resourcePath = match[4] ? match[4] : '/';
500     return (resourcePath.indexOf(cookie.path) === 0
501         && (!cookie.port || resourcePort == cookie.port)
502         && (!cookie.secure || match[1].toLowerCase() === 'https'));
503 }
504
505 WebInspector.Cookies.cookieDomainMatchesResourceDomain = function(cookieDomain, resourceDomain)
506 {
507     if (cookieDomain.charAt(0) !== '.')
508         return resourceDomain === cookieDomain;
509     return !!resourceDomain.match(new RegExp("^([^\\.]+\\.)?" + cookieDomain.substring(1).escapeForRegExp() + "$"), "i");
510 }
511
512 WebInspector.EventListeners = {}
513
514 WebInspector.EventListeners.getEventListenersForNodeAsync = function(node, callback)
515 {
516     if (!node)
517         return;
518
519     var callId = WebInspector.Callback.wrap(callback);
520     InspectorBackend.getEventListenersForNode(callId, node.id);
521 }
522
523 WebInspector.CSSStyleDeclaration = function(payload)
524 {
525     this.id = payload.id;
526     this.injectedScriptId = payload.injectedScriptId;
527     this.width = payload.width;
528     this.height = payload.height;
529     this.__disabledProperties = payload.__disabledProperties;
530     this.__disabledPropertyValues = payload.__disabledPropertyValues;
531     this.__disabledPropertyPriorities = payload.__disabledPropertyPriorities;
532     this.uniqueStyleProperties = payload.uniqueStyleProperties;
533     this._shorthandValues = payload.shorthandValues;
534     this._propertyMap = {};
535     this._longhandProperties = {};
536     this.length = payload.properties.length;
537
538     for (var i = 0; i < this.length; ++i) {
539         var property = payload.properties[i];
540         var name = property.name;
541         this[i] = name;
542         this._propertyMap[name] = property;
543     }
544
545     // Index longhand properties.
546     for (var i = 0; i < this.uniqueStyleProperties.length; ++i) {
547         var name = this.uniqueStyleProperties[i];
548         var property = this._propertyMap[name];
549         if (property.shorthand) {
550             var longhands = this._longhandProperties[property.shorthand];
551             if (!longhands) {
552                 longhands = [];
553                 this._longhandProperties[property.shorthand] = longhands;
554             }
555             longhands.push(name);
556         }
557     }
558 }
559
560 WebInspector.CSSStyleDeclaration.parseStyle = function(payload)
561 {
562     return new WebInspector.CSSStyleDeclaration(payload);
563 }
564
565 WebInspector.CSSStyleDeclaration.parseRule = function(payload)
566 {
567     var rule = {};
568     rule.id = payload.id;
569     rule.injectedScriptId = payload.injectedScriptId;
570     rule.selectorText = payload.selectorText;
571     rule.style = new WebInspector.CSSStyleDeclaration(payload.style);
572     rule.style.parentRule = rule;
573     rule.isUserAgent = payload.isUserAgent;
574     rule.isUser = payload.isUser;
575     rule.isViaInspector = payload.isViaInspector;
576     if (payload.parentStyleSheet)
577         rule.parentStyleSheet = { href: payload.parentStyleSheet.href };
578
579     return rule;
580 }
581
582 WebInspector.CSSStyleDeclaration.prototype = {
583     getPropertyValue: function(name)
584     {
585         var property = this._propertyMap[name];
586         return property ? property.value : "";
587     },
588
589     getPropertyPriority: function(name)
590     {
591         var property = this._propertyMap[name];
592         return property ? property.priority : "";
593     },
594
595     getPropertyShorthand: function(name)
596     {
597         var property = this._propertyMap[name];
598         return property ? property.shorthand : "";
599     },
600
601     isPropertyImplicit: function(name)
602     {
603         var property = this._propertyMap[name];
604         return property ? property.implicit : "";
605     },
606
607     styleTextWithShorthands: function()
608     {
609         var cssText = "";
610         var foundProperties = {};
611         for (var i = 0; i < this.length; ++i) {
612             var individualProperty = this[i];
613             var shorthandProperty = this.getPropertyShorthand(individualProperty);
614             var propertyName = (shorthandProperty || individualProperty);
615
616             if (propertyName in foundProperties)
617                 continue;
618
619             if (shorthandProperty) {
620                 var value = this.getPropertyValue(shorthandProperty);
621                 var priority = this.getShorthandPriority(shorthandProperty);
622             } else {
623                 var value = this.getPropertyValue(individualProperty);
624                 var priority = this.getPropertyPriority(individualProperty);
625             }
626
627             foundProperties[propertyName] = true;
628
629             cssText += propertyName + ": " + value;
630             if (priority)
631                 cssText += " !" + priority;
632             cssText += "; ";
633         }
634
635         return cssText;
636     },
637
638     getLonghandProperties: function(name)
639     {
640         return this._longhandProperties[name] || [];
641     },
642
643     getShorthandValue: function(shorthandProperty)
644     {
645         return this._shorthandValues[shorthandProperty];
646     },
647
648     getShorthandPriority: function(shorthandProperty)
649     {
650         var priority = this.getPropertyPriority(shorthandProperty);
651         if (priority)
652             return priority;
653
654         var longhands = this._longhandProperties[shorthandProperty];
655         return longhands ? this.getPropertyPriority(longhands[0]) : null;
656     }
657 }
658
659 WebInspector.attributesUpdated = function()
660 {
661     this.domAgent._attributesUpdated.apply(this.domAgent, arguments);
662 }
663
664 WebInspector.setDocument = function()
665 {
666     this.domAgent._setDocument.apply(this.domAgent, arguments);
667 }
668
669 WebInspector.setDetachedRoot = function()
670 {
671     this.domAgent._setDetachedRoot.apply(this.domAgent, arguments);
672 }
673
674 WebInspector.setChildNodes = function()
675 {
676     this.domAgent._setChildNodes.apply(this.domAgent, arguments);
677 }
678
679 WebInspector.childNodeCountUpdated = function()
680 {
681     this.domAgent._childNodeCountUpdated.apply(this.domAgent, arguments);
682 }
683
684 WebInspector.childNodeInserted = function()
685 {
686     this.domAgent._childNodeInserted.apply(this.domAgent, arguments);
687 }
688
689 WebInspector.childNodeRemoved = function()
690 {
691     this.domAgent._childNodeRemoved.apply(this.domAgent, arguments);
692 }
693
694 WebInspector.didGetCookies = WebInspector.Callback.processCallback;
695 WebInspector.didGetChildNodes = WebInspector.Callback.processCallback;
696 WebInspector.didPerformSearch = WebInspector.Callback.processCallback;
697 WebInspector.didApplyDomChange = WebInspector.Callback.processCallback;
698 WebInspector.didRemoveAttribute = WebInspector.Callback.processCallback;
699 WebInspector.didSetTextNodeValue = WebInspector.Callback.processCallback;
700 WebInspector.didGetEventListenersForNode = WebInspector.Callback.processCallback;