1/* 2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 15 * its contributors may be used to endorse or promote products derived 16 * from this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 30WebInspector.ElementsTreeOutline = function() { 31 this.element = document.createElement("ol"); 32 this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); 33 this.element.addEventListener("dblclick", this._ondblclick.bind(this), false); 34 this.element.addEventListener("mousemove", this._onmousemove.bind(this), false); 35 this.element.addEventListener("mouseout", this._onmouseout.bind(this), false); 36 37 TreeOutline.call(this, this.element); 38 39 this.includeRootDOMNode = true; 40 this.selectEnabled = false; 41 this.rootDOMNode = null; 42 this.focusedDOMNode = null; 43} 44 45WebInspector.ElementsTreeOutline.prototype = { 46 get rootDOMNode() 47 { 48 return this._rootDOMNode; 49 }, 50 51 set rootDOMNode(x) 52 { 53 if (objectsAreSame(this._rootDOMNode, x)) 54 return; 55 56 this._rootDOMNode = x; 57 58 this.update(); 59 }, 60 61 get focusedDOMNode() 62 { 63 return this._focusedDOMNode; 64 }, 65 66 set focusedDOMNode(x) 67 { 68 if (objectsAreSame(this._focusedDOMNode, x)) { 69 this.revealAndSelectNode(x); 70 return; 71 } 72 73 this._focusedDOMNode = x; 74 75 this.revealAndSelectNode(x); 76 77 // The revealAndSelectNode() method might find a different element if there is inlined text, 78 // and the select() call would change the focusedDOMNode and reenter this setter. So to 79 // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same 80 // node as the one passed in. 81 if (objectsAreSame(this._focusedDOMNode, x)) { 82 this.focusedNodeChanged(); 83 84 if (x && !this.suppressSelectHighlight) { 85 InspectorController.highlightDOMNode(x); 86 87 if ("_restorePreviousHighlightNodeTimeout" in this) 88 clearTimeout(this._restorePreviousHighlightNodeTimeout); 89 90 function restoreHighlightToHoveredNode() 91 { 92 var hoveredNode = WebInspector.hoveredDOMNode; 93 if (hoveredNode) 94 InspectorController.highlightDOMNode(hoveredNode); 95 else 96 InspectorController.hideDOMNodeHighlight(); 97 } 98 99 this._restorePreviousHighlightNodeTimeout = setTimeout(restoreHighlightToHoveredNode, 2000); 100 } 101 } 102 }, 103 104 update: function() 105 { 106 this.removeChildren(); 107 108 if (!this.rootDOMNode) 109 return; 110 111 var treeElement; 112 if (this.includeRootDOMNode) { 113 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode); 114 treeElement.selectable = this.selectEnabled; 115 this.appendChild(treeElement); 116 } else { 117 // FIXME: this could use findTreeElement to reuse a tree element if it already exists 118 var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild); 119 while (node) { 120 treeElement = new WebInspector.ElementsTreeElement(node); 121 treeElement.selectable = this.selectEnabled; 122 this.appendChild(treeElement); 123 node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling; 124 } 125 } 126 127 this.updateSelection(); 128 }, 129 130 updateSelection: function() 131 { 132 if (!this.selectedTreeElement) 133 return; 134 var element = this.treeOutline.selectedTreeElement; 135 element.updateSelection(); 136 }, 137 138 focusedNodeChanged: function(forceUpdate) {}, 139 140 findTreeElement: function(node, isAncestor, getParent, equal) 141 { 142 if (typeof isAncestor === "undefined") 143 isAncestor = isAncestorIncludingParentFrames; 144 if (typeof getParent === "undefined") 145 getParent = parentNodeOrFrameElement; 146 if (typeof equal === "undefined") 147 equal = objectsAreSame; 148 149 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestor, getParent, equal); 150 if (!treeElement && node.nodeType === Node.TEXT_NODE) { 151 // The text node might have been inlined if it was short, so try to find the parent element. 152 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestor, getParent, equal); 153 } 154 155 return treeElement; 156 }, 157 158 revealAndSelectNode: function(node) 159 { 160 if (!node) 161 return; 162 163 var treeElement = this.findTreeElement(node); 164 if (!treeElement) 165 return; 166 167 treeElement.reveal(); 168 treeElement.select(); 169 }, 170 171 _treeElementFromEvent: function(event) 172 { 173 var root = this.element; 174 175 // We choose this X coordinate based on the knowledge that our list 176 // items extend nearly to the right edge of the outer <ol>. 177 var x = root.totalOffsetLeft + root.offsetWidth - 20; 178 179 var y = event.pageY; 180 181 // Our list items have 1-pixel cracks between them vertically. We avoid 182 // the cracks by checking slightly above and slightly below the mouse 183 // and seeing if we hit the same element each time. 184 var elementUnderMouse = this.treeElementFromPoint(x, y); 185 var elementAboveMouse = this.treeElementFromPoint(x, y - 2); 186 var element; 187 if (elementUnderMouse === elementAboveMouse) 188 element = elementUnderMouse; 189 else 190 element = this.treeElementFromPoint(x, y + 2); 191 192 return element; 193 }, 194 195 _ondblclick: function(event) 196 { 197 var element = this._treeElementFromEvent(event); 198 199 if (!element || !element.ondblclick) 200 return; 201 202 element.ondblclick(element, event); 203 }, 204 205 _onmousedown: function(event) 206 { 207 var element = this._treeElementFromEvent(event); 208 209 if (!element || element.isEventWithinDisclosureTriangle(event)) 210 return; 211 212 element.select(); 213 }, 214 215 _onmousemove: function(event) 216 { 217 if (this._previousHoveredElement) { 218 this._previousHoveredElement.hovered = false; 219 delete this._previousHoveredElement; 220 } 221 222 var element = this._treeElementFromEvent(event); 223 if (element && !element.elementCloseTag) { 224 element.hovered = true; 225 this._previousHoveredElement = element; 226 } 227 228 WebInspector.hoveredDOMNode = (element && !element.elementCloseTag ? element.representedObject : null); 229 }, 230 231 _onmouseout: function(event) 232 { 233 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 234 if (nodeUnderMouse.isDescendant(this.element)) 235 return; 236 237 if (this._previousHoveredElement) { 238 this._previousHoveredElement.hovered = false; 239 delete this._previousHoveredElement; 240 } 241 242 WebInspector.hoveredDOMNode = null; 243 } 244} 245 246WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype; 247 248WebInspector.ElementsTreeElement = function(node) 249{ 250 var hasChildren = node.contentDocument || (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes()); 251 var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL); 252 253 if (titleInfo.hasChildren) 254 this.whitespaceIgnored = Preferences.ignoreWhitespace; 255 256 // The title will be updated in onattach. 257 TreeElement.call(this, "", node, titleInfo.hasChildren); 258} 259 260WebInspector.ElementsTreeElement.prototype = { 261 get highlighted() 262 { 263 return this._highlighted; 264 }, 265 266 set highlighted(x) 267 { 268 if (this._highlighted === x) 269 return; 270 271 this._highlighted = x; 272 273 if (this.listItemElement) { 274 if (x) 275 this.listItemElement.addStyleClass("highlighted"); 276 else 277 this.listItemElement.removeStyleClass("highlighted"); 278 } 279 }, 280 281 get hovered() 282 { 283 return this._hovered; 284 }, 285 286 set hovered(x) 287 { 288 if (this._hovered === x) 289 return; 290 291 this._hovered = x; 292 293 if (this.listItemElement) { 294 if (x) { 295 this.updateSelection(); 296 this.listItemElement.addStyleClass("hovered"); 297 } else 298 this.listItemElement.removeStyleClass("hovered"); 299 } 300 }, 301 302 updateSelection: function() 303 { 304 var listItemElement = this.listItemElement; 305 if (!listItemElement) 306 return; 307 308 if (document.body.offsetWidth <= 0) { 309 // The stylesheet hasn't loaded yet or the window is closed, 310 // so we can't calculate what is need. Return early. 311 return; 312 } 313 314 if (!this.selectionElement) { 315 this.selectionElement = document.createElement("div"); 316 this.selectionElement.className = "selection selected"; 317 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); 318 } 319 320 this.selectionElement.style.height = listItemElement.offsetHeight + "px"; 321 }, 322 323 onattach: function() 324 { 325 this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false); 326 327 if (this._highlighted) 328 this.listItemElement.addStyleClass("highlighted"); 329 330 if (this._hovered) { 331 this.updateSelection(); 332 this.listItemElement.addStyleClass("hovered"); 333 } 334 335 this._updateTitle(); 336 337 this._preventFollowingLinksOnDoubleClick(); 338 }, 339 340 _preventFollowingLinksOnDoubleClick: function() 341 { 342 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"); 343 if (!links) 344 return; 345 346 for (var i = 0; i < links.length; ++i) 347 links[i].preventFollowOnDoubleClick = true; 348 }, 349 350 onpopulate: function() 351 { 352 if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace) 353 return; 354 355 this.whitespaceIgnored = Preferences.ignoreWhitespace; 356 357 this.updateChildren(); 358 }, 359 360 updateChildren: function(fullRefresh) 361 { 362 if (fullRefresh) { 363 var selectedTreeElement = this.treeOutline.selectedTreeElement; 364 if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) 365 this.select(); 366 this.removeChildren(); 367 } 368 369 var treeElement = this; 370 var treeChildIndex = 0; 371 372 function updateChildrenOfNode(node) 373 { 374 var treeOutline = treeElement.treeOutline; 375 var child = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(node) : node.firstChild); 376 while (child) { 377 var currentTreeElement = treeElement.children[treeChildIndex]; 378 if (!currentTreeElement || !objectsAreSame(currentTreeElement.representedObject, child)) { 379 // Find any existing element that is later in the children list. 380 var existingTreeElement = null; 381 for (var i = (treeChildIndex + 1); i < treeElement.children.length; ++i) { 382 if (objectsAreSame(treeElement.children[i].representedObject, child)) { 383 existingTreeElement = treeElement.children[i]; 384 break; 385 } 386 } 387 388 if (existingTreeElement && existingTreeElement.parent === treeElement) { 389 // If an existing element was found and it has the same parent, just move it. 390 var wasSelected = existingTreeElement.selected; 391 treeElement.removeChild(existingTreeElement); 392 treeElement.insertChild(existingTreeElement, treeChildIndex); 393 if (wasSelected) 394 existingTreeElement.select(); 395 } else { 396 // No existing element found, insert a new element. 397 var newElement = new WebInspector.ElementsTreeElement(child); 398 newElement.selectable = treeOutline.selectEnabled; 399 treeElement.insertChild(newElement, treeChildIndex); 400 } 401 } 402 403 child = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(child) : child.nextSibling; 404 ++treeChildIndex; 405 } 406 } 407 408 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent. 409 for (var i = (this.children.length - 1); i >= 0; --i) { 410 if ("elementCloseTag" in this.children[i]) 411 continue; 412 413 var currentChild = this.children[i]; 414 var currentNode = currentChild.representedObject; 415 var currentParentNode = currentNode.parentNode; 416 417 if (objectsAreSame(currentParentNode, this.representedObject)) 418 continue; 419 if (this.representedObject.contentDocument && objectsAreSame(currentParentNode, this.representedObject.contentDocument)) 420 continue; 421 422 var selectedTreeElement = this.treeOutline.selectedTreeElement; 423 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) 424 this.select(); 425 426 this.removeChildAtIndex(i); 427 428 if (this.treeOutline.panel && currentNode.contentDocument) 429 this.treeOutline.panel.unregisterMutationEventListeners(currentNode.contentDocument.defaultView); 430 } 431 432 if (this.representedObject.contentDocument) 433 updateChildrenOfNode(this.representedObject.contentDocument); 434 updateChildrenOfNode(this.representedObject); 435 436 var lastChild = this.children[this.children.length - 1]; 437 if (this.representedObject.nodeType == Node.ELEMENT_NODE && (!lastChild || !lastChild.elementCloseTag)) { 438 var title = "<span class=\"webkit-html-tag close\"></" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "></span>"; 439 var item = new TreeElement(title, null, false); 440 item.selectable = false; 441 item.elementCloseTag = true; 442 this.appendChild(item); 443 } 444 }, 445 446 onexpand: function() 447 { 448 this.treeOutline.updateSelection(); 449 450 if (this.treeOutline.panel && this.representedObject.contentDocument) 451 this.treeOutline.panel.registerMutationEventListeners(this.representedObject.contentDocument.defaultView); 452 }, 453 454 oncollapse: function() 455 { 456 this.treeOutline.updateSelection(); 457 }, 458 459 onreveal: function() 460 { 461 if (this.listItemElement) 462 this.listItemElement.scrollIntoViewIfNeeded(false); 463 }, 464 465 onselect: function() 466 { 467 this.treeOutline.focusedDOMNode = this.representedObject; 468 this.updateSelection(); 469 }, 470 471 onmousedown: function(event) 472 { 473 if (this._editing) 474 return; 475 476 // Prevent selecting the nearest word on double click. 477 if (event.detail >= 2) 478 event.preventDefault(); 479 }, 480 481 ondblclick: function(treeElement, event) 482 { 483 if (this._editing) 484 return; 485 486 if (this._startEditing(event)) 487 return; 488 489 if (this.treeOutline.panel) { 490 this.treeOutline.rootDOMNode = this.parent.representedObject; 491 this.treeOutline.focusedDOMNode = this.representedObject; 492 } 493 494 if (this.hasChildren && !this.expanded) 495 this.expand(); 496 }, 497 498 _startEditing: function(event) 499 { 500 if (this.treeOutline.focusedDOMNode != this.representedObject) 501 return; 502 503 if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE) 504 return false; 505 506 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 507 if (textNode) 508 return this._startEditingTextNode(textNode); 509 510 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 511 if (attribute) 512 return this._startEditingAttribute(attribute, event); 513 514 return false; 515 }, 516 517 _startEditingAttribute: function(attribute, event) 518 { 519 if (WebInspector.isBeingEdited(attribute)) 520 return true; 521 522 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; 523 if (!attributeNameElement) 524 return false; 525 526 var attributeName = attributeNameElement.innerText; 527 528 function removeZeroWidthSpaceRecursive(node) 529 { 530 if (node.nodeType === Node.TEXT_NODE) { 531 node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); 532 return; 533 } 534 535 if (node.nodeType !== Node.ELEMENT_NODE) 536 return; 537 538 for (var child = node.firstChild; child; child = child.nextSibling) 539 removeZeroWidthSpaceRecursive(child); 540 } 541 542 // Remove zero-width spaces that were added by nodeTitleInfo. 543 removeZeroWidthSpaceRecursive(attribute); 544 545 this._editing = true; 546 547 WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName); 548 window.getSelection().setBaseAndExtent(event.target, 0, event.target, 1); 549 550 return true; 551 }, 552 553 _startEditingTextNode: function(textNode) 554 { 555 if (WebInspector.isBeingEdited(textNode)) 556 return true; 557 558 this._editing = true; 559 560 WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this)); 561 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1); 562 563 return true; 564 }, 565 566 _attributeEditingCommitted: function(element, newText, oldText, attributeName) 567 { 568 delete this._editing; 569 570 var parseContainerElement = document.createElement("span"); 571 parseContainerElement.innerHTML = "<span " + newText + "></span>"; 572 var parseElement = parseContainerElement.firstChild; 573 if (!parseElement || !parseElement.hasAttributes()) { 574 editingCancelled(element, context); 575 return; 576 } 577 578 var foundOriginalAttribute = false; 579 for (var i = 0; i < parseElement.attributes.length; ++i) { 580 var attr = parseElement.attributes[i]; 581 foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName; 582 InspectorController.inspectedWindow().Element.prototype.setAttribute.call(this.representedObject, attr.name, attr.value); 583 } 584 585 if (!foundOriginalAttribute) 586 InspectorController.inspectedWindow().Element.prototype.removeAttribute.call(this.representedObject, attributeName); 587 588 this._updateTitle(); 589 590 this.treeOutline.focusedNodeChanged(true); 591 }, 592 593 _textNodeEditingCommitted: function(element, newText) 594 { 595 delete this._editing; 596 597 var textNode; 598 if (this.representedObject.nodeType == Node.ELEMENT_NODE) { 599 // We only show text nodes inline in elements if the element only 600 // has a single child, and that child is a text node. 601 textNode = this.representedObject.firstChild; 602 } else if (this.representedObject.nodeType == Node.TEXT_NODE) 603 textNode = this.representedObject; 604 605 textNode.nodeValue = newText; 606 this._updateTitle(); 607 }, 608 609 _editingCancelled: function(element, context) 610 { 611 delete this._editing; 612 613 this._updateTitle(); 614 }, 615 616 _updateTitle: function() 617 { 618 var title = nodeTitleInfo.call(this.representedObject, this.hasChildren, WebInspector.linkifyURL).title; 619 this.title = "<span class=\"highlight\">" + title + "</span>"; 620 delete this.selectionElement; 621 this.updateSelection(); 622 this._preventFollowingLinksOnDoubleClick(); 623 }, 624} 625 626WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype; 627