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