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