• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4 * Copyright (C) 2009 Joseph Pecoraro
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1.  Redistributions of source code must retain the above copyright
11 *     notice, this list of conditions and the following disclaimer.
12 * 2.  Redistributions in binary form must reproduce the above copyright
13 *     notice, this list of conditions and the following disclaimer in the
14 *     documentation and/or other materials provided with the distribution.
15 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 *     its contributors may be used to endorse or promote products derived
17 *     from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.ElementsPanel = function()
32{
33    WebInspector.Panel.call(this);
34
35    this.element.addStyleClass("elements");
36
37    this.contentElement = document.createElement("div");
38    this.contentElement.id = "elements-content";
39    this.contentElement.className = "outline-disclosure";
40
41    this.treeOutline = new WebInspector.ElementsTreeOutline();
42    this.treeOutline.panel = this;
43    this.treeOutline.includeRootDOMNode = false;
44    this.treeOutline.selectEnabled = true;
45
46    this.treeOutline.focusedNodeChanged = function(forceUpdate)
47    {
48        if (this.panel.visible && WebInspector.currentFocusElement !== document.getElementById("search"))
49            WebInspector.currentFocusElement = document.getElementById("main-panels");
50
51        this.panel.updateBreadcrumb(forceUpdate);
52
53        for (var pane in this.panel.sidebarPanes)
54           this.panel.sidebarPanes[pane].needsUpdate = true;
55
56        this.panel.updateStyles(true);
57        this.panel.updateMetrics();
58        this.panel.updateProperties();
59
60        if (InspectorController.searchingForNode()) {
61            InspectorController.toggleNodeSearch();
62            this.panel.nodeSearchButton.removeStyleClass("toggled-on");
63        }
64        WebInspector.console.addInspectedNode(this._focusedDOMNode);
65    };
66
67    this.contentElement.appendChild(this.treeOutline.element);
68
69    this.crumbsElement = document.createElement("div");
70    this.crumbsElement.className = "crumbs";
71    this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
72    this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false);
73
74    this.sidebarPanes = {};
75    this.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
76    this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
77    this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
78
79    this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this);
80    this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this);
81    this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this);
82
83    this.sidebarPanes.styles.expanded = true;
84
85    this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this);
86    this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this);
87    this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this);
88
89    this.sidebarElement = document.createElement("div");
90    this.sidebarElement.id = "elements-sidebar";
91
92    this.sidebarElement.appendChild(this.sidebarPanes.styles.element);
93    this.sidebarElement.appendChild(this.sidebarPanes.metrics.element);
94    this.sidebarElement.appendChild(this.sidebarPanes.properties.element);
95
96    this.sidebarResizeElement = document.createElement("div");
97    this.sidebarResizeElement.className = "sidebar-resizer-vertical";
98    this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
99
100    this.nodeSearchButton = this.createStatusBarButton();
101    this.nodeSearchButton.title = WebInspector.UIString("Select an element in the page to inspect it.");
102    this.nodeSearchButton.id = "node-search-status-bar-item";
103    this.nodeSearchButton.className = "status-bar-item";
104    this.nodeSearchButton.addEventListener("click", this._nodeSearchButtonClicked.bind(this), false);
105
106    this.searchingForNode = false;
107
108    this.element.appendChild(this.contentElement);
109    this.element.appendChild(this.sidebarElement);
110    this.element.appendChild(this.sidebarResizeElement);
111
112    this._mutationMonitoredWindows = [];
113    this._nodeInsertedEventListener = InspectorController.wrapCallback(this._nodeInserted.bind(this));
114    this._nodeRemovedEventListener = InspectorController.wrapCallback(this._nodeRemoved.bind(this));
115    this._contentLoadedEventListener = InspectorController.wrapCallback(this._contentLoaded.bind(this));
116
117    this.stylesheet = null;
118    this.styles = {};
119
120    this.reset();
121}
122
123WebInspector.ElementsPanel.prototype = {
124    toolbarItemClass: "elements",
125
126    get toolbarItemLabel()
127    {
128        return WebInspector.UIString("Elements");
129    },
130
131    get statusBarItems()
132    {
133        return [this.nodeSearchButton, this.crumbsElement];
134    },
135
136    updateStatusBarItems: function()
137    {
138        this.updateBreadcrumbSizes();
139    },
140
141    show: function()
142    {
143        WebInspector.Panel.prototype.show.call(this);
144        this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px";
145        this.updateBreadcrumb();
146        this.treeOutline.updateSelection();
147        if (this.recentlyModifiedNodes.length)
148            this._updateModifiedNodes();
149    },
150
151    hide: function()
152    {
153        WebInspector.Panel.prototype.hide.call(this);
154
155        WebInspector.hoveredDOMNode = null;
156
157        if (InspectorController.searchingForNode()) {
158            InspectorController.toggleNodeSearch();
159            this.nodeSearchButton.removeStyleClass("toggled-on");
160        }
161    },
162
163    resize: function()
164    {
165        this.treeOutline.updateSelection();
166        this.updateBreadcrumbSizes();
167    },
168
169    reset: function()
170    {
171        this.rootDOMNode = null;
172        this.focusedDOMNode = null;
173
174        WebInspector.hoveredDOMNode = null;
175
176        if (InspectorController.searchingForNode()) {
177            InspectorController.toggleNodeSearch();
178            this.nodeSearchButton.removeStyleClass("toggled-on");
179        }
180
181        this.recentlyModifiedNodes = [];
182        this.unregisterAllMutationEventListeners();
183
184        delete this.currentQuery;
185        this.searchCanceled();
186
187        var inspectedWindow = Preferences.useDOMAgent ? WebInspector.domAgent.inspectedWindow : InspectorController.inspectedWindow();
188        if (!inspectedWindow || !inspectedWindow.document)
189            return;
190
191        if (!inspectedWindow.document.firstChild) {
192            function contentLoaded()
193            {
194                inspectedWindow.document.removeEventListener("DOMContentLoaded", contentLoadedCallback, false);
195
196                this.reset();
197            }
198
199            var contentLoadedCallback = InspectorController.wrapCallback(contentLoaded.bind(this));
200            inspectedWindow.document.addEventListener("DOMContentLoaded", contentLoadedCallback, false);
201            return;
202        }
203
204        // If the window isn't visible, return early so the DOM tree isn't built
205        // and mutation event listeners are not added.
206        if (!InspectorController.isWindowVisible())
207            return;
208
209        this.registerMutationEventListeners(inspectedWindow);
210
211        var inspectedRootDocument = inspectedWindow.document;
212        this.rootDOMNode = inspectedRootDocument;
213
214        var canidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement;
215        if (canidateFocusNode) {
216            this.treeOutline.suppressSelectHighlight = true;
217            this.focusedDOMNode = canidateFocusNode;
218            this.treeOutline.suppressSelectHighlight = false;
219
220            if (this.treeOutline.selectedTreeElement)
221                this.treeOutline.selectedTreeElement.expand();
222        }
223    },
224
225    includedInSearchResultsPropertyName: "__includedInInspectorSearchResults",
226
227    searchCanceled: function()
228    {
229        if (this._searchResults) {
230            const searchResultsProperty = this.includedInSearchResultsPropertyName;
231            for (var i = 0; i < this._searchResults.length; ++i) {
232                var node = this._searchResults[i];
233
234                // Remove the searchResultsProperty since there might be an unfinished search.
235                delete node[searchResultsProperty];
236
237                var treeElement = this.treeOutline.findTreeElement(node);
238                if (treeElement)
239                    treeElement.highlighted = false;
240            }
241        }
242
243        WebInspector.updateSearchMatchesCount(0, this);
244
245        if (this._currentSearchChunkIntervalIdentifier) {
246            clearInterval(this._currentSearchChunkIntervalIdentifier);
247            delete this._currentSearchChunkIntervalIdentifier;
248        }
249
250        this._currentSearchResultIndex = 0;
251        this._searchResults = [];
252    },
253
254    performSearch: function(query)
255    {
256        // Call searchCanceled since it will reset everything we need before doing a new search.
257        this.searchCanceled();
258
259        const whitespaceTrimmedQuery = query.trimWhitespace();
260        if (!whitespaceTrimmedQuery.length)
261            return;
262
263        var tagNameQuery = whitespaceTrimmedQuery;
264        var attributeNameQuery = whitespaceTrimmedQuery;
265        var startTagFound = (tagNameQuery.indexOf("<") === 0);
266        var endTagFound = (tagNameQuery.lastIndexOf(">") === (tagNameQuery.length - 1));
267
268        if (startTagFound || endTagFound) {
269            var tagNameQueryLength = tagNameQuery.length;
270            tagNameQuery = tagNameQuery.substring((startTagFound ? 1 : 0), (endTagFound ? (tagNameQueryLength - 1) : tagNameQueryLength));
271        }
272
273        // Check the tagNameQuery is it is a possibly valid tag name.
274        if (!/^[a-zA-Z0-9\-_:]+$/.test(tagNameQuery))
275            tagNameQuery = null;
276
277        // Check the attributeNameQuery is it is a possibly valid tag name.
278        if (!/^[a-zA-Z0-9\-_:]+$/.test(attributeNameQuery))
279            attributeNameQuery = null;
280
281        const escapedQuery = query.escapeCharacters("'");
282        const escapedTagNameQuery = (tagNameQuery ? tagNameQuery.escapeCharacters("'") : null);
283        const escapedWhitespaceTrimmedQuery = whitespaceTrimmedQuery.escapeCharacters("'");
284        const searchResultsProperty = this.includedInSearchResultsPropertyName;
285
286        var updatedMatchCountOnce = false;
287        var matchesCountUpdateTimeout = null;
288
289        function updateMatchesCount()
290        {
291            WebInspector.updateSearchMatchesCount(this._searchResults.length, this);
292            matchesCountUpdateTimeout = null;
293            updatedMatchCountOnce = true;
294        }
295
296        function updateMatchesCountSoon()
297        {
298            if (!updatedMatchCountOnce)
299                return updateMatchesCount.call(this);
300            if (matchesCountUpdateTimeout)
301                return;
302            // Update the matches count every half-second so it doesn't feel twitchy.
303            matchesCountUpdateTimeout = setTimeout(updateMatchesCount.bind(this), 500);
304        }
305
306        function addNodesToResults(nodes, length, getItem)
307        {
308            if (!length)
309                return;
310
311            for (var i = 0; i < length; ++i) {
312                var node = getItem.call(nodes, i);
313                // Skip this node if it already has the property.
314                if (searchResultsProperty in node)
315                    continue;
316
317                if (!this._searchResults.length) {
318                    this._currentSearchResultIndex = 0;
319                    this.focusedDOMNode = node;
320                }
321
322                node[searchResultsProperty] = true;
323                this._searchResults.push(node);
324
325                // Highlight the tree element to show it matched the search.
326                // FIXME: highlight the substrings in text nodes and attributes.
327                var treeElement = this.treeOutline.findTreeElement(node);
328                if (treeElement)
329                    treeElement.highlighted = true;
330            }
331
332            updateMatchesCountSoon.call(this);
333        }
334
335        function matchExactItems(doc)
336        {
337            matchExactId.call(this, doc);
338            matchExactClassNames.call(this, doc);
339            matchExactTagNames.call(this, doc);
340            matchExactAttributeNames.call(this, doc);
341        }
342
343        function matchExactId(doc)
344        {
345            const result = doc.__proto__.getElementById.call(doc, whitespaceTrimmedQuery);
346            addNodesToResults.call(this, result, (result ? 1 : 0), function() { return this });
347        }
348
349        function matchExactClassNames(doc)
350        {
351            const result = doc.__proto__.getElementsByClassName.call(doc, whitespaceTrimmedQuery);
352            addNodesToResults.call(this, result, result.length, result.item);
353        }
354
355        function matchExactTagNames(doc)
356        {
357            if (!tagNameQuery)
358                return;
359            const result = doc.__proto__.getElementsByTagName.call(doc, tagNameQuery);
360            addNodesToResults.call(this, result, result.length, result.item);
361        }
362
363        function matchExactAttributeNames(doc)
364        {
365            if (!attributeNameQuery)
366                return;
367            const result = doc.__proto__.querySelectorAll.call(doc, "[" + attributeNameQuery + "]");
368            addNodesToResults.call(this, result, result.length, result.item);
369        }
370
371        function matchPartialTagNames(doc)
372        {
373            if (!tagNameQuery)
374                return;
375            const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
376            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
377        }
378
379        function matchStartOfTagNames(doc)
380        {
381            if (!tagNameQuery)
382                return;
383            const result = doc.__proto__.evaluate.call(doc, "//*[starts-with(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
384            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
385        }
386
387        function matchPartialTagNamesAndAttributeValues(doc)
388        {
389            if (!tagNameQuery) {
390                matchPartialAttributeValues.call(this, doc);
391                return;
392            }
393
394            const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "') or contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
395            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
396        }
397
398        function matchPartialAttributeValues(doc)
399        {
400            const result = doc.__proto__.evaluate.call(doc, "//*[contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
401            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
402        }
403
404        function matchStyleSelector(doc)
405        {
406            const result = doc.__proto__.querySelectorAll.call(doc, whitespaceTrimmedQuery);
407            addNodesToResults.call(this, result, result.length, result.item);
408        }
409
410        function matchPlainText(doc)
411        {
412            const result = doc.__proto__.evaluate.call(doc, "//text()[contains(., '" + escapedQuery + "')] | //comment()[contains(., '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
413            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
414        }
415
416        function matchXPathQuery(doc)
417        {
418            const result = doc.__proto__.evaluate.call(doc, whitespaceTrimmedQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
419            addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem);
420        }
421
422        function finishedSearching()
423        {
424            // Remove the searchResultsProperty now that the search is finished.
425            for (var i = 0; i < this._searchResults.length; ++i)
426                delete this._searchResults[i][searchResultsProperty];
427        }
428
429        const mainFrameDocument = InspectorController.inspectedWindow().document;
430        const searchDocuments = [mainFrameDocument];
431
432        if (tagNameQuery && startTagFound && endTagFound)
433            const searchFunctions = [matchExactTagNames, matchPlainText];
434        else if (tagNameQuery && startTagFound)
435            const searchFunctions = [matchStartOfTagNames, matchPlainText];
436        else if (tagNameQuery && endTagFound) {
437            // FIXME: we should have a matchEndOfTagNames search function if endTagFound is true but not startTagFound.
438            // This requires ends-with() support in XPath, WebKit only supports starts-with() and contains().
439            const searchFunctions = [matchPartialTagNames, matchPlainText];
440        } else if (whitespaceTrimmedQuery === "//*" || whitespaceTrimmedQuery === "*") {
441            // These queries will match every node. Matching everything isn't useful and can be slow for large pages,
442            // so limit the search functions list to plain text and attribute matching.
443            const searchFunctions = [matchPartialAttributeValues, matchPlainText];
444        } else
445            const searchFunctions = [matchExactItems, matchStyleSelector, matchPartialTagNamesAndAttributeValues, matchPlainText, matchXPathQuery];
446
447        // Find all frames, iframes and object elements to search their documents.
448        const querySelectorAllFunction = InspectorController.inspectedWindow().Document.prototype.querySelectorAll;
449        const subdocumentResult = querySelectorAllFunction.call(mainFrameDocument, "iframe, frame, object");
450
451        for (var i = 0; i < subdocumentResult.length; ++i) {
452            var element = subdocumentResult.item(i);
453            if (element.contentDocument)
454                searchDocuments.push(element.contentDocument);
455        }
456
457        const panel = this;
458        var documentIndex = 0;
459        var searchFunctionIndex = 0;
460        var chunkIntervalIdentifier = null;
461
462        // Split up the work into chunks so we don't block the UI thread while processing.
463
464        function processChunk()
465        {
466            var searchDocument = searchDocuments[documentIndex];
467            var searchFunction = searchFunctions[searchFunctionIndex];
468
469            if (++searchFunctionIndex > searchFunctions.length) {
470                searchFunction = searchFunctions[0];
471                searchFunctionIndex = 0;
472
473                if (++documentIndex > searchDocuments.length) {
474                    if (panel._currentSearchChunkIntervalIdentifier === chunkIntervalIdentifier)
475                        delete panel._currentSearchChunkIntervalIdentifier;
476                    clearInterval(chunkIntervalIdentifier);
477                    finishedSearching.call(panel);
478                    return;
479                }
480
481                searchDocument = searchDocuments[documentIndex];
482            }
483
484            if (!searchDocument || !searchFunction)
485                return;
486
487            try {
488                searchFunction.call(panel, searchDocument);
489            } catch(err) {
490                // ignore any exceptions. the query might be malformed, but we allow that.
491            }
492        }
493
494        processChunk();
495
496        chunkIntervalIdentifier = setInterval(processChunk, 25);
497        this._currentSearchChunkIntervalIdentifier = chunkIntervalIdentifier;
498    },
499
500    jumpToNextSearchResult: function()
501    {
502        if (!this._searchResults || !this._searchResults.length)
503            return;
504        if (++this._currentSearchResultIndex >= this._searchResults.length)
505            this._currentSearchResultIndex = 0;
506        this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex];
507    },
508
509    jumpToPreviousSearchResult: function()
510    {
511        if (!this._searchResults || !this._searchResults.length)
512            return;
513        if (--this._currentSearchResultIndex < 0)
514            this._currentSearchResultIndex = (this._searchResults.length - 1);
515        this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex];
516    },
517
518    inspectedWindowCleared: function(window)
519    {
520        if (InspectorController.isWindowVisible())
521            this.updateMutationEventListeners(window);
522    },
523
524    renameSelector: function(oldIdentifier, newIdentifier, oldSelector, newSelector)
525    {
526        // TODO: Implement Shifting the oldSelector, and its contents to a newSelector
527    },
528
529    addStyleChange: function(identifier, style, property)
530    {
531        if (!style.parentRule)
532            return;
533
534        var selector = style.parentRule.selectorText;
535        if (!this.styles[identifier])
536            this.styles[identifier] = {};
537
538        if (!this.styles[identifier][selector])
539            this.styles[identifier][selector] = {};
540
541        if (!this.styles[identifier][selector][property])
542            WebInspector.styleChanges += 1;
543
544        this.styles[identifier][selector][property] = style.getPropertyValue(property);
545    },
546
547    removeStyleChange: function(identifier, style, property)
548    {
549        if (!style.parentRule)
550            return;
551
552        var selector = style.parentRule.selectorText;
553        if (!this.styles[identifier] || !this.styles[identifier][selector])
554            return;
555
556        if (this.styles[identifier][selector][property]) {
557            delete this.styles[identifier][selector][property];
558            WebInspector.styleChanges -= 1;
559        }
560    },
561
562    generateStylesheet: function()
563    {
564        if (!WebInspector.styleChanges)
565            return;
566
567        // Merge Down to Just Selectors
568        var mergedSelectors = {};
569        for (var identifier in this.styles) {
570            for (var selector in this.styles[identifier]) {
571                if (!mergedSelectors[selector])
572                    mergedSelectors[selector] = this.styles[identifier][selector];
573                else { // merge on selector
574                    var merge = {};
575                    for (var property in mergedSelectors[selector])
576                        merge[property] = mergedSelectors[selector][property];
577                    for (var property in this.styles[identifier][selector]) {
578                        if (!merge[property])
579                            merge[property] = this.styles[identifier][selector][property];
580                        else { // merge on property within a selector, include comment to notify user
581                            var value1 = merge[property];
582                            var value2 = this.styles[identifier][selector][property];
583
584                            if (value1 === value2)
585                                merge[property] = [value1];
586                            else if (Object.type(value1) === "array")
587                                merge[property].push(value2);
588                            else
589                                merge[property] = [value1, value2];
590                        }
591                    }
592                    mergedSelectors[selector] = merge;
593                }
594            }
595        }
596
597        var builder = [];
598        builder.push("/**");
599        builder.push(" * Inspector Generated Stylesheet"); // UIString?
600        builder.push(" */\n");
601
602        var indent = "  ";
603        function displayProperty(property, value, comment) {
604            if (comment)
605                return indent + "/* " + property + ": " + value + "; */";
606            else
607                return indent + property + ": " + value + ";";
608        }
609
610        for (var selector in mergedSelectors) {
611            var psuedoStyle = mergedSelectors[selector];
612            var properties = Object.properties(psuedoStyle);
613            if (properties.length) {
614                builder.push(selector + " {");
615                for (var i = 0; i < properties.length; ++i) {
616                    var property = properties[i];
617                    var value = psuedoStyle[property];
618                    if (Object.type(value) !== "array")
619                        builder.push(displayProperty(property, value));
620                    else {
621                        if (value.length === 1)
622                            builder.push(displayProperty(property, value) + " /* merged from equivalent edits */"); // UIString?
623                        else {
624                            builder.push(indent + "/* There was a Conflict... There were Multiple Edits for '" + property + "' */"); // UIString?
625                            for (var j = 0; j < value.length; ++j)
626                                builder.push(displayProperty(property, value, true));
627                        }
628                    }
629                }
630                builder.push("}\n");
631            }
632        }
633
634        WebInspector.showConsole();
635        var result = builder.join("\n");
636        InspectorController.inspectedWindow().console.log(result);
637    },
638
639    _addMutationEventListeners: function(monitoredWindow)
640    {
641        monitoredWindow.document.addEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true);
642        monitoredWindow.document.addEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true);
643        if (monitoredWindow.frameElement)
644            monitoredWindow.addEventListener("DOMContentLoaded", this._contentLoadedEventListener, true);
645    },
646
647    _removeMutationEventListeners: function(monitoredWindow)
648    {
649        if (monitoredWindow.frameElement)
650            monitoredWindow.removeEventListener("DOMContentLoaded", this._contentLoadedEventListener, true);
651        if (!monitoredWindow.document)
652            return;
653        monitoredWindow.document.removeEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true);
654        monitoredWindow.document.removeEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true);
655    },
656
657    updateMutationEventListeners: function(monitoredWindow)
658    {
659        this._addMutationEventListeners(monitoredWindow);
660    },
661
662    registerMutationEventListeners: function(monitoredWindow)
663    {
664        if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) !== -1)
665            return;
666        this._mutationMonitoredWindows.push(monitoredWindow);
667        if (InspectorController.isWindowVisible())
668            this._addMutationEventListeners(monitoredWindow);
669    },
670
671    unregisterMutationEventListeners: function(monitoredWindow)
672    {
673        if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) === -1)
674            return;
675        this._mutationMonitoredWindows.remove(monitoredWindow);
676        this._removeMutationEventListeners(monitoredWindow);
677    },
678
679    unregisterAllMutationEventListeners: function()
680    {
681        for (var i = 0; i < this._mutationMonitoredWindows.length; ++i)
682            this._removeMutationEventListeners(this._mutationMonitoredWindows[i]);
683        this._mutationMonitoredWindows = [];
684    },
685
686    get rootDOMNode()
687    {
688        return this.treeOutline.rootDOMNode;
689    },
690
691    set rootDOMNode(x)
692    {
693        this.treeOutline.rootDOMNode = x;
694    },
695
696    get focusedDOMNode()
697    {
698        return this.treeOutline.focusedDOMNode;
699    },
700
701    set focusedDOMNode(x)
702    {
703        this.treeOutline.focusedDOMNode = x;
704    },
705
706    _contentLoaded: function(event)
707    {
708        this.recentlyModifiedNodes.push({node: event.target, parent: event.target.defaultView.frameElement, replaced: true});
709        if (this.visible)
710            this._updateModifiedNodesSoon();
711    },
712
713    _nodeInserted: function(event)
714    {
715        this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, inserted: true});
716        if (this.visible)
717            this._updateModifiedNodesSoon();
718    },
719
720    _nodeRemoved: function(event)
721    {
722        this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, removed: true});
723        if (this.visible)
724            this._updateModifiedNodesSoon();
725    },
726
727    _updateModifiedNodesSoon: function()
728    {
729        if ("_updateModifiedNodesTimeout" in this)
730            return;
731        this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 0);
732    },
733
734    _updateModifiedNodes: function()
735    {
736        if ("_updateModifiedNodesTimeout" in this) {
737            clearTimeout(this._updateModifiedNodesTimeout);
738            delete this._updateModifiedNodesTimeout;
739        }
740
741        var updatedParentTreeElements = [];
742        var updateBreadcrumbs = false;
743
744        for (var i = 0; i < this.recentlyModifiedNodes.length; ++i) {
745            var replaced = this.recentlyModifiedNodes[i].replaced;
746            var parent = this.recentlyModifiedNodes[i].parent;
747            if (!parent)
748                continue;
749
750            var parentNodeItem = this.treeOutline.findTreeElement(parent, null, null, objectsAreSame);
751            if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
752                parentNodeItem.updateChildren(replaced);
753                parentNodeItem.alreadyUpdatedChildren = true;
754                updatedParentTreeElements.push(parentNodeItem);
755            }
756
757            if (!updateBreadcrumbs && (objectsAreSame(this.focusedDOMNode, parent) || isAncestorIncludingParentFrames(this.focusedDOMNode, parent)))
758                updateBreadcrumbs = true;
759        }
760
761        for (var i = 0; i < updatedParentTreeElements.length; ++i)
762            delete updatedParentTreeElements[i].alreadyUpdatedChildren;
763
764        this.recentlyModifiedNodes = [];
765
766        if (updateBreadcrumbs)
767            this.updateBreadcrumb(true);
768    },
769
770    _stylesPaneEdited: function()
771    {
772        this.sidebarPanes.metrics.needsUpdate = true;
773        this.updateMetrics();
774    },
775
776    _metricsPaneEdited: function()
777    {
778        this.sidebarPanes.styles.needsUpdate = true;
779        this.updateStyles(true);
780    },
781
782    _mouseMovedInCrumbs: function(event)
783    {
784        var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
785        var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
786
787        WebInspector.hoveredDOMNode = (crumbElement ? crumbElement.representedObject : null);
788
789        if ("_mouseOutOfCrumbsTimeout" in this) {
790            clearTimeout(this._mouseOutOfCrumbsTimeout);
791            delete this._mouseOutOfCrumbsTimeout;
792        }
793    },
794
795    _mouseMovedOutOfCrumbs: function(event)
796    {
797        var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
798        if (nodeUnderMouse.isDescendant(this.crumbsElement))
799            return;
800
801        WebInspector.hoveredDOMNode = null;
802
803        this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000);
804    },
805
806    updateBreadcrumb: function(forceUpdate)
807    {
808        if (!this.visible)
809            return;
810
811        var crumbs = this.crumbsElement;
812
813        var handled = false;
814        var foundRoot = false;
815        var crumb = crumbs.firstChild;
816        while (crumb) {
817            if (objectsAreSame(crumb.representedObject, this.rootDOMNode))
818                foundRoot = true;
819
820            if (foundRoot)
821                crumb.addStyleClass("dimmed");
822            else
823                crumb.removeStyleClass("dimmed");
824
825            if (objectsAreSame(crumb.representedObject, this.focusedDOMNode)) {
826                crumb.addStyleClass("selected");
827                handled = true;
828            } else {
829                crumb.removeStyleClass("selected");
830            }
831
832            crumb = crumb.nextSibling;
833        }
834
835        if (handled && !forceUpdate) {
836            // We don't need to rebuild the crumbs, but we need to adjust sizes
837            // to reflect the new focused or root node.
838            this.updateBreadcrumbSizes();
839            return;
840        }
841
842        crumbs.removeChildren();
843
844        var panel = this;
845
846        function selectCrumbFunction(event)
847        {
848            var crumb = event.currentTarget;
849            if (crumb.hasStyleClass("collapsed")) {
850                // Clicking a collapsed crumb will expose the hidden crumbs.
851                if (crumb === panel.crumbsElement.firstChild) {
852                    // If the focused crumb is the first child, pick the farthest crumb
853                    // that is still hidden. This allows the user to expose every crumb.
854                    var currentCrumb = crumb;
855                    while (currentCrumb) {
856                        var hidden = currentCrumb.hasStyleClass("hidden");
857                        var collapsed = currentCrumb.hasStyleClass("collapsed");
858                        if (!hidden && !collapsed)
859                            break;
860                        crumb = currentCrumb;
861                        currentCrumb = currentCrumb.nextSibling;
862                    }
863                }
864
865                panel.updateBreadcrumbSizes(crumb);
866            } else {
867                // Clicking a dimmed crumb or double clicking (event.detail >= 2)
868                // will change the root node in addition to the focused node.
869                if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
870                    panel.rootDOMNode = crumb.representedObject.parentNode;
871                panel.focusedDOMNode = crumb.representedObject;
872            }
873
874            event.preventDefault();
875        }
876
877        foundRoot = false;
878        for (var current = this.focusedDOMNode; current; current = parentNodeOrFrameElement(current)) {
879            if (current.nodeType === Node.DOCUMENT_NODE)
880                continue;
881
882            if (objectsAreSame(current, this.rootDOMNode))
883                foundRoot = true;
884
885            var crumb = document.createElement("span");
886            crumb.className = "crumb";
887            crumb.representedObject = current;
888            crumb.addEventListener("mousedown", selectCrumbFunction, false);
889
890            var crumbTitle;
891            switch (current.nodeType) {
892                case Node.ELEMENT_NODE:
893                    crumbTitle = current.nodeName.toLowerCase();
894
895                    var nameElement = document.createElement("span");
896                    nameElement.textContent = crumbTitle;
897                    crumb.appendChild(nameElement);
898
899                    var idAttribute = current.getAttribute("id");
900                    if (idAttribute) {
901                        var idElement = document.createElement("span");
902                        crumb.appendChild(idElement);
903
904                        var part = "#" + idAttribute;
905                        crumbTitle += part;
906                        idElement.appendChild(document.createTextNode(part));
907
908                        // Mark the name as extra, since the ID is more important.
909                        nameElement.className = "extra";
910                    }
911
912                    var classAttribute = current.getAttribute("class");
913                    if (classAttribute) {
914                        var classes = classAttribute.split(/\s+/);
915                        var foundClasses = {};
916
917                        if (classes.length) {
918                            var classesElement = document.createElement("span");
919                            classesElement.className = "extra";
920                            crumb.appendChild(classesElement);
921
922                            for (var i = 0; i < classes.length; ++i) {
923                                var className = classes[i];
924                                if (className && !(className in foundClasses)) {
925                                    var part = "." + className;
926                                    crumbTitle += part;
927                                    classesElement.appendChild(document.createTextNode(part));
928                                    foundClasses[className] = true;
929                                }
930                            }
931                        }
932                    }
933
934                    break;
935
936                case Node.TEXT_NODE:
937                    if (isNodeWhitespace.call(current))
938                        crumbTitle = WebInspector.UIString("(whitespace)");
939                    else
940                        crumbTitle = WebInspector.UIString("(text)");
941                    break
942
943                case Node.COMMENT_NODE:
944                    crumbTitle = "<!-->";
945                    break;
946
947                case Node.DOCUMENT_TYPE_NODE:
948                    crumbTitle = "<!DOCTYPE>";
949                    break;
950
951                default:
952                    crumbTitle = current.nodeName.toLowerCase();
953            }
954
955            if (!crumb.childNodes.length) {
956                var nameElement = document.createElement("span");
957                nameElement.textContent = crumbTitle;
958                crumb.appendChild(nameElement);
959            }
960
961            crumb.title = crumbTitle;
962
963            if (foundRoot)
964                crumb.addStyleClass("dimmed");
965            if (objectsAreSame(current, this.focusedDOMNode))
966                crumb.addStyleClass("selected");
967            if (!crumbs.childNodes.length)
968                crumb.addStyleClass("end");
969
970            crumbs.appendChild(crumb);
971        }
972
973        if (crumbs.hasChildNodes())
974            crumbs.lastChild.addStyleClass("start");
975
976        this.updateBreadcrumbSizes();
977    },
978
979    updateBreadcrumbSizes: function(focusedCrumb)
980    {
981        if (!this.visible)
982            return;
983
984        if (document.body.offsetWidth <= 0) {
985            // The stylesheet hasn't loaded yet or the window is closed,
986            // so we can't calculate what is need. Return early.
987            return;
988        }
989
990        var crumbs = this.crumbsElement;
991        if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0)
992            return; // No crumbs, do nothing.
993
994        // A Zero index is the right most child crumb in the breadcrumb.
995        var selectedIndex = 0;
996        var focusedIndex = 0;
997        var selectedCrumb;
998
999        var i = 0;
1000        var crumb = crumbs.firstChild;
1001        while (crumb) {
1002            // Find the selected crumb and index.
1003            if (!selectedCrumb && crumb.hasStyleClass("selected")) {
1004                selectedCrumb = crumb;
1005                selectedIndex = i;
1006            }
1007
1008            // Find the focused crumb index.
1009            if (crumb === focusedCrumb)
1010                focusedIndex = i;
1011
1012            // Remove any styles that affect size before
1013            // deciding to shorten any crumbs.
1014            if (crumb !== crumbs.lastChild)
1015                crumb.removeStyleClass("start");
1016            if (crumb !== crumbs.firstChild)
1017                crumb.removeStyleClass("end");
1018
1019            crumb.removeStyleClass("compact");
1020            crumb.removeStyleClass("collapsed");
1021            crumb.removeStyleClass("hidden");
1022
1023            crumb = crumb.nextSibling;
1024            ++i;
1025        }
1026
1027        // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
1028        // The order of the crumbs in the document is opposite of the visual order.
1029        crumbs.firstChild.addStyleClass("end");
1030        crumbs.lastChild.addStyleClass("start");
1031
1032        function crumbsAreSmallerThanContainer()
1033        {
1034            var rightPadding = 20;
1035            var errorWarningElement = document.getElementById("error-warning-count");
1036            if (!WebInspector.drawer.visible && errorWarningElement)
1037                rightPadding += errorWarningElement.offsetWidth;
1038            return ((crumbs.totalOffsetLeft + crumbs.offsetWidth + rightPadding) < window.innerWidth);
1039        }
1040
1041        if (crumbsAreSmallerThanContainer())
1042            return; // No need to compact the crumbs, they all fit at full size.
1043
1044        var BothSides = 0;
1045        var AncestorSide = -1;
1046        var ChildSide = 1;
1047
1048        function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
1049        {
1050            if (!significantCrumb)
1051                significantCrumb = (focusedCrumb || selectedCrumb);
1052
1053            if (significantCrumb === selectedCrumb)
1054                var significantIndex = selectedIndex;
1055            else if (significantCrumb === focusedCrumb)
1056                var significantIndex = focusedIndex;
1057            else {
1058                var significantIndex = 0;
1059                for (var i = 0; i < crumbs.childNodes.length; ++i) {
1060                    if (crumbs.childNodes[i] === significantCrumb) {
1061                        significantIndex = i;
1062                        break;
1063                    }
1064                }
1065            }
1066
1067            function shrinkCrumbAtIndex(index)
1068            {
1069                var shrinkCrumb = crumbs.childNodes[index];
1070                if (shrinkCrumb && shrinkCrumb !== significantCrumb)
1071                    shrinkingFunction(shrinkCrumb);
1072                if (crumbsAreSmallerThanContainer())
1073                    return true; // No need to compact the crumbs more.
1074                return false;
1075            }
1076
1077            // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
1078            // fit in the container or we run out of crumbs to shrink.
1079            if (direction) {
1080                // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
1081                var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
1082                while (index !== significantIndex) {
1083                    if (shrinkCrumbAtIndex(index))
1084                        return true;
1085                    index += (direction > 0 ? 1 : -1);
1086                }
1087            } else {
1088                // Crumbs are shrunk in order of descending distance from the signifcant crumb,
1089                // with a tie going to child crumbs.
1090                var startIndex = 0;
1091                var endIndex = crumbs.childNodes.length - 1;
1092                while (startIndex != significantIndex || endIndex != significantIndex) {
1093                    var startDistance = significantIndex - startIndex;
1094                    var endDistance = endIndex - significantIndex;
1095                    if (startDistance >= endDistance)
1096                        var index = startIndex++;
1097                    else
1098                        var index = endIndex--;
1099                    if (shrinkCrumbAtIndex(index))
1100                        return true;
1101                }
1102            }
1103
1104            // We are not small enough yet, return false so the caller knows.
1105            return false;
1106        }
1107
1108        function coalesceCollapsedCrumbs()
1109        {
1110            var crumb = crumbs.firstChild;
1111            var collapsedRun = false;
1112            var newStartNeeded = false;
1113            var newEndNeeded = false;
1114            while (crumb) {
1115                var hidden = crumb.hasStyleClass("hidden");
1116                if (!hidden) {
1117                    var collapsed = crumb.hasStyleClass("collapsed");
1118                    if (collapsedRun && collapsed) {
1119                        crumb.addStyleClass("hidden");
1120                        crumb.removeStyleClass("compact");
1121                        crumb.removeStyleClass("collapsed");
1122
1123                        if (crumb.hasStyleClass("start")) {
1124                            crumb.removeStyleClass("start");
1125                            newStartNeeded = true;
1126                        }
1127
1128                        if (crumb.hasStyleClass("end")) {
1129                            crumb.removeStyleClass("end");
1130                            newEndNeeded = true;
1131                        }
1132
1133                        continue;
1134                    }
1135
1136                    collapsedRun = collapsed;
1137
1138                    if (newEndNeeded) {
1139                        newEndNeeded = false;
1140                        crumb.addStyleClass("end");
1141                    }
1142                } else
1143                    collapsedRun = true;
1144                crumb = crumb.nextSibling;
1145            }
1146
1147            if (newStartNeeded) {
1148                crumb = crumbs.lastChild;
1149                while (crumb) {
1150                    if (!crumb.hasStyleClass("hidden")) {
1151                        crumb.addStyleClass("start");
1152                        break;
1153                    }
1154                    crumb = crumb.previousSibling;
1155                }
1156            }
1157        }
1158
1159        function compact(crumb)
1160        {
1161            if (crumb.hasStyleClass("hidden"))
1162                return;
1163            crumb.addStyleClass("compact");
1164        }
1165
1166        function collapse(crumb, dontCoalesce)
1167        {
1168            if (crumb.hasStyleClass("hidden"))
1169                return;
1170            crumb.addStyleClass("collapsed");
1171            crumb.removeStyleClass("compact");
1172            if (!dontCoalesce)
1173                coalesceCollapsedCrumbs();
1174        }
1175
1176        function compactDimmed(crumb)
1177        {
1178            if (crumb.hasStyleClass("dimmed"))
1179                compact(crumb);
1180        }
1181
1182        function collapseDimmed(crumb)
1183        {
1184            if (crumb.hasStyleClass("dimmed"))
1185                collapse(crumb);
1186        }
1187
1188        if (!focusedCrumb) {
1189            // When not focused on a crumb we can be biased and collapse less important
1190            // crumbs that the user might not care much about.
1191
1192            // Compact child crumbs.
1193            if (makeCrumbsSmaller(compact, ChildSide))
1194                return;
1195
1196            // Collapse child crumbs.
1197            if (makeCrumbsSmaller(collapse, ChildSide))
1198                return;
1199
1200            // Compact dimmed ancestor crumbs.
1201            if (makeCrumbsSmaller(compactDimmed, AncestorSide))
1202                return;
1203
1204            // Collapse dimmed ancestor crumbs.
1205            if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
1206                return;
1207        }
1208
1209        // Compact ancestor crumbs, or from both sides if focused.
1210        if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
1211            return;
1212
1213        // Collapse ancestor crumbs, or from both sides if focused.
1214        if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
1215            return;
1216
1217        if (!selectedCrumb)
1218            return;
1219
1220        // Compact the selected crumb.
1221        compact(selectedCrumb);
1222        if (crumbsAreSmallerThanContainer())
1223            return;
1224
1225        // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
1226        collapse(selectedCrumb, true);
1227    },
1228
1229    updateStyles: function(forceUpdate)
1230    {
1231        var stylesSidebarPane = this.sidebarPanes.styles;
1232        if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
1233            return;
1234
1235        stylesSidebarPane.update(this.focusedDOMNode, null, forceUpdate);
1236        stylesSidebarPane.needsUpdate = false;
1237    },
1238
1239    updateMetrics: function()
1240    {
1241        var metricsSidebarPane = this.sidebarPanes.metrics;
1242        if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
1243            return;
1244
1245        metricsSidebarPane.update(this.focusedDOMNode);
1246        metricsSidebarPane.needsUpdate = false;
1247    },
1248
1249    updateProperties: function()
1250    {
1251        var propertiesSidebarPane = this.sidebarPanes.properties;
1252        if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
1253            return;
1254
1255        propertiesSidebarPane.update(this.focusedDOMNode);
1256        propertiesSidebarPane.needsUpdate = false;
1257    },
1258
1259    handleKeyEvent: function(event)
1260    {
1261        this.treeOutline.handleKeyEvent(event);
1262    },
1263
1264    handleCopyEvent: function(event)
1265    {
1266        // Don't prevent the normal copy if the user has a selection.
1267        if (!window.getSelection().isCollapsed)
1268            return;
1269
1270        switch (this.focusedDOMNode.nodeType) {
1271            case Node.ELEMENT_NODE:
1272                var data = this.focusedDOMNode.outerHTML;
1273                break;
1274
1275            case Node.COMMENT_NODE:
1276                var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
1277                break;
1278
1279            default:
1280            case Node.TEXT_NODE:
1281                var data = this.focusedDOMNode.nodeValue;
1282        }
1283
1284        event.clipboardData.clearData();
1285        event.preventDefault();
1286
1287        if (data)
1288            event.clipboardData.setData("text/plain", data);
1289    },
1290
1291    rightSidebarResizerDragStart: function(event)
1292    {
1293        WebInspector.elementDragStart(this.sidebarElement, this.rightSidebarResizerDrag.bind(this), this.rightSidebarResizerDragEnd.bind(this), event, "col-resize");
1294    },
1295
1296    rightSidebarResizerDragEnd: function(event)
1297    {
1298        WebInspector.elementDragEnd(event);
1299    },
1300
1301    rightSidebarResizerDrag: function(event)
1302    {
1303        var x = event.pageX;
1304        var newWidth = Number.constrain(window.innerWidth - x, Preferences.minElementsSidebarWidth, window.innerWidth * 0.66);
1305
1306        this.sidebarElement.style.width = newWidth + "px";
1307        this.contentElement.style.right = newWidth + "px";
1308        this.sidebarResizeElement.style.right = (newWidth - 3) + "px";
1309
1310        this.treeOutline.updateSelection();
1311
1312        event.preventDefault();
1313    },
1314
1315    _nodeSearchButtonClicked: function(event)
1316    {
1317        InspectorController.toggleNodeSearch();
1318
1319        if (InspectorController.searchingForNode())
1320            this.nodeSearchButton.addStyleClass("toggled-on");
1321        else
1322            this.nodeSearchButton.removeStyleClass("toggled-on");
1323    }
1324}
1325
1326WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype;
1327