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 panel.updateBreadcrumb();
42 panel.updateTreeSelection();
45 domView.sideContentElement = document.createElement("div");
46 domView.sideContentElement.className = "content side";
48 domView.treeContentElement = document.createElement("div");
49 domView.treeContentElement.className = "content tree outline-disclosure";
51 function clearNodeHighlight(event)
53 if (event.target === this)
54 InspectorController.hideDOMNodeHighlight();
57 domView.treeListElement = document.createElement("ol");
58 domView.treeListElement.addEventListener("mousedown", this._onmousedown.bind(this), false);
59 domView.treeListElement.addEventListener("dblclick", this._ondblclick.bind(this), false);
60 domView.treeListElement.addEventListener("mousemove", this._onmousemove.bind(this), false);
61 domView.treeListElement.addEventListener("mouseout", clearNodeHighlight.bind(domView.treeListElement), false);
62 domView.treeOutline = new TreeOutline(domView.treeListElement);
63 domView.treeOutline.panel = this;
65 domView.crumbsElement = document.createElement("div");
66 domView.crumbsElement.className = "crumbs";
67 domView.crumbsElement.addEventListener("mouseout", clearNodeHighlight.bind(domView.crumbsElement), false);
69 domView.innerCrumbsElement = document.createElement("div");
70 domView.crumbsElement.appendChild(domView.innerCrumbsElement);
72 domView.sidebarPanes = {};
73 domView.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
74 domView.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
75 domView.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
77 domView.sidebarPanes.styles.onexpand = function() { panel.updateStyles() };
78 domView.sidebarPanes.metrics.onexpand = function() { panel.updateMetrics() };
79 domView.sidebarPanes.properties.onexpand = function() { panel.updateProperties() };
81 domView.sidebarPanes.styles.expanded = true;
83 domView.sidebarElement = document.createElement("div");
84 domView.sidebarElement.className = "sidebar";
86 domView.sidebarElement.appendChild(domView.sidebarPanes.styles.element);
87 domView.sidebarElement.appendChild(domView.sidebarPanes.metrics.element);
88 domView.sidebarElement.appendChild(domView.sidebarPanes.properties.element);
90 domView.sideContentElement.appendChild(domView.treeContentElement);
91 domView.sideContentElement.appendChild(domView.crumbsElement);
92 domView.treeContentElement.appendChild(domView.treeListElement);
94 domView.sidebarResizeElement = document.createElement("div");
95 domView.sidebarResizeElement.className = "sidebar-resizer-vertical sidebar-resizer-vertical-right";
96 domView.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
98 domView.contentElement.appendChild(domView.sideContentElement);
99 domView.contentElement.appendChild(domView.sidebarElement);
100 domView.contentElement.appendChild(domView.sidebarResizeElement);
102 this.rootDOMNode = this.resource.documentNode;
105 WebInspector.DocumentPanel.prototype = {
108 this.updateTreeSelection();
109 this.updateBreadcrumbSizes();
112 updateTreeSelection: function()
114 if (!this.views.dom.treeOutline || !this.views.dom.treeOutline.selectedTreeElement)
116 var element = this.views.dom.treeOutline.selectedTreeElement;
117 element.updateSelection();
122 return this._rootDOMNode;
127 if (this._rootDOMNode === x)
130 this._rootDOMNode = x;
132 this.updateBreadcrumb();
133 this.updateTreeOutline();
138 return this._focusedDOMNode;
141 set focusedDOMNode(x)
143 if (this._focusedDOMNode === x) {
144 var nodeItem = this.revealNode(x);
150 this._focusedDOMNode = x;
152 this._focusedNodeChanged();
154 var nodeItem = this.revealNode(x);
159 _focusedNodeChanged: function(forceUpdate)
161 this.updateBreadcrumb(forceUpdate);
163 for (var pane in this.views.dom.sidebarPanes)
164 this.views.dom.sidebarPanes[pane].needsUpdate = true;
166 this.updateStyles(forceUpdate);
167 this.updateMetrics();
168 this.updateProperties();
171 revealNode: function(node)
173 var nodeItem = this.views.dom.treeOutline.findTreeElement(node, this._isAncestorIncludingParentFramesWithinPanel.bind(this), this._parentNodeOrFrameElementWithinPanel.bind(this));
181 updateTreeOutline: function()
183 this.views.dom.treeOutline.removeChildrenRecursive();
185 if (!this.rootDOMNode)
188 // FIXME: this could use findTreeElement to reuse a tree element if it already exists
189 var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild);
191 this.views.dom.treeOutline.appendChild(new WebInspector.DOMNodeTreeElement(node));
192 node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
195 this.updateTreeSelection();
198 updateBreadcrumb: function(forceUpdate)
203 var crumbs = this.views.dom.innerCrumbsElement;
206 var foundRoot = false;
207 var crumb = crumbs.firstChild;
209 if (crumb.representedObject === this.rootDOMNode)
213 crumb.addStyleClass("dimmed");
215 crumb.removeStyleClass("dimmed");
217 if (crumb.representedObject === this.focusedDOMNode) {
218 crumb.addStyleClass("selected");
221 crumb.removeStyleClass("selected");
224 crumb = crumb.nextSibling;
227 if (handled && !forceUpdate) {
228 // We don't need to rebuild the crumbs, but we need to adjust sizes
229 // to reflect the new focused or root node.
230 this.updateBreadcrumbSizes();
234 crumbs.removeChildren();
237 var selectCrumbFunction = function(event) {
238 var crumb = event.currentTarget;
239 if (crumb.hasStyleClass("collapsed")) {
240 // Clicking a collapsed crumb will expose the hidden crumbs.
241 if (crumb === panel.views.dom.innerCrumbsElement.firstChild) {
242 // If the focused crumb is the first child, pick the farthest crumb
243 // that is still hidden. This allows the user to expose every crumb.
244 var currentCrumb = crumb;
245 while (currentCrumb) {
246 var hidden = currentCrumb.hasStyleClass("hidden");
247 var collapsed = currentCrumb.hasStyleClass("collapsed");
248 if (!hidden && !collapsed)
250 crumb = currentCrumb;
251 currentCrumb = currentCrumb.nextSibling;
255 panel.updateBreadcrumbSizes(crumb);
257 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
258 // will change the root node in addition to the focused node.
259 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
260 panel.rootDOMNode = crumb.representedObject.parentNode;
261 panel.focusedDOMNode = crumb.representedObject;
264 event.preventDefault();
267 var mouseOverCrumbFunction = function(event) {
268 panel.mouseOverCrumb = true;
269 InspectorController.highlightDOMNode(this.representedObject);
271 if ("mouseOutTimeout" in panel) {
272 clearTimeout(panel.mouseOutTimeout);
273 delete panel.mouseOutTimeout;
277 var mouseOutCrumbFunction = function(event) {
278 delete panel.mouseOverCrumb;
280 if ("mouseOutTimeout" in panel) {
281 clearTimeout(panel.mouseOutTimeout);
282 delete panel.mouseOutTimeout;
285 var timeoutFunction = function() {
286 if (!panel.mouseOverCrumb)
287 panel.updateBreadcrumbSizes();
290 panel.mouseOutTimeout = setTimeout(timeoutFunction, 500);
294 for (var current = this.focusedDOMNode; current; current = this._parentNodeOrFrameElementWithinPanel(current)) {
295 if (current === this.resource.documentNode)
298 if (current.nodeType === Node.DOCUMENT_NODE)
301 if (current === this.rootDOMNode)
304 var crumb = document.createElement("span");
305 crumb.className = "crumb";
306 crumb.representedObject = current;
307 crumb.addEventListener("mousedown", selectCrumbFunction, false);
308 crumb.addEventListener("mouseover", mouseOverCrumbFunction.bind(crumb), false);
309 crumb.addEventListener("mouseout", mouseOutCrumbFunction, false);
312 switch (current.nodeType) {
313 case Node.ELEMENT_NODE:
314 crumbTitle = current.nodeName.toLowerCase();
316 var nameElement = document.createElement("span");
317 nameElement.textContent = crumbTitle;
318 crumb.appendChild(nameElement);
320 var idAttribute = current.getAttribute("id");
322 var idElement = document.createElement("span");
323 crumb.appendChild(idElement);
325 var part = "#" + idAttribute;
327 idElement.appendChild(document.createTextNode(part));
329 // Mark the name as extra, since the ID is more important.
330 nameElement.className = "extra";
333 var classAttribute = current.getAttribute("class");
334 if (classAttribute) {
335 var classes = classAttribute.split(/\s+/);
336 var foundClasses = {};
338 if (classes.length) {
339 var classesElement = document.createElement("span");
340 classesElement.className = "extra";
341 crumb.appendChild(classesElement);
343 for (var i = 0; i < classes.length; ++i) {
344 var className = classes[i];
345 if (className && !(className in foundClasses)) {
346 var part = "." + className;
348 classesElement.appendChild(document.createTextNode(part));
349 foundClasses[className] = true;
358 if (isNodeWhitespace.call(current))
359 crumbTitle = WebInspector.UIString("(whitespace)");
361 crumbTitle = WebInspector.UIString("(text)");
364 case Node.COMMENT_NODE:
365 crumbTitle = "<!-->";
369 crumbTitle = current.nodeName.toLowerCase();
372 if (!crumb.childNodes.length) {
373 var nameElement = document.createElement("span");
374 nameElement.textContent = crumbTitle;
375 crumb.appendChild(nameElement);
378 crumb.title = crumbTitle;
381 crumb.addStyleClass("dimmed");
382 if (current === this.focusedDOMNode)
383 crumb.addStyleClass("selected");
384 if (!crumbs.childNodes.length)
385 crumb.addStyleClass("end");
387 crumbs.appendChild(crumb);
390 if (crumbs.hasChildNodes())
391 crumbs.lastChild.addStyleClass("start");
393 this.updateBreadcrumbSizes();
396 updateBreadcrumbSizes: function(focusedCrumb)
401 if (document.body.offsetWidth <= 0) {
402 // The stylesheet hasn't loaded yet, so we need to update later.
403 setTimeout(this.updateBreadcrumbSizes.bind(this), 0);
407 var crumbs = this.views.dom.innerCrumbsElement;
408 if (!crumbs.childNodes.length)
409 return; // No crumbs, do nothing.
411 var crumbsContainer = this.views.dom.crumbsElement;
412 if (crumbsContainer.offsetWidth <= 0 || crumbs.offsetWidth <= 0)
415 // A Zero index is the right most child crumb in the breadcrumb.
416 var selectedIndex = 0;
417 var focusedIndex = 0;
421 var crumb = crumbs.firstChild;
423 // Find the selected crumb and index.
424 if (!selectedCrumb && crumb.hasStyleClass("selected")) {
425 selectedCrumb = crumb;
429 // Find the focused crumb index.
430 if (crumb === focusedCrumb)
433 // Remove any styles that affect size before
434 // deciding to shorten any crumbs.
435 if (crumb !== crumbs.lastChild)
436 crumb.removeStyleClass("start");
437 if (crumb !== crumbs.firstChild)
438 crumb.removeStyleClass("end");
440 crumb.removeStyleClass("compact");
441 crumb.removeStyleClass("collapsed");
442 crumb.removeStyleClass("hidden");
444 crumb = crumb.nextSibling;
448 // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
449 // The order of the crumbs in the document is opposite of the visual order.
450 crumbs.firstChild.addStyleClass("end");
451 crumbs.lastChild.addStyleClass("start");
453 function crumbsAreSmallerThanContainer()
455 // There is some fixed extra space that is not returned in the crumbs' offsetWidth.
456 // This padding is added to the crumbs' offsetWidth when comparing to the crumbsContainer.
457 var rightPadding = 9;
458 return ((crumbs.offsetWidth + rightPadding) < crumbsContainer.offsetWidth);
461 if (crumbsAreSmallerThanContainer())
462 return; // No need to compact the crumbs, they all fit at full size.
465 var AncestorSide = -1;
468 function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
470 if (!significantCrumb)
471 significantCrumb = (focusedCrumb || selectedCrumb);
473 if (significantCrumb === selectedCrumb)
474 var significantIndex = selectedIndex;
475 else if (significantCrumb === focusedCrumb)
476 var significantIndex = focusedIndex;
478 var significantIndex = 0;
479 for (var i = 0; i < crumbs.childNodes.length; ++i) {
480 if (crumbs.childNodes[i] === significantCrumb) {
481 significantIndex = i;
487 function shrinkCrumbAtIndex(index)
489 var shrinkCrumb = crumbs.childNodes[index];
490 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
491 shrinkingFunction(shrinkCrumb);
492 if (crumbsAreSmallerThanContainer())
493 return true; // No need to compact the crumbs more.
497 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
498 // fit in the crumbsContainer or we run out of crumbs to shrink.
500 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
501 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
502 while (index !== significantIndex) {
503 if (shrinkCrumbAtIndex(index))
505 index += (direction > 0 ? 1 : -1);
508 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
509 // with a tie going to child crumbs.
511 var endIndex = crumbs.childNodes.length - 1;
512 while (startIndex != significantIndex || endIndex != significantIndex) {
513 var startDistance = significantIndex - startIndex;
514 var endDistance = endIndex - significantIndex;
515 if (startDistance >= endDistance)
516 var index = startIndex++;
518 var index = endIndex--;
519 if (shrinkCrumbAtIndex(index))
524 // We are not small enough yet, return false so the caller knows.
528 function coalesceCollapsedCrumbs()
530 var crumb = crumbs.firstChild;
531 var collapsedRun = false;
532 var newStartNeeded = false;
533 var newEndNeeded = false;
535 var hidden = crumb.hasStyleClass("hidden");
537 var collapsed = crumb.hasStyleClass("collapsed");
538 if (collapsedRun && collapsed) {
539 crumb.addStyleClass("hidden");
540 crumb.removeStyleClass("compact");
541 crumb.removeStyleClass("collapsed");
543 if (crumb.hasStyleClass("start")) {
544 crumb.removeStyleClass("start");
545 newStartNeeded = true;
548 if (crumb.hasStyleClass("end")) {
549 crumb.removeStyleClass("end");
556 collapsedRun = collapsed;
559 newEndNeeded = false;
560 crumb.addStyleClass("end");
564 crumb = crumb.nextSibling;
567 if (newStartNeeded) {
568 crumb = crumbs.lastChild;
570 if (!crumb.hasStyleClass("hidden")) {
571 crumb.addStyleClass("start");
574 crumb = crumb.previousSibling;
579 function compact(crumb)
581 if (crumb.hasStyleClass("hidden"))
583 crumb.addStyleClass("compact");
586 function collapse(crumb, dontCoalesce)
588 if (crumb.hasStyleClass("hidden"))
590 crumb.addStyleClass("collapsed");
591 crumb.removeStyleClass("compact");
593 coalesceCollapsedCrumbs();
596 function compactDimmed(crumb)
598 if (crumb.hasStyleClass("dimmed"))
602 function collapseDimmed(crumb)
604 if (crumb.hasStyleClass("dimmed"))
609 // When not focused on a crumb we can be biased and collapse less important
610 // crumbs that the user might not care much about.
612 // Compact child crumbs.
613 if (makeCrumbsSmaller(compact, ChildSide))
616 // Collapse child crumbs.
617 if (makeCrumbsSmaller(collapse, ChildSide))
620 // Compact dimmed ancestor crumbs.
621 if (makeCrumbsSmaller(compactDimmed, AncestorSide))
624 // Collapse dimmed ancestor crumbs.
625 if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
629 // Compact ancestor crumbs, or from both sides if focused.
630 if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
633 // Collapse ancestor crumbs, or from both sides if focused.
634 if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
640 // Compact the selected crumb.
641 compact(selectedCrumb);
642 if (crumbsAreSmallerThanContainer())
645 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
646 collapse(selectedCrumb, true);
649 updateStyles: function(forceUpdate)
651 var stylesSidebarPane = this.views.dom.sidebarPanes.styles;
652 if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
655 stylesSidebarPane.update(this.focusedDOMNode, undefined, forceUpdate);
656 stylesSidebarPane.needsUpdate = false;
659 updateMetrics: function()
661 var metricsSidebarPane = this.views.dom.sidebarPanes.metrics;
662 if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
665 metricsSidebarPane.update(this.focusedDOMNode);
666 metricsSidebarPane.needsUpdate = false;
669 updateProperties: function()
671 var propertiesSidebarPane = this.views.dom.sidebarPanes.properties;
672 if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
675 propertiesSidebarPane.update(this.focusedDOMNode);
676 propertiesSidebarPane.needsUpdate = false;
679 handleKeyEvent: function(event)
681 if (this.views.dom.treeOutline && this.currentView && this.currentView === this.views.dom)
682 this.views.dom.treeOutline.handleKeyEvent(event);
685 handleCopyEvent: function(event)
687 if (this.currentView !== this.views.dom)
690 // Don't prevent the normal copy if the user has a selection.
691 if (!window.getSelection().isCollapsed)
694 switch (this.focusedDOMNode.nodeType) {
695 case Node.ELEMENT_NODE:
696 var data = this.focusedDOMNode.outerHTML;
699 case Node.COMMENT_NODE:
700 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
705 var data = this.focusedDOMNode.nodeValue;
708 event.clipboardData.clearData();
709 event.preventDefault();
712 event.clipboardData.setData("text/plain", data);
715 rightSidebarResizerDragStart: function(event)
717 if (this.sidebarDragEventListener || this.sidebarDragEndEventListener)
718 return this.rightSidebarResizerDragEnd(event);
720 this.sidebarDragEventListener = this.rightSidebarResizerDrag.bind(this);
721 this.sidebarDragEndEventListener = this.rightSidebarResizerDragEnd.bind(this);
722 WebInspector.elementDragStart(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event, "col-resize");
725 rightSidebarResizerDragEnd: function(event)
727 WebInspector.elementDragEnd(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event);
728 delete this.sidebarDragEventListener;
729 delete this.sidebarDragEndEventListener;
732 rightSidebarResizerDrag: function(event)
736 var leftSidebarWidth = document.getElementById("sidebar").offsetWidth;
737 var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
739 this.views.dom.sidebarElement.style.width = newWidth + "px";
740 this.views.dom.sideContentElement.style.right = newWidth + "px";
741 this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
743 this.updateTreeSelection();
744 this.updateBreadcrumbSizes();
746 event.preventDefault();
749 _getDocumentForNode: function(node)
751 return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
754 _parentNodeOrFrameElementWithinPanel: function(node)
756 var parent = node.parentNode;
760 var document = this._getDocumentForNode(node);
762 if (document === this.resource.documentNode)
765 return document.defaultView.frameElement;
768 _isAncestorIncludingParentFramesWithinPanel: function(a, b)
770 for (var node = b; node; node = this._getDocumentForNode(node).defaultView.frameElement) {
771 if (isAncestorNode.call(a, node))
774 if (this._getDocumentForNode(node) === this.resource.documentNode) {
775 // We've gone as high in the frame hierarchy as we can without
776 // moving out of this DocumentPanel.
784 _treeElementFromEvent: function(event)
786 var outline = this.views.dom.treeOutline;
788 var root = this.views.dom.treeListElement;
790 // We choose this X coordinate based on the knowledge that our list
791 // items extend nearly to the right edge of the outer <ol>.
792 var x = root.totalOffsetLeft + root.offsetWidth - 20;
796 // Our list items have 1-pixel cracks between them vertically. We avoid
797 // the cracks by checking slightly above and slightly below the mouse
798 // and seeing if we hit the same element each time.
799 var elementUnderMouse = outline.treeElementFromPoint(x, y);
800 var elementAboveMouse = outline.treeElementFromPoint(x, y - 2);
802 if (elementUnderMouse === elementAboveMouse)
803 element = elementUnderMouse;
805 element = outline.treeElementFromPoint(x, y + 2);
810 _ondblclick: function(event)
812 var element = this._treeElementFromEvent(event);
817 element.ondblclick();
820 _onmousedown: function(event)
822 var element = this._treeElementFromEvent(event);
824 if (!element || element.isEventWithinDisclosureTriangle(event))
830 _onmousemove: function(event)
832 var element = this._treeElementFromEvent(event);
836 InspectorController.highlightDOMNode(element.representedObject);
840 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
842 WebInspector.DOMNodeTreeElement = function(node)
844 var hasChildren = node.contentDocument || (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
845 var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
847 if (titleInfo.hasChildren)
848 this.whitespaceIgnored = Preferences.ignoreWhitespace;
850 TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
853 WebInspector.DOMNodeTreeElement.prototype = {
854 updateSelection: function()
856 var listItemElement = this.listItemElement;
857 if (!listItemElement)
860 if (document.body.offsetWidth <= 0) {
861 // The stylesheet hasn't loaded yet, so we need to update later.
862 setTimeout(this.updateSelection.bind(this), 0);
866 if (!this.selectionElement) {
867 this.selectionElement = document.createElement("div");
868 this.selectionElement.className = "selection selected";
869 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
872 this.selectionElement.style.height = listItemElement.offsetHeight + "px";
877 this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false);
879 this._makeURLsActivateOnModifiedClick();
882 _makeURLsActivateOnModifiedClick: function()
884 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");
888 var isMac = InspectorController.platform().indexOf("mac") == 0;
890 for (var i = 0; i < links.length; ++i) {
892 var isExternal = link.hasStyleClass("webkit-html-external-link");
893 var href = link.getAttribute("href");
897 title = WebInspector.UIString("Option-click to visit %s.", href);
899 title = WebInspector.UIString("Option-click to show %s.", href);
902 title = WebInspector.UIString("Alt-click to visit %s.", href);
904 title = WebInspector.UIString("Alt-click to show %s.", href);
906 link.setAttribute("title", title);
907 link.followOnAltClick = true;
911 onpopulate: function()
913 if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
916 this.removeChildren();
917 this.whitespaceIgnored = Preferences.ignoreWhitespace;
919 var treeElement = this;
920 function appendChildrenOfNode(node)
922 var child = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(node) : node.firstChild);
924 treeElement.appendChild(new WebInspector.DOMNodeTreeElement(child));
925 child = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(child) : child.nextSibling;
929 if (this.representedObject.contentDocument)
930 appendChildrenOfNode(this.representedObject.contentDocument);
932 appendChildrenOfNode(this.representedObject);
934 if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
935 var title = "<span class=\"webkit-html-tag close\"></" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "></span>";
936 var item = new TreeElement(title, this.representedObject, false);
937 item.selectable = false;
938 this.appendChild(item);
944 this.treeOutline.panel.updateTreeSelection();
947 oncollapse: function()
949 this.treeOutline.panel.updateTreeSelection();
954 if (this.listItemElement)
955 this.listItemElement.scrollIntoViewIfNeeded(false);
960 this._selectedByCurrentMouseDown = true;
961 this.treeOutline.panel.focusedDOMNode = this.representedObject;
962 this.updateSelection();
965 onmousedown: function(event)
970 if (this._selectedByCurrentMouseDown)
971 delete this._selectedByCurrentMouseDown;
972 else if (this._startEditing(event)) {
973 event.preventDefault();
977 // Prevent selecting the nearest word on double click.
978 if (event.detail >= 2)
979 event.preventDefault();
982 ondblclick: function(treeElement, event)
987 var panel = this.treeOutline.panel;
988 panel.rootDOMNode = this.parent.representedObject;
989 panel.focusedDOMNode = this.representedObject;
991 if (this.hasChildren && !this.expanded)
995 _startEditing: function(event)
997 if (this.treeOutline.panel.focusedDOMNode != this.representedObject)
1000 if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE)
1003 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1005 return this._startEditingTextNode(textNode);
1007 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1009 return this._startEditingAttribute(attribute, event);
1014 _startEditingAttribute: function(attribute, event)
1016 if (WebInspector.isBeingEdited(attribute))
1019 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1020 if (!attributeNameElement)
1023 var isURL = event.target.enclosingNodeOrSelfWithClass("webkit-html-external-link") || event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link");
1024 if (isURL && event.altKey)
1027 var attributeName = attributeNameElement.innerText;
1029 function removeZeroWidthSpaceRecursive(node)
1031 if (node.nodeType === Node.TEXT_NODE) {
1032 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1036 if (node.nodeType !== Node.ELEMENT_NODE)
1039 for (var child = node.firstChild; child; child = child.nextSibling)
1040 removeZeroWidthSpaceRecursive(child);
1043 // Remove zero-width spaces that were added by nodeTitleInfo.
1044 removeZeroWidthSpaceRecursive(attribute);
1046 this._editing = true;
1048 WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1049 window.getSelection().setBaseAndExtent(event.target, 0, event.target, 1);
1054 _startEditingTextNode: function(textNode)
1056 if (WebInspector.isBeingEdited(textNode))
1059 this._editing = true;
1061 WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
1062 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
1067 _attributeEditingCommitted: function(element, newText, oldText, attributeName)
1069 delete this._editing;
1071 var parseContainerElement = document.createElement("span");
1072 parseContainerElement.innerHTML = "<span " + newText + "></span>";
1073 var parseElement = parseContainerElement.firstChild;
1074 if (!parseElement || !parseElement.hasAttributes()) {
1075 editingCancelled(element, context);
1079 var foundOriginalAttribute = false;
1080 for (var i = 0; i < parseElement.attributes.length; ++i) {
1081 var attr = parseElement.attributes[i];
1082 foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
1083 Element.prototype.setAttribute.call(this.representedObject, attr.name, attr.value);
1086 if (!foundOriginalAttribute)
1087 Element.prototype.removeAttribute.call(this.representedObject, attributeName);
1089 this._updateTitle();
1090 this.treeOutline.panel._focusedNodeChanged(true);
1093 _textNodeEditingCommitted: function(element, newText)
1095 delete this._editing;
1098 if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
1099 // We only show text nodes inline in elements if the element only
1100 // has a single child, and that child is a text node.
1101 textNode = this.representedObject.firstChild;
1102 } else if (this.representedObject.nodeType == Node.TEXT_NODE)
1103 textNode = this.representedObject;
1105 textNode.nodeValue = newText;
1106 this._updateTitle();
1110 _editingCancelled: function(element, context)
1112 delete this._editing;
1114 this._updateTitle();
1117 _updateTitle: function()
1119 this.title = nodeTitleInfo.call(this.representedObject, this.hasChildren, WebInspector.linkifyURL).title;
1120 delete this.selectionElement;
1121 this.updateSelection();
1122 this._makeURLsActivateOnModifiedClick();
1126 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;