Reviewed by Tim Hatcher.
[WebKit-https.git] / WebCore / page / inspector / inspector.js
1 /*
2  * Copyright (C) 2006, 2007 Apple Inc.  All rights reserved.
3  * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer. 
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution. 
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission. 
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 var Preferences = {
31     ignoreWhitespace: true,
32     showUserAgentStyles: true,
33     maxInlineTextChildLength: 80,
34     maxTextSearchResultLength: 80
35 }
36
37 var WebInspector = {
38     resources: [],
39     resourceURLMap: {},
40     backForwardList: [],
41
42     get consolePanel()
43     {
44         if (!this._consolePanel)
45             this._consolePanel = new WebInspector.ConsolePanel();
46
47         return this._consolePanel;
48     },
49
50     get networkPanel()
51     {
52         if (!this._networkPanel)
53             this._networkPanel = new WebInspector.NetworkPanel();
54
55         return this._networkPanel;
56     },
57
58     get currentBackForwardIndex()
59     {
60         if (this._currentBackForwardIndex === undefined)
61             this._currentBackForwardIndex = -1;
62
63         return this._currentBackForwardIndex;
64     },
65
66     set currentBackForwardIndex(x)
67     {
68         if (this._currentBackForwardIndex === x)
69             return;
70
71         this._currentBackForwardIndex = x;
72         this.updateBackForwardButtons();
73     },
74
75     get currentFocusElement()
76     {
77         return this._currentFocusElement;
78     },
79
80     set currentFocusElement(x)
81     {
82         if (!x || this._currentFocusElement === x)
83             return;
84
85         if (this._currentFocusElement) {
86             this._currentFocusElement.removeStyleClass("focused");
87             this._currentFocusElement.addStyleClass("blurred");
88         }
89
90         this._currentFocusElement = x;
91
92         if (x) {
93             x.addStyleClass("focused");
94             x.removeStyleClass("blurred");
95         }
96     },
97
98     get currentPanel()
99     {
100         return this._currentPanel;
101     },
102
103     set currentPanel(x)
104     {
105         if (this._currentPanel === x)
106             return;
107
108         if (this._currentPanel)
109             this._currentPanel.hide();
110
111         this._currentPanel = x;
112         this.updateViewButtons();
113
114         if (x)
115             x.show();
116     },
117
118     get attached()
119     {
120         return this._attached;
121     },
122
123     set attached(x)
124     {
125         if (this._attached === x)
126             return;
127
128         this._attached = x;
129
130         var body = document.body;
131         if (x) {
132             InspectorController.attach();
133             body.removeStyleClass("detached");
134             body.addStyleClass("attached");
135         } else {
136             InspectorController.detach();
137             body.removeStyleClass("attached");
138             body.addStyleClass("detached");
139         }
140     },
141
142     get showingStatusArea()
143     {
144         return this._showingStatusArea;
145     },
146
147     set showingStatusArea(x)
148     {
149         if (this._showingStatusArea === x)
150             return;
151
152         this._showingStatusArea = x;
153
154         var list = document.getElementById("list");
155         var status = document.getElementById("status");
156         var statusButton = document.getElementById("statusToggle");
157
158         if (x) {
159             statusButton.addStyleClass("hide");
160             WebInspector.animateStyle([{element: list, end: {bottom: 99}}, {element: status, end: {bottom: 21}}], 250);
161         } else {
162             statusButton.removeStyleClass("hide");
163             WebInspector.animateStyle([{element: list, end: {bottom: 21}}, {element: status, end: {bottom: -57}}], 250);
164         }
165     },
166
167     get showingSearchResults()
168     {
169         return this._showingSearchResults;
170     },
171
172     set showingSearchResults(x)
173     {
174         if (this._showingSearchResults === x)
175             return;
176
177         this._showingSearchResults = x;
178
179         var resultsContainer = document.getElementById("searchResults");
180         if (x) {
181             var animations = [
182                 {element: resultsContainer, end: {top: 28}},
183                 {element: document.getElementById("main"), end: {top: 129}}
184             ];
185             WebInspector.animateStyle(animations, 250);
186         } else {
187             var animations = [
188                 {element: resultsContainer, end: {top: -73}},
189                 {element: document.getElementById("main"), end: {top: 28}}
190             ];
191             WebInspector.animateStyle(animations, 250, function() { resultsContainer.removeChildren(); delete this.searchResultsTree; });
192         }
193     }
194 }
195
196 WebInspector.loaded = function(event)
197 {
198     this.fileOutline = new TreeOutline(document.getElementById("list"));
199     this.statusOutline = new TreeOutline(document.getElementById("status"));
200
201     this.resourceCategories = {
202         documents: new WebInspector.ResourceCategory("documents"),
203         stylesheets: new WebInspector.ResourceCategory("stylesheets"),
204         images: new WebInspector.ResourceCategory("images"),
205         scripts: new WebInspector.ResourceCategory("scripts"),
206         other: new WebInspector.ResourceCategory("other")
207     };
208
209     this.consoleListItem = new WebInspector.ConsoleStatusTreeElement();
210     this.consoleListItem.item.onselect = function(element) { WebInspector.StatusTreeElement.selected(element); WebInspector.navigateToPanel(WebInspector.consolePanel) };
211     this.consoleListItem.item.ondeselect = function(element) { WebInspector.consolePanel.hide() };
212     this.statusOutline.appendChild(this.consoleListItem.item);
213
214     this.networkListItem = new WebInspector.StatusTreeElement("Network");
215     this.networkListItem.onselect = function(element) { WebInspector.StatusTreeElement.selected(element); WebInspector.navigateToPanel(WebInspector.networkPanel); };
216     this.networkListItem.ondeselect = function(element) { WebInspector.networkPanel.hide() };
217     this.statusOutline.appendChild(this.networkListItem);
218
219     this.resourceCategories.documents.listItem.expand();
220
221     this.currentFocusElement = document.getElementById("sidebar");
222
223     this.addMainEventListeners(document);
224
225     window.addEventListener("unload", function(event) { WebInspector.windowUnload(event) }, true);
226     document.addEventListener("mousedown", function(event) { WebInspector.changeFocus(event) }, true);
227     document.addEventListener("focus", function(event) { WebInspector.changeFocus(event) }, true);
228     document.addEventListener("keypress", function(event) { WebInspector.documentKeypress(event) }, true);
229
230     document.getElementById("back").title = "Show previous panel.";
231     document.getElementById("forward").title = "Show next panel.";
232
233     document.getElementById("back").addEventListener("click", function(event) { WebInspector.back() }, true);
234     document.getElementById("forward").addEventListener("click", function(event) { WebInspector.forward() }, true);
235     this.updateBackForwardButtons();
236
237     document.getElementById("attachToggle").addEventListener("click", function(event) { WebInspector.toggleAttach() }, true);
238     document.getElementById("statusToggle").addEventListener("click", function(event) { WebInspector.toggleStatusArea() }, true);
239     document.getElementById("sidebarResizeWidget").addEventListener("mousedown", WebInspector.sidebarResizerDragStart, true);
240
241     document.body.addStyleClass("detached");
242
243     window.removeEventListener("load", this.loaded, false);
244     delete this.loaded;
245
246     InspectorController.loaded();
247 }
248
249 window.addEventListener("load", function(event) { WebInspector.loaded(event) }, false);
250
251 WebInspector.windowUnload = function(event)
252 {
253     InspectorController.windowUnloading();
254 }
255
256 WebInspector.windowFocused = function(event)
257 {
258     if (event.target.nodeType === Node.DOCUMENT_NODE)
259         document.body.removeStyleClass("inactive");
260 }
261
262 WebInspector.windowBlured = function(event)
263 {
264     if (event.target.nodeType === Node.DOCUMENT_NODE)
265         document.body.addStyleClass("inactive");
266 }
267
268 WebInspector.changeFocus = function(event)
269 {
270     var nextFocusElement;
271
272     var current = event.target;
273     while (current) {
274         if (current.nodeName.toLowerCase() === "input")
275             nextFocusElement = current;
276         current = current.parentNode;
277     }
278
279     if (!nextFocusElement)
280         nextFocusElement = event.target.firstParentWithClass("focusable");
281
282     this.currentFocusElement = nextFocusElement;
283 }
284
285 WebInspector.documentClick = function(event)
286 {
287     var anchor = event.target.firstParentOrSelfWithNodeName("a");
288     if (!anchor || !anchor.hasStyleClass("webkit-html-resource-link"))
289         return;
290
291     if (WebInspector.showResourceForURL(anchor.getAttribute("href"))) {
292         event.preventDefault();
293         event.stopPropagation();
294     }
295 }
296
297 WebInspector.documentKeypress = function(event)
298 {
299     if (!this.currentFocusElement || !this.currentFocusElement.id || !this.currentFocusElement.id.length)
300         return;
301     if (this.currentFocusElement.id + "Keypress" in WebInspector)
302         WebInspector[this.currentFocusElement.id + "Keypress"](event);
303 }
304
305 WebInspector.sidebarKeypress = function(event)
306 {
307     var nextSelectedElement;
308
309     if (this.fileOutline.selectedTreeElement) {
310         if (!this.fileOutline.handleKeyEvent(event) && event.keyIdentifier === "Down" && !event.altKey && this.showingStatusArea) {
311             var nextSelectedElement = this.statusOutline.children[0];
312             while (nextSelectedElement && !nextSelectedElement.selectable)
313                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
314         }
315     } else if (this.statusOutline.selectedTreeElement) {
316         if (!this.showingStatusArea || (!this.statusOutline.handleKeyEvent(event) && event.keyIdentifier === "Up" && !event.altKey)) {
317             var nextSelectedElement = this.fileOutline.children[0];
318             var lastSelectable = null;
319
320             while (nextSelectedElement) {
321                 if (nextSelectedElement.selectable)
322                     lastSelectable = nextSelectedElement;
323                 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
324             }
325
326             nextSelectedElement = lastSelectable;
327         }
328     }
329
330     if (nextSelectedElement) {
331         nextSelectedElement.reveal();
332         nextSelectedElement.select();
333
334         event.preventDefault();
335         event.stopPropagation();
336     }
337 }
338
339 WebInspector.mainKeypress = function(event)
340 {
341     if (this.currentPanel && this.currentPanel.handleKeyEvent)
342         this.currentPanel.handleKeyEvent(event);
343 }
344
345 WebInspector.searchResultsKeypress = function(event)
346 {
347     if (this.searchResultsTree)
348         this.searchResultsTree.handleKeyEvent(event);
349 }
350
351 WebInspector.animateStyle = function(animations, duration, callback, complete)
352 {
353     if (complete === undefined)
354         complete = 0;
355     var slice = (1000 / 30); // 30 frames per second
356
357     var defaultUnit = "px";
358     var propertyUnit = {opacity: ""};
359
360     for (var i = 0; i < animations.length; ++i) {
361         var animation = animations[i];
362         var element = null;
363         var start = null;
364         var current = null;
365         var end = null;
366         for (key in animation) {
367             if (key === "element")
368                 element = animation[key];
369             else if (key === "start")
370                 start = animation[key];
371             else if (key == "current")
372                 current = animation[key];
373             else if (key === "end")
374                 end = animation[key];
375         }
376
377         if (!element || !end)
378             continue;
379
380         var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
381         if (!start) {
382             start = {};
383             for (key in end)
384                 start[key] = parseInt(computedStyle.getPropertyValue(key));
385             animation.start = start;
386         } else if (complete == 0)
387             for (key in start)
388                 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
389
390         if (!current) {
391             current = {};
392             for (key in start)
393                 current[key] = start[key];
394             animation.current = current;
395         }
396
397         function cubicInOut(t, b, c, d)
398         {
399             if ((t/=d/2) < 1) return c/2*t*t*t + b;
400             return c/2*((t-=2)*t*t + 2) + b;
401         }
402
403         var style = element.style;
404         for (key in end) {
405             var startValue = start[key];
406             var currentValue = current[key];
407             var endValue = end[key];
408             if ((complete + slice) < duration) {
409                 var delta = (endValue - startValue) / (duration / slice);
410                 var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
411                 style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
412                 current[key] = newValue;
413             } else {
414                 style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
415             }
416         }
417     }
418
419     if (complete < duration)
420         setTimeout(WebInspector.animateStyle, slice, animations, duration, callback, complete + slice);
421     else if (callback)
422         callback();
423 }
424
425 WebInspector.toggleAttach = function()
426 {
427     this.attached = !this.attached;
428 }
429
430 WebInspector.toggleStatusArea = function()
431 {
432     this.showingStatusArea = !this.showingStatusArea;
433 }
434
435 WebInspector.sidebarResizerDragStart = function(event)
436 {
437     WebInspector.dividerDragStart(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event, "col-resize");
438 }
439
440 WebInspector.sidebarResizerDragEnd = function(event)
441 {
442     WebInspector.dividerDragEnd(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event);
443 }
444
445 WebInspector.sidebarResizerDrag = function(event)
446 {
447     var sidebar = document.getElementById("sidebar");
448     if (sidebar.dragging == true) {
449         var main = document.getElementById("main");
450
451         var x = event.clientX + window.scrollX;
452         var delta = sidebar.dragLastX - x;
453         var newWidth = WebInspector.constrainedWidthFromElement(x, main);
454
455         if (x == newWidth)
456             sidebar.dragLastX = x;
457
458         sidebar.style.width = newWidth + "px";
459         main.style.left = newWidth + "px";
460         event.preventDefault();
461     }
462 }
463
464 WebInspector.dividerDragStart = function(element, dividerDrag, dividerDragEnd, event, cursor) 
465 {
466     element.dragging = true;
467     element.dragLastY = event.clientY + window.scrollY;
468     element.dragLastX = event.clientX + window.scrollX;
469     document.addEventListener("mousemove", dividerDrag, true);
470     document.addEventListener("mouseup", dividerDragEnd, true);
471     document.body.style.cursor = cursor;
472     event.preventDefault();
473 }
474
475 WebInspector.dividerDragEnd = function(element, dividerDrag, dividerDragEnd, event) 
476 {
477     element.dragging = false;
478     document.removeEventListener("mousemove", dividerDrag, true);
479     document.removeEventListener("mouseup", dividerDragEnd, true);
480     document.body.style.removeProperty("cursor");
481 }
482
483 WebInspector.constrainedWidthFromElement = function(width, element, constrainLeft, constrainRight) 
484 {
485     if (constrainLeft === undefined) constrainLeft = 0.25;
486     if (constrainRight === undefined) constrainRight = 0.75;
487
488     if (width < element.clientWidth * constrainLeft)
489         width = 200;
490     else if (width > element.clientWidth * constrainRight)
491         width = element.clientWidth * constrainRight;
492
493     return width;
494 }
495
496 WebInspector.back = function()
497 {
498     if (this.currentBackForwardIndex <= 0) {
499         alert("Can't go back from index " + this.currentBackForwardIndex);
500         return;
501     }
502
503     this.navigateToPanel(this.backForwardList[--this.currentBackForwardIndex], true);
504 }
505
506 WebInspector.forward = function()
507 {
508     if (this.currentBackForwardIndex >= this.backForwardList.length - 1) {
509         alert("Can't go forward from index " + this.currentBackForwardIndex);
510         return;
511     }
512
513     this.navigateToPanel(this.backForwardList[++this.currentBackForwardIndex], true);
514 }
515
516 WebInspector.updateBackForwardButtons = function()
517 {
518     var index = this.currentBackForwardIndex;
519
520     document.getElementById("back").disabled = index <= 0;
521     document.getElementById("forward").disabled = index >= this.backForwardList.length - 1;
522 }
523
524 WebInspector.updateViewButtons = function()
525 {
526     var buttonContainer = document.getElementById("viewbuttons");
527     buttonContainer.removeChildren();
528
529     if (!this.currentPanel || !this.currentPanel.viewButtons)
530         return;
531
532     var buttons = this.currentPanel.viewButtons;
533     if (buttons.length < 2)
534         return;
535
536     for (var i = 0; i < buttons.length; ++i) {
537         var button = buttons[i];
538
539         if (i === 0)
540             button.addStyleClass("first");
541         else if (i === (buttons.length - 1))
542             button.addStyleClass("last");
543
544         if (i) {
545             var divider = document.createElement("img");
546             divider.className = "split-button-divider";
547             buttonContainer.appendChild(divider);
548         }
549
550         button.addStyleClass("split-button");
551         button.addStyleClass("view-button-" + button.title.toLowerCase());
552
553         buttonContainer.appendChild(button);
554     }
555 }
556
557 WebInspector.addResource = function(resource)
558 {
559     this.resources.push(resource);
560     this.resourceURLMap[resource.url] = resource;
561
562     if (resource.mainResource)
563         this.mainResource = resource;
564
565     this.networkPanel.addResourceToTimeline(resource);
566 }
567
568 WebInspector.removeResource = function(resource)
569 {
570     resource.detach();
571
572     resource.category.removeResource(resource);
573
574     delete this.resourceURLMap[resource.url];
575
576     var resourcesLength = this.resources.length;
577     for (var i = 0; i < resourcesLength; ++i) {
578         if (this.resources[i] === resource) {
579             this.resources.splice(i, 1);
580             break;
581         }
582     }
583 }
584
585 WebInspector.clearResources = function()
586 {
587     for (var category in this.resourceCategories)
588         this.resourceCategories[category].removeAllResources();
589     this.resources = [];
590     this.backForwardList = [];
591     this.currentBackForwardIndex = -1;
592     delete this.mainResource;
593 }
594
595 WebInspector.resourceURLChanged = function(resource, oldURL)
596 {
597     delete this.resourceURLMap[oldURL];
598     this.resourceURLMap[resource.url] = resource;
599 }
600
601 WebInspector.addMessageToConsole = function(msg)
602 {
603     this.consolePanel.addMessage(msg);
604     switch (msg.level) {
605         case WebInspector.ConsoleMessage.WarningMessageLevel:
606             ++this.consoleListItem.warnings;
607             this.showingStatusArea = true;
608             break;
609         case WebInspector.ConsoleMessage.ErrorMessageLevel:
610             ++this.consoleListItem.errors;
611             this.showingStatusArea = true;
612             break;
613     }
614 }
615
616 WebInspector.clearConsoleMessages = function()
617 {
618     this.consolePanel.clearMessages();
619     this.consoleListItem.warnings = this.consoleListItem.errors = 0;
620 }
621
622 WebInspector.clearNetworkTimeline = function()
623 {
624     if (this._networkPanel)
625         this._networkPanel.clearTimeline();
626 }
627
628 WebInspector.drawLoadingPieChart = function(canvas, percent) {
629     var g = canvas.getContext("2d");
630     var darkColor = "rgb(122, 168, 218)";
631     var lightColor = "rgb(228, 241, 251)";
632     var cx = 8;
633     var cy = 8;
634     var r = 7;
635
636     g.beginPath();
637     g.arc(cx, cy, r, 0, Math.PI * 2, false); 
638     g.closePath();
639
640     g.lineWidth = 1;
641     g.strokeStyle = darkColor;
642     g.fillStyle = lightColor;
643     g.fill();
644     g.stroke();
645
646     var startangle = -Math.PI / 2;
647     var endangle = startangle + (percent * Math.PI * 2);
648
649     g.beginPath();
650     g.moveTo(cx, cy);
651     g.arc(cx, cy, r, startangle, endangle, false); 
652     g.closePath();
653
654     g.fillStyle = darkColor;
655     g.fill();
656 }
657
658 WebInspector.updateFocusedNode = function(node)
659 {
660     if (!node)
661         // FIXME: Should we deselect if null is passed in?
662         return;
663
664     for (var i = 0; i < this.resourceCategories.documents.resources.length; ++i) {
665         var resource = this.resourceCategories.documents.resources[i];
666         if (resource.documentNode !== node.ownerDocument)
667             continue;
668
669         resource.panel.navigateToView("dom");
670         resource.panel.focusedDOMNode = node;
671
672         this.currentFocusElement = document.getElementById("main");
673
674         break;
675     }
676 }
677
678 WebInspector.resourceForURL = function(url)
679 {
680     for (var resourceURL in this.resourceURLMap) {
681         if (resourceURL.hasSubstring(url))
682             return this.resourceURLMap[resourceURL];
683     }
684
685     return null;
686 }
687
688 WebInspector.showResourceForURL = function(url)
689 {
690     var resource = this.resourceForURL(url);
691     if (!resource)
692         return false;
693
694     this.navigateToResource(resource);
695     return true;
696 }
697
698 WebInspector.linkifyURL = function(url, linkText, isExternal)
699 {
700     if (linkText === undefined)
701         linkText = url.escapeHTML();
702     var className = isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
703     var link = "<a href=\"" + url + "\" class=\"" + className + "\" target=\"_blank\">" + linkText + "</a>";
704     return link;
705 }
706
707 WebInspector.addMainEventListeners = function(doc)
708 {
709     doc.defaultView.addEventListener("focus", function(event) { WebInspector.windowFocused(event) }, true);
710     doc.defaultView.addEventListener("blur", function(event) { WebInspector.windowBlured(event) }, true);
711     doc.addEventListener("click", function(event) { WebInspector.documentClick(event) }, true);
712 }
713
714 WebInspector.navigateToView = function(view)
715 {
716     if (!view) {
717         alert("Called navigateToView(null)");
718         return;
719     }
720
721     view.panel.currentView = view;
722     this.navigateToPanel(view.panel);
723 }
724
725 WebInspector.performSearch = function(query)
726 {
727     if (!query || !query.length) {
728         this.showingSearchResults = false;
729         return;
730     }
731
732     var resultsContainer = document.getElementById("searchResults");
733     resultsContainer.removeChildren();
734
735     var isXPath = query.indexOf("/") !== -1;
736
737     var xpathQuery;
738     if (isXPath)
739         xpathQuery = query;
740     else {
741         var escapedQuery = query.escapeCharacters("'");
742         xpathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
743     }
744
745     var resourcesToSearch = [].concat(this.resourceCategories.documents.resources, this.resourceCategories.stylesheets.resources, this.resourceCategories.scripts.resources, this.resourceCategories.other.resources);
746
747     var files = [];
748     for (var i = 0; i < resourcesToSearch.length; ++i) {
749         var resource = resourcesToSearch[i];
750
751         var sourceResults = [];
752         if (!isXPath) {
753             resource.panel.refreshIfNeeded();
754             if ("sourceFrame" in resource.panel)
755                 sourceResults = InspectorController.search(resource.panel.sourceFrame.contentDocument, query);
756         }
757
758         var domResults = [];
759         if (resource.category === this.resourceCategories.documents) {
760             try {
761                 var doc = resource.documentNode;
762                 var nodeList = doc.evaluate(xpathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
763                 for (var j = 0; j < nodeList.snapshotLength; ++j)
764                     domResults.push(nodeList.snapshotItem(i));
765             } catch(err) {
766                 // ignore any exceptions. the query might be malformed, but we allow that.
767             }
768         }
769
770         if ((!sourceResults || !sourceResults.length) && !domResults.length)
771             continue;
772
773         files.push({resource: resource, sourceResults: sourceResults, domResults: domResults});
774     }
775
776     if (!files.length)
777         return;
778
779     this.showingSearchResults = true;
780
781     var fileList = document.createElement("ol");
782     fileList.className = "outline-disclosure";
783     resultsContainer.appendChild(fileList);
784     this.searchResultsTree = new TreeOutline(fileList);
785
786     var sourceResultSelected = function(element)
787     {
788         var selection = window.getSelection();
789         selection.removeAllRanges();
790         selection.addRange(element.representedObject.range);
791
792         element.representedObject.panel.navigateToView("source");
793         element.representedObject.line.scrollIntoView(true);
794         resultsContainer.scrollToElement(element._listItemNode);
795     }
796
797     var domResultSelected = function(element)
798     {
799         element.representedObject.panel.navigateToView("dom");
800         element.representedObject.panel.focusedDOMNode = element.representedObject.node;
801         resultsContainer.scrollToElement(element._listItemNode);
802     }
803
804     for (var i = 0; i < files.length; ++i) {
805         var file = files[i];
806
807         var fileItem = new TreeElement(file.resource.displayName, {}, true);
808         fileItem.expanded = true;
809         fileItem.selectable = false;
810         this.searchResultsTree.appendChild(fileItem);
811
812         if (file.sourceResults.length) {
813             for (var j = 0; j < file.sourceResults.length; ++j) {
814                 var range = file.sourceResults[j];
815
816                 var line = range.startContainer;
817                 while (line.parentNode && line.nodeName.toLowerCase() != "tr")
818                     line = line.parentNode;
819                 var lineRange = file.resource.panel.sourceFrame.contentDocument.createRange();
820                 lineRange.selectNodeContents(line);
821
822                 // Don't include any error bubbles in the search result
823                 var end = line.lastChild.lastChild;
824                 if (end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble")) {
825                     while (end && end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble"))
826                         end = end.previousSibling;
827                     lineRange.setEndAfter(end);
828                 }
829
830                 var beforeRange = file.resource.panel.sourceFrame.contentDocument.createRange();
831                 beforeRange.setStart(lineRange.startContainer, lineRange.startOffset);
832                 beforeRange.setEnd(range.startContainer, range.startOffset);
833
834                 var afterRange = file.resource.panel.sourceFrame.contentDocument.createRange();
835                 afterRange.setStart(range.endContainer, range.endOffset);
836                 afterRange.setEnd(lineRange.endContainer, lineRange.endOffset);
837
838                 var beforeText = beforeRange.toString().trimLeadingWhitespace();
839                 var text = range.toString();
840                 var afterText = afterRange.toString().trimTrailingWhitespace();
841
842                 var length = beforeText.length + text.length + afterText.length;
843                 if (length > Preferences.maxTextSearchResultLength) {
844                     var beforeAfterLength = (Preferences.maxTextSearchResultLength - text.length) / 2;
845                     if (beforeText.length > beforeAfterLength)
846                         beforeText = "\u2026" + beforeText.substr(-beforeAfterLength);
847                     if (afterText.length > beforeAfterLength)
848                         afterText = afterText.substr(0, beforeAfterLength) + "\u2026";
849                 }
850
851                 var title = "<div class=\"selection selected\"></div>";
852                 if (j == 0)
853                     title += "<div class=\"search-results-section\">Source</div>";
854                 title += beforeText.escapeHTML() + "<span class=\"search-matched-string\">" + text.escapeHTML() + "</span>" + afterText.escapeHTML();
855                 var item = new TreeElement(title, {panel: file.resource.panel, line: line, range: range}, false);
856                 item.onselect = sourceResultSelected;
857                 fileItem.appendChild(item);
858             }
859         }
860
861         if (file.domResults.length) {
862             for (var j = 0; j < file.domResults.length; ++j) {
863                 var node = file.domResults[j];
864                 var title = "<div class=\"selection selected\"></div>";
865                 if (j == 0)
866                     title += "<div class=\"search-results-section\">DOM</div>";
867                 title += nodeTitleInfo.call(node).title;
868                 var item = new TreeElement(title, {panel: file.resource.panel, node: node}, false);
869                 item.onselect = domResultSelected;
870                 fileItem.appendChild(item);
871             }
872         }
873     }
874 }
875
876 WebInspector.navigateToResource = function(resource)
877 {
878     this.navigateToPanel(resource.panel);
879 }
880
881 WebInspector.navigateToPanel = function(panel, fromBackForwardAction)
882 {
883     if (this.currentPanel == panel)
884         return;
885
886     if (!fromBackForwardAction) {
887         var oldIndex = this.currentBackForwardIndex;
888         if (oldIndex >= 0)
889             this.backForwardList.splice(oldIndex + 1, this.backForwardList.length - oldIndex);
890         this.currentBackForwardIndex++;
891         this.backForwardList.push(panel);
892     }
893
894     this.currentPanel = panel;
895 }
896
897 WebInspector.Panel = function()
898 {
899     this.element = document.createElement("div");
900     this.element.className = "panel";
901     this.attach();
902
903     this._needsRefresh = true;
904     this.refresh();
905 }
906
907 WebInspector.Panel.prototype = {
908     show: function()
909     {
910         this.visible = true;
911     },
912
913     hide: function()
914     {
915         this.visible = false;
916     },
917
918     attach: function()
919     {
920         document.getElementById("main").appendChild(this.element);
921     },
922
923     detach: function()
924     {
925         if (WebInspector.currentPanel === this)
926             WebInspector.currentPanel = null;
927         if (this.element && this.element.parentNode)
928             this.element.parentNode.removeChild(this.element);
929     },
930
931     refresh: function()
932     {
933     },
934
935     refreshIfNeeded: function()
936     {
937         if (this.needsRefresh)
938             this.refresh();
939     },
940
941     get visible()
942     {
943         return this._visible;
944     },
945
946     set visible(x)
947     {
948         if (this._visible === x)
949             return;
950
951         this._visible = x;
952
953         if (x) {
954             this.element.addStyleClass("selected");
955             this.refreshIfNeeded();
956         } else {
957             this.element.removeStyleClass("selected");
958         }
959     },
960
961     get needsRefresh()
962     {
963         return this._needsRefresh;
964     },
965
966     set needsRefresh(x)
967     {
968         if (this._needsRefresh === x)
969             return;
970         this._needsRefresh = x;
971         if (x && this.visible)
972             this.refresh();
973     }
974 }
975
976 WebInspector.StatusTreeElement = function(title)
977 {
978     var item = new TreeElement("<span class=\"title only\">" + title + "</span><span class=\"icon " + title.toLowerCase() + "\"></span>", {}, false);
979     item.onselect = WebInspector.StatusTreeElement.selected;
980     return item;
981 }
982
983 WebInspector.StatusTreeElement.selected = function(element)
984 {
985     var selectedElement = WebInspector.fileOutline.selectedTreeElement;
986     if (selectedElement)
987         selectedElement.deselect();
988 }
989
990 WebInspector.ConsoleStatusTreeElement = function()
991 {
992     this.item = WebInspector.StatusTreeElement.call(this, "Console");
993 }
994
995 WebInspector.ConsoleStatusTreeElement.prototype = {
996     get warnings()
997     {
998         if (!("_warnings" in this))
999             this._warnings = 0;
1000
1001         return this._warnings;
1002     },
1003
1004     set warnings(x)
1005     {
1006         if (this._warnings === x)
1007             return;
1008
1009         this._warnings = x;
1010
1011         this._updateTitle();
1012     },
1013
1014     get errors()
1015     {
1016         if (!("_errors" in this))
1017             this._errors = 0;
1018
1019         return this._errors;
1020     },
1021
1022     set errors(x)
1023     {
1024         if (this._errors === x)
1025             return;
1026
1027         this._errors = x;
1028
1029         this._updateTitle();
1030     },
1031
1032     _updateTitle: function()
1033     {
1034         var title = "<span class=\"title";
1035         if (!this.warnings && !this.errors)
1036             title += " only";
1037         title += "\">Console</span><span class=\"icon console\"></span>";
1038
1039         if (this.warnings || this.errors) {
1040             title += "<span class=\"info\">";
1041             if (this.errors) {
1042                 title += this.errors + " error";
1043                 if (this.errors > 1)
1044                     title += "s";
1045             }
1046             if (this.warnings) {
1047                 if (this.errors)
1048                     title += ", ";
1049                 title += this.warnings + " warning";
1050                 if (this.warnings > 1)
1051                     title += "s";
1052             }
1053             title += "</span>";
1054         }
1055
1056         this.item.title = title;
1057     }
1058 }
1059
1060 WebInspector.ConsoleStatusTreeElement.prototype.__proto__ = WebInspector.StatusTreeElement.prototype;
1061
1062 WebInspector.Tips = {
1063     ResourceNotCompressed: {id: 0, message: "You could save bandwidth by having your web server compress this transfer with gzip or zlib."}
1064 }
1065
1066 WebInspector.Warnings = {
1067     IncorrectMIMEType: {id: 0, message: "Resource interpreted as %s but transferred with MIME type %s."}
1068 }
1069
1070 // This table maps MIME types to the Resource.Types which are valid for them.
1071 // The following line:
1072 //    "text/html":                {0: 1},
1073 // means that text/html is a valid MIME type for resources that have type
1074 // WebInspector.Resource.Type.Document (which has a value of 0).
1075 WebInspector.MIMETypes = {
1076     "text/html":                {0: 1},
1077     "text/xml":                 {0: 1},
1078     "text/plain":               {0: 1},
1079     "application/xhtml+xml":    {0: 1},
1080     "text/css":                 {1: 1},
1081     "text/xsl":                 {1: 1},
1082     "image/jpeg":               {2: 1},
1083     "image/png":                {2: 1},
1084     "image/gif":                {2: 1},
1085     "image/bmp":                {2: 1},
1086     "image/x-icon":             {2: 1},
1087     "image/x-xbitmap":          {2: 1},
1088     "text/javascript":          {3: 1},
1089     "text/ecmascript":          {3: 1},
1090     "application/javascript":   {3: 1},
1091     "application/ecmascript":   {3: 1},
1092     "application/x-javascript": {3: 1},
1093     "text/javascript1.1":       {3: 1},
1094     "text/javascript1.2":       {3: 1},
1095     "text/javascript1.3":       {3: 1},
1096     "text/jscript":             {3: 1},
1097     "text/livescript":          {3: 1},
1098 }