2 * Copyright (C) 2007 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 WebInspector.DocumentPanel = function(resource, views)
31 var allViews = [{ title: WebInspector.UIString("DOM"), name: "dom" }];
33 allViews = allViews.concat(views);
35 WebInspector.SourcePanel.call(this, resource, allViews);
38 var domView = this.views.dom;
39 domView.hide = function() { InspectorController.hideDOMNodeHighlight() };
40 domView.show = function() {
41 InspectorController.highlightDOMNode(panel.focusedDOMNode);
42 panel.updateBreadcrumb();
43 panel.updateTreeSelection();
46 domView.sideContentElement = document.createElement("div");
47 domView.sideContentElement.className = "content side";
49 domView.treeContentElement = document.createElement("div");
50 domView.treeContentElement.className = "content tree outline-disclosure";
52 domView.treeListElement = document.createElement("ol");
53 domView.treeOutline = new TreeOutline(domView.treeListElement);
54 domView.treeOutline.panel = this;
56 domView.crumbsElement = document.createElement("div");
57 domView.crumbsElement.className = "crumbs";
59 domView.innerCrumbsElement = document.createElement("div");
60 domView.crumbsElement.appendChild(domView.innerCrumbsElement);
62 domView.sidebarPanes = {};
63 domView.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
64 domView.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
65 domView.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
67 domView.sidebarPanes.styles.onexpand = function() { panel.updateStyles() };
68 domView.sidebarPanes.metrics.onexpand = function() { panel.updateMetrics() };
69 domView.sidebarPanes.properties.onexpand = function() { panel.updateProperties() };
71 domView.sidebarPanes.styles.expanded = true;
73 domView.sidebarElement = document.createElement("div");
74 domView.sidebarElement.className = "sidebar";
76 domView.sidebarElement.appendChild(domView.sidebarPanes.styles.element);
77 domView.sidebarElement.appendChild(domView.sidebarPanes.metrics.element);
78 domView.sidebarElement.appendChild(domView.sidebarPanes.properties.element);
80 domView.sideContentElement.appendChild(domView.treeContentElement);
81 domView.sideContentElement.appendChild(domView.crumbsElement);
82 domView.treeContentElement.appendChild(domView.treeListElement);
84 domView.sidebarResizeElement = document.createElement("div");
85 domView.sidebarResizeElement.className = "sidebar-resizer-vertical sidebar-resizer-vertical-right";
86 domView.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
88 domView.contentElement.appendChild(domView.sideContentElement);
89 domView.contentElement.appendChild(domView.sidebarElement);
90 domView.contentElement.appendChild(domView.sidebarResizeElement);
92 this.rootDOMNode = this.resource.documentNode;
95 WebInspector.DocumentPanel.prototype = {
98 this.updateTreeSelection();
99 this.updateBreadcrumbSizes();
102 updateTreeSelection: function()
104 if (!this.views.dom.treeOutline || !this.views.dom.treeOutline.selectedTreeElement)
106 var element = this.views.dom.treeOutline.selectedTreeElement;
107 element.updateSelection();
112 return this._rootDOMNode;
117 if (this._rootDOMNode === x)
120 this._rootDOMNode = x;
122 this.updateBreadcrumb();
123 this.updateTreeOutline();
128 return this._focusedDOMNode;
131 set focusedDOMNode(x)
133 if (this._focusedDOMNode === x) {
134 var nodeItem = this.revealNode(x);
140 this._focusedDOMNode = x;
142 this._focusedNodeChanged();
144 InspectorController.highlightDOMNode(x);
146 var nodeItem = this.revealNode(x);
151 _focusedNodeChanged: function(forceUpdate)
153 this.updateBreadcrumb(forceUpdate);
155 for (var pane in this.views.dom.sidebarPanes)
156 this.views.dom.sidebarPanes[pane].needsUpdate = true;
158 this.updateStyles(forceUpdate);
159 this.updateMetrics();
160 this.updateProperties();
163 revealNode: function(node)
165 var nodeItem = this.views.dom.treeOutline.findTreeElement(node, this._isAncestorIncludingParentFramesWithinPanel.bind(this), this._parentNodeOrFrameElementWithinPanel.bind(this));
173 updateTreeOutline: function()
175 this.views.dom.treeOutline.removeChildrenRecursive();
177 if (!this.rootDOMNode)
180 // FIXME: this could use findTreeElement to reuse a tree element if it already exists
181 var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild);
183 this.views.dom.treeOutline.appendChild(new WebInspector.DOMNodeTreeElement(node));
184 node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
187 this.updateTreeSelection();
190 updateBreadcrumb: function(forceUpdate)
195 var crumbs = this.views.dom.innerCrumbsElement;
198 var foundRoot = false;
199 var crumb = crumbs.firstChild;
201 if (crumb.representedObject === this.rootDOMNode)
205 crumb.addStyleClass("dimmed");
207 crumb.removeStyleClass("dimmed");
209 if (crumb.representedObject === this.focusedDOMNode) {
210 crumb.addStyleClass("selected");
213 crumb.removeStyleClass("selected");
216 crumb = crumb.nextSibling;
219 if (handled && !forceUpdate) {
220 // We don't need to rebuild the crumbs, but we need to adjust sizes
221 // to reflect the new focused or root node.
222 this.updateBreadcrumbSizes();
226 crumbs.removeChildren();
229 var selectCrumbFunction = function(event) {
230 var crumb = event.currentTarget;
231 if (crumb.hasStyleClass("collapsed")) {
232 // Clicking a collapsed crumb will expose the hidden crumbs.
233 if (crumb === panel.views.dom.innerCrumbsElement.firstChild) {
234 // If the focused crumb is the first child, pick the farthest crumb
235 // that is still hidden. This allows the user to expose every crumb.
236 var currentCrumb = crumb;
237 while (currentCrumb) {
238 var hidden = currentCrumb.hasStyleClass("hidden");
239 var collapsed = currentCrumb.hasStyleClass("collapsed");
240 if (!hidden && !collapsed)
242 crumb = currentCrumb;
243 currentCrumb = currentCrumb.nextSibling;
247 panel.updateBreadcrumbSizes(crumb);
249 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
250 // will change the root node in addition to the focused node.
251 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
252 panel.rootDOMNode = crumb.representedObject.parentNode;
253 panel.focusedDOMNode = crumb.representedObject;
256 event.preventDefault();
259 var mouseOverCrumbFunction = function(event) {
260 panel.mouseOverCrumb = true;
262 if ("mouseOutTimeout" in panel) {
263 clearTimeout(panel.mouseOutTimeout);
264 delete panel.mouseOutTimeout;
268 var mouseOutCrumbFunction = function(event) {
269 delete panel.mouseOverCrumb;
271 if ("mouseOutTimeout" in panel) {
272 clearTimeout(panel.mouseOutTimeout);
273 delete panel.mouseOutTimeout;
276 var timeoutFunction = function() {
277 if (!panel.mouseOverCrumb)
278 panel.updateBreadcrumbSizes();
281 panel.mouseOutTimeout = setTimeout(timeoutFunction, 500);
285 for (var current = this.focusedDOMNode; current; current = this._parentNodeOrFrameElementWithinPanel(current)) {
286 if (current === this.resource.documentNode)
289 if (current.nodeType === Node.DOCUMENT_NODE)
292 if (current === this.rootDOMNode)
295 var crumb = document.createElement("span");
296 crumb.className = "crumb";
297 crumb.representedObject = current;
298 crumb.addEventListener("mousedown", selectCrumbFunction, false);
299 crumb.addEventListener("mouseover", mouseOverCrumbFunction, false);
300 crumb.addEventListener("mouseout", mouseOutCrumbFunction, false);
303 switch (current.nodeType) {
304 case Node.ELEMENT_NODE:
305 crumbTitle = current.nodeName.toLowerCase();
307 var nameElement = document.createElement("span");
308 nameElement.textContent = crumbTitle;
309 crumb.appendChild(nameElement);
311 var idAttribute = current.getAttribute("id");
313 var idElement = document.createElement("span");
314 crumb.appendChild(idElement);
316 var part = "#" + idAttribute;
318 idElement.appendChild(document.createTextNode(part));
320 // Mark the name as extra, since the ID is more important.
321 nameElement.className = "extra";
324 var classAttribute = current.getAttribute("class");
325 if (classAttribute) {
326 var classes = classAttribute.split(/\s+/);
327 var foundClasses = {};
329 if (classes.length) {
330 var classesElement = document.createElement("span");
331 classesElement.className = "extra";
332 crumb.appendChild(classesElement);
334 for (var i = 0; i < classes.length; ++i) {
335 var className = classes[i];
336 if (className && !(className in foundClasses)) {
337 var part = "." + className;
339 classesElement.appendChild(document.createTextNode(part));
340 foundClasses[className] = true;
349 if (isNodeWhitespace.call(current))
350 crumbTitle = WebInspector.UIString("(whitespace)");
352 crumbTitle = WebInspector.UIString("(text)");
355 case Node.COMMENT_NODE:
356 crumbTitle = "<!-->";
360 crumbTitle = current.nodeName.toLowerCase();
363 if (!crumb.childNodes.length) {
364 var nameElement = document.createElement("span");
365 nameElement.textContent = crumbTitle;
366 crumb.appendChild(nameElement);
369 crumb.title = crumbTitle;
372 crumb.addStyleClass("dimmed");
373 if (current === this.focusedDOMNode)
374 crumb.addStyleClass("selected");
375 if (!crumbs.childNodes.length)
376 crumb.addStyleClass("end");
378 crumbs.appendChild(crumb);
381 if (crumbs.hasChildNodes())
382 crumbs.lastChild.addStyleClass("start");
384 this.updateBreadcrumbSizes();
387 updateBreadcrumbSizes: function(focusedCrumb)
392 if (document.body.offsetWidth <= 0) {
393 // The stylesheet hasn't loaded yet, so we need to update later.
394 setTimeout(this.updateBreadcrumbSizes.bind(this), 0);
398 var crumbs = this.views.dom.innerCrumbsElement;
399 if (!crumbs.childNodes.length)
400 return; // No crumbs, do nothing.
402 var crumbsContainer = this.views.dom.crumbsElement;
403 if (crumbsContainer.offsetWidth <= 0 || crumbs.offsetWidth <= 0)
406 // A Zero index is the right most child crumb in the breadcrumb.
407 var selectedIndex = 0;
408 var focusedIndex = 0;
412 var crumb = crumbs.firstChild;
414 // Find the selected crumb and index.
415 if (!selectedCrumb && crumb.hasStyleClass("selected")) {
416 selectedCrumb = crumb;
420 // Find the focused crumb index.
421 if (crumb === focusedCrumb)
424 // Remove any styles that affect size before
425 // deciding to shorten any crumbs.
426 if (crumb !== crumbs.lastChild)
427 crumb.removeStyleClass("start");
428 if (crumb !== crumbs.firstChild)
429 crumb.removeStyleClass("end");
431 crumb.removeStyleClass("compact");
432 crumb.removeStyleClass("collapsed");
433 crumb.removeStyleClass("hidden");
435 crumb = crumb.nextSibling;
439 // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
440 // The order of the crumbs in the document is opposite of the visual order.
441 crumbs.firstChild.addStyleClass("end");
442 crumbs.lastChild.addStyleClass("start");
444 function crumbsAreSmallerThanContainer()
446 // There is some fixed extra space that is not returned in the crumbs' offsetWidth.
447 // This padding is added to the crumbs' offsetWidth when comparing to the crumbsContainer.
448 var rightPadding = 9;
449 return ((crumbs.offsetWidth + rightPadding) < crumbsContainer.offsetWidth);
452 if (crumbsAreSmallerThanContainer())
453 return; // No need to compact the crumbs, they all fit at full size.
456 var AncestorSide = -1;
459 function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
461 if (!significantCrumb)
462 significantCrumb = (focusedCrumb || selectedCrumb);
464 if (significantCrumb === selectedCrumb)
465 var significantIndex = selectedIndex;
466 else if (significantCrumb === focusedCrumb)
467 var significantIndex = focusedIndex;
469 var significantIndex = 0;
470 for (var i = 0; i < crumbs.childNodes.length; ++i) {
471 if (crumbs.childNodes[i] === significantCrumb) {
472 significantIndex = i;
478 function shrinkCrumbAtIndex(index)
480 var shrinkCrumb = crumbs.childNodes[index];
481 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
482 shrinkingFunction(shrinkCrumb);
483 if (crumbsAreSmallerThanContainer())
484 return true; // No need to compact the crumbs more.
488 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
489 // fit in the crumbsContainer or we run out of crumbs to shrink.
491 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
492 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
493 while (index !== significantIndex) {
494 if (shrinkCrumbAtIndex(index))
496 index += (direction > 0 ? 1 : -1);
499 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
500 // with a tie going to child crumbs.
502 var endIndex = crumbs.childNodes.length - 1;
503 while (startIndex != significantIndex || endIndex != significantIndex) {
504 var startDistance = significantIndex - startIndex;
505 var endDistance = endIndex - significantIndex;
506 if (startDistance >= endDistance)
507 var index = startIndex++;
509 var index = endIndex--;
510 if (shrinkCrumbAtIndex(index))
515 // We are not small enough yet, return false so the caller knows.
519 function coalesceCollapsedCrumbs()
521 var crumb = crumbs.firstChild;
522 var collapsedRun = false;
523 var newStartNeeded = false;
524 var newEndNeeded = false;
526 var hidden = crumb.hasStyleClass("hidden");
528 var collapsed = crumb.hasStyleClass("collapsed");
529 if (collapsedRun && collapsed) {
530 crumb.addStyleClass("hidden");
531 crumb.removeStyleClass("compact");
532 crumb.removeStyleClass("collapsed");
534 if (crumb.hasStyleClass("start")) {
535 crumb.removeStyleClass("start");
536 newStartNeeded = true;
539 if (crumb.hasStyleClass("end")) {
540 crumb.removeStyleClass("end");
547 collapsedRun = collapsed;
550 newEndNeeded = false;
551 crumb.addStyleClass("end");
555 crumb = crumb.nextSibling;
558 if (newStartNeeded) {
559 crumb = crumbs.lastChild;
561 if (!crumb.hasStyleClass("hidden")) {
562 crumb.addStyleClass("start");
565 crumb = crumb.previousSibling;
570 function compact(crumb)
572 if (crumb.hasStyleClass("hidden"))
574 crumb.addStyleClass("compact");
577 function collapse(crumb, dontCoalesce)
579 if (crumb.hasStyleClass("hidden"))
581 crumb.addStyleClass("collapsed");
582 crumb.removeStyleClass("compact");
584 coalesceCollapsedCrumbs();
587 function compactDimmed(crumb)
589 if (crumb.hasStyleClass("dimmed"))
593 function collapseDimmed(crumb)
595 if (crumb.hasStyleClass("dimmed"))
600 // When not focused on a crumb we can be biased and collapse less important
601 // crumbs that the user might not care much about.
603 // Compact child crumbs.
604 if (makeCrumbsSmaller(compact, ChildSide))
607 // Collapse child crumbs.
608 if (makeCrumbsSmaller(collapse, ChildSide))
611 // Compact dimmed ancestor crumbs.
612 if (makeCrumbsSmaller(compactDimmed, AncestorSide))
615 // Collapse dimmed ancestor crumbs.
616 if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
620 // Compact ancestor crumbs, or from both sides if focused.
621 if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
624 // Collapse ancestor crumbs, or from both sides if focused.
625 if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
631 // Compact the selected crumb.
632 compact(selectedCrumb);
633 if (crumbsAreSmallerThanContainer())
636 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
637 collapse(selectedCrumb, true);
640 updateStyles: function(forceUpdate)
642 var stylesSidebarPane = this.views.dom.sidebarPanes.styles;
643 if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
646 stylesSidebarPane.update(this.focusedDOMNode, undefined, forceUpdate);
647 stylesSidebarPane.needsUpdate = false;
650 updateMetrics: function()
652 var metricsSidebarPane = this.views.dom.sidebarPanes.metrics;
653 if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
656 metricsSidebarPane.update(this.focusedDOMNode);
657 metricsSidebarPane.needsUpdate = false;
660 updateProperties: function()
662 var propertiesSidebarPane = this.views.dom.sidebarPanes.properties;
663 if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
666 propertiesSidebarPane.update(this.focusedDOMNode);
667 propertiesSidebarPane.needsUpdate = false;
670 handleKeyEvent: function(event)
672 if (this.views.dom.treeOutline && this.currentView && this.currentView === this.views.dom)
673 this.views.dom.treeOutline.handleKeyEvent(event);
676 handleCopyEvent: function(event)
678 if (this.currentView !== this.views.dom)
681 // Don't prevent the normal copy if the user has a selection.
682 if (!window.getSelection().isCollapsed)
685 switch (this.focusedDOMNode.nodeType) {
686 case Node.ELEMENT_NODE:
687 var data = this.focusedDOMNode.outerHTML;
690 case Node.COMMENT_NODE:
691 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
696 var data = this.focusedDOMNode.nodeValue;
699 event.clipboardData.clearData();
700 event.preventDefault();
703 event.clipboardData.setData("text/plain", data);
706 rightSidebarResizerDragStart: function(event)
708 if (this.sidebarDragEventListener || this.sidebarDragEndEventListener)
709 return this.rightSidebarResizerDragEnd(event);
711 this.sidebarDragEventListener = this.rightSidebarResizerDrag.bind(this);
712 this.sidebarDragEndEventListener = this.rightSidebarResizerDragEnd.bind(this);
713 WebInspector.elementDragStart(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event, "col-resize");
716 rightSidebarResizerDragEnd: function(event)
718 WebInspector.elementDragEnd(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event);
719 delete this.sidebarDragEventListener;
720 delete this.sidebarDragEndEventListener;
723 rightSidebarResizerDrag: function(event)
727 var leftSidebarWidth = document.getElementById("sidebar").offsetWidth;
728 var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
730 this.views.dom.sidebarElement.style.width = newWidth + "px";
731 this.views.dom.sideContentElement.style.right = newWidth + "px";
732 this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
734 this.updateTreeSelection();
735 this.updateBreadcrumbSizes();
737 event.preventDefault();
740 _getDocumentForNode: function(node)
742 return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
745 _parentNodeOrFrameElementWithinPanel: function(node)
747 var parent = node.parentNode;
751 var document = this._getDocumentForNode(node);
753 if (document === this.resource.documentNode)
756 return document.defaultView.frameElement;
759 _isAncestorIncludingParentFramesWithinPanel: function(a, b)
761 for (var node = b; node; node = this._getDocumentForNode(node).defaultView.frameElement) {
762 if (isAncestorNode.call(a, node))
765 if (this._getDocumentForNode(node) === this.resource.documentNode) {
766 // We've gone as high in the frame hierarchy as we can without
767 // moving out of this DocumentPanel.
776 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
778 WebInspector.DOMNodeTreeElement = function(node)
780 var hasChildren = node.contentDocument || (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
781 var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
783 if (titleInfo.hasChildren)
784 this.whitespaceIgnored = Preferences.ignoreWhitespace;
786 TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
789 WebInspector.DOMNodeTreeElement.prototype = {
790 updateSelection: function()
792 var listItemElement = this.listItemElement;
793 if (!listItemElement)
796 if (document.body.offsetWidth <= 0) {
797 // The stylesheet hasn't loaded yet, so we need to update later.
798 setTimeout(this.updateSelection.bind(this), 0);
802 if (!this.selectionElement) {
803 this.selectionElement = document.createElement("div");
804 this.selectionElement.className = "selection selected";
805 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
808 this.selectionElement.style.height = listItemElement.offsetHeight + "px";
813 this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false);
815 this._makeURLsActivateOnModifiedClick();
818 _makeURLsActivateOnModifiedClick: function()
820 var links = this.listItemElement.querySelectorAll("li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link");
824 var isMac = InspectorController.platform().indexOf("mac") == 0;
826 for (var i = 0; i < links.length; ++i) {
828 var isExternal = link.hasStyleClass("webkit-html-external-link");
829 var href = link.getAttribute("href");
833 title = WebInspector.UIString("Option-click to visit %s.", href);
835 title = WebInspector.UIString("Option-click to show %s.", href);
838 title = WebInspector.UIString("Alt-click to visit %s.", href);
840 title = WebInspector.UIString("Alt-click to show %s.", href);
842 link.setAttribute("title", title);
843 link.followOnAltClick = true;
847 onpopulate: function()
849 if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
852 this.removeChildren();
853 this.whitespaceIgnored = Preferences.ignoreWhitespace;
855 var treeElement = this;
856 function appendChildrenOfNode(node)
858 var child = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(node) : node.firstChild);
860 treeElement.appendChild(new WebInspector.DOMNodeTreeElement(child));
861 child = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(child) : child.nextSibling;
865 if (this.representedObject.contentDocument)
866 appendChildrenOfNode(this.representedObject.contentDocument);
868 appendChildrenOfNode(this.representedObject);
870 if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
871 var title = "<span class=\"webkit-html-tag close\"></" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "></span>";
872 var item = new TreeElement(title, this.representedObject, false);
873 item.selectable = false;
874 this.appendChild(item);
880 this.treeOutline.panel.updateTreeSelection();
883 oncollapse: function()
885 this.treeOutline.panel.updateTreeSelection();
890 if (this.listItemElement)
891 this.listItemElement.scrollIntoViewIfNeeded(false);
896 this._selectedByCurrentMouseDown = true;
897 this.treeOutline.panel.focusedDOMNode = this.representedObject;
898 this.updateSelection();
901 onmousedown: function(event)
906 if (this._selectedByCurrentMouseDown)
907 delete this._selectedByCurrentMouseDown;
908 else if (this._startEditing(event)) {
909 event.preventDefault();
913 // Prevent selecting the nearest word on double click.
914 if (event.detail >= 2)
915 event.preventDefault();
918 ondblclick: function(treeElement, event)
923 var panel = this.treeOutline.panel;
924 panel.rootDOMNode = this.parent.representedObject;
925 panel.focusedDOMNode = this.representedObject;
927 if (this.hasChildren && !this.expanded)
931 _startEditing: function(event)
933 if (this.treeOutline.panel.focusedDOMNode != this.representedObject)
936 if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE)
939 var textNode = event.target.firstParentOrSelfWithClass("webkit-html-text-node");
941 return this._startEditingTextNode(textNode);
943 var attribute = event.target.firstParentOrSelfWithClass("webkit-html-attribute");
945 return this._startEditingAttribute(attribute, event);
950 _startEditingAttribute: function(attribute, event)
952 if (WebInspector.isBeingEdited(attribute))
955 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
956 if (!attributeNameElement)
959 var isURL = event.target.firstParentOrSelfWithClass("webkit-html-external-link") || event.target.firstParentOrSelfWithClass("webkit-html-resource-link");
960 if (isURL && event.altKey)
963 var attributeName = attributeNameElement.innerText;
965 function removeZeroWidthSpaceRecursive(node)
967 if (node.nodeType === Node.TEXT_NODE) {
968 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
972 if (node.nodeType !== Node.ELEMENT_NODE)
975 for (var child = node.firstChild; child; child = child.nextSibling)
976 removeZeroWidthSpaceRecursive(child);
979 // Remove zero-width spaces that were added by nodeTitleInfo.
980 removeZeroWidthSpaceRecursive(attribute);
982 this._editing = true;
984 WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
985 window.getSelection().setBaseAndExtent(event.target, 0, event.target, 1);
990 _startEditingTextNode: function(textNode)
992 if (WebInspector.isBeingEdited(textNode))
995 this._editing = true;
997 WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
998 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
1003 _attributeEditingCommitted: function(element, newText, oldText, attributeName)
1005 delete this._editing;
1007 var parseContainerElement = document.createElement("span");
1008 parseContainerElement.innerHTML = "<span " + newText + "></span>";
1009 var parseElement = parseContainerElement.firstChild;
1010 if (!parseElement || !parseElement.hasAttributes()) {
1011 editingCancelled(element, context);
1015 var foundOriginalAttribute = false;
1016 for (var i = 0; i < parseElement.attributes.length; ++i) {
1017 var attr = parseElement.attributes[i];
1018 foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
1019 Element.prototype.setAttribute.call(this.representedObject, attr.name, attr.value);
1022 if (!foundOriginalAttribute)
1023 Element.prototype.removeAttribute.call(this.representedObject, attributeName);
1025 this._updateTitle();
1026 this.treeOutline.panel._focusedNodeChanged(true);
1029 _textNodeEditingCommitted: function(element, newText)
1031 delete this._editing;
1034 if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
1035 // We only show text nodes inline in elements if the element only
1036 // has a single child, and that child is a text node.
1037 textNode = this.representedObject.firstChild;
1038 } else if (this.representedObject.nodeType == Node.TEXT_NODE)
1039 textNode = this.representedObject;
1041 textNode.nodeValue = newText;
1042 this._updateTitle();
1046 _editingCancelled: function(element, context)
1048 delete this._editing;
1050 this._updateTitle();
1053 _updateTitle: function()
1055 this.title = nodeTitleInfo.call(this.representedObject, this.hasChildren, WebInspector.linkifyURL).title;
1056 delete this.selectionElement;
1057 this.updateSelection();
1058 this._makeURLsActivateOnModifiedClick();
1062 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;