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