1 /* 2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Matt Lilek <webkit (at) 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 31 WebInspector.ElementsTreeOutline = function() { 32 this.element = document.createElement("ol"); 33 this.element.addEventListener("mousedown", this._onmousedown.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.showInElementsPanelEnabled = false; 42 this.rootDOMNode = null; 43 this.focusedDOMNode = null; 44 } 45 46 WebInspector.ElementsTreeOutline.prototype = { 47 get rootDOMNode() 48 { 49 return this._rootDOMNode; 50 }, 51 52 set rootDOMNode(x) 53 { 54 if (this._rootDOMNode === x) 55 return; 56 57 this._rootDOMNode = x; 58 59 this._isXMLMimeType = !!(WebInspector.mainResource && WebInspector.mainResource.mimeType && WebInspector.mainResource.mimeType.match(/x(?:ht)?ml/i)); 60 61 this.update(); 62 }, 63 64 get isXMLMimeType() 65 { 66 return this._isXMLMimeType; 67 }, 68 69 nodeNameToCorrectCase: function(nodeName) 70 { 71 return this.isXMLMimeType ? nodeName : nodeName.toLowerCase(); 72 }, 73 74 get focusedDOMNode() 75 { 76 return this._focusedDOMNode; 77 }, 78 79 set focusedDOMNode(x) 80 { 81 if (this._focusedDOMNode === x) { 82 this.revealAndSelectNode(x); 83 return; 84 } 85 86 this._focusedDOMNode = x; 87 88 this.revealAndSelectNode(x); 89 90 // The revealAndSelectNode() method might find a different element if there is inlined text, 91 // and the select() call would change the focusedDOMNode and reenter this setter. So to 92 // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same 93 // node as the one passed in. 94 if (this._focusedDOMNode === x) 95 this.focusedNodeChanged(); 96 }, 97 98 get editing() 99 { 100 return this._editing; 101 }, 102 103 update: function() 104 { 105 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; 106 107 this.removeChildren(); 108 109 if (!this.rootDOMNode) 110 return; 111 112 var treeElement; 113 if (this.includeRootDOMNode) { 114 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode); 115 treeElement.selectable = this.selectEnabled; 116 this.appendChild(treeElement); 117 } else { 118 // FIXME: this could use findTreeElement to reuse a tree element if it already exists 119 var node = this.rootDOMNode.firstChild; 120 while (node) { 121 treeElement = new WebInspector.ElementsTreeElement(node); 122 treeElement.selectable = this.selectEnabled; 123 this.appendChild(treeElement); 124 node = node.nextSibling; 125 } 126 } 127 128 if (selectedNode) 129 this.revealAndSelectNode(selectedNode); 130 }, 131 132 updateSelection: function() 133 { 134 if (!this.selectedTreeElement) 135 return; 136 var element = this.treeOutline.selectedTreeElement; 137 element.updateSelection(); 138 }, 139 140 focusedNodeChanged: function(forceUpdate) {}, 141 142 findTreeElement: function(node) 143 { 144 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode); 145 if (!treeElement && node.nodeType() === Node.TEXT_NODE) { 146 // The text node might have been inlined if it was short, so try to find the parent element. 147 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode); 148 } 149 150 return treeElement; 151 }, 152 153 createTreeElementFor: function(node) 154 { 155 var treeElement = this.findTreeElement(node); 156 if (treeElement) 157 return treeElement; 158 if (!node.parentNode) 159 return null; 160 161 var treeElement = this.createTreeElementFor(node.parentNode); 162 if (treeElement && treeElement.showChild(node.index)) 163 return treeElement.children[node.index]; 164 165 return null; 166 }, 167 168 set suppressRevealAndSelect(x) 169 { 170 if (this._suppressRevealAndSelect === x) 171 return; 172 this._suppressRevealAndSelect = x; 173 }, 174 175 revealAndSelectNode: function(node) 176 { 177 if (!node || this._suppressRevealAndSelect) 178 return; 179 180 var treeElement = this.createTreeElementFor(node); 181 if (!treeElement) 182 return; 183 184 treeElement.reveal(); 185 treeElement.select(); 186 }, 187 188 _treeElementFromEvent: function(event) 189 { 190 var scrollContainer = this.element.parentElement; 191 192 // We choose this X coordinate based on the knowledge that our list 193 // items extend at least to the right edge of the outer <ol> container. 194 // In the no-word-wrap mode the outer <ol> may be wider than the tree container 195 // (and partially hidden), in which case we are left to use only its right boundary. 196 var x = scrollContainer.totalOffsetLeft + scrollContainer.offsetWidth - 36; 197 198 var y = event.pageY; 199 200 // Our list items have 1-pixel cracks between them vertically. We avoid 201 // the cracks by checking slightly above and slightly below the mouse 202 // and seeing if we hit the same element each time. 203 var elementUnderMouse = this.treeElementFromPoint(x, y); 204 var elementAboveMouse = this.treeElementFromPoint(x, y - 2); 205 var element; 206 if (elementUnderMouse === elementAboveMouse) 207 element = elementUnderMouse; 208 else 209 element = this.treeElementFromPoint(x, y + 2); 210 211 return element; 212 }, 213 214 _onmousedown: function(event) 215 { 216 var element = this._treeElementFromEvent(event); 217 218 if (!element || element.isEventWithinDisclosureTriangle(event)) 219 return; 220 221 element.select(); 222 }, 223 224 _onmousemove: function(event) 225 { 226 var element = this._treeElementFromEvent(event); 227 if (element && this._previousHoveredElement === element) 228 return; 229 230 if (this._previousHoveredElement) { 231 this._previousHoveredElement.hovered = false; 232 delete this._previousHoveredElement; 233 } 234 235 if (element) { 236 element.hovered = true; 237 this._previousHoveredElement = element; 238 239 // Lazily compute tag-specific tooltips. 240 if (element.representedObject && !element.tooltip) 241 element._createTooltipForNode(); 242 } 243 244 WebInspector.highlightDOMNode(element ? element.representedObject.id : 0); 245 }, 246 247 _onmouseout: function(event) 248 { 249 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 250 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element)) 251 return; 252 253 if (this._previousHoveredElement) { 254 this._previousHoveredElement.hovered = false; 255 delete this._previousHoveredElement; 256 } 257 258 WebInspector.highlightDOMNode(0); 259 }, 260 261 populateContextMenu: function(contextMenu, event) 262 { 263 var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI"); 264 if (!listItem || !listItem.treeElement) 265 return false; 266 267 var populated; 268 if (this.showInElementsPanelEnabled) { 269 function focusElement() 270 { 271 WebInspector.panels.elements.switchToAndFocus(listItem.treeElement.representedObject); 272 } 273 contextMenu.appendItem(WebInspector.UIString("Reveal in Elements Panel"), focusElement.bind(this)); 274 populated = true; 275 } else { 276 var href = event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link") || event.target.enclosingNodeOrSelfWithClass("webkit-html-external-link"); 277 var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag"); 278 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 279 if (href) 280 populated = WebInspector.panels.elements.populateHrefContextMenu(contextMenu, event, href); 281 if (tag && listItem.treeElement._populateTagContextMenu) { 282 if (populated) 283 contextMenu.appendSeparator(); 284 listItem.treeElement._populateTagContextMenu(contextMenu, event); 285 populated = true; 286 } else if (textNode && listItem.treeElement._populateTextContextMenu) { 287 if (populated) 288 contextMenu.appendSeparator(); 289 listItem.treeElement._populateTextContextMenu(contextMenu, textNode); 290 populated = true; 291 } 292 } 293 294 return populated; 295 } 296 } 297 298 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype; 299 300 WebInspector.ElementsTreeElement = function(node, elementCloseTag) 301 { 302 this._elementCloseTag = elementCloseTag; 303 var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node); 304 305 // The title will be updated in onattach. 306 TreeElement.call(this, "", node, hasChildrenOverride); 307 308 if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag) 309 this._canAddAttributes = true; 310 this._searchQuery = null; 311 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit; 312 } 313 314 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500; 315 316 // A union of HTML4 and HTML5-Draft elements that explicitly 317 // or implicitly (for HTML5) forbid the closing tag. 318 // FIXME: Revise once HTML5 Final is published. 319 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [ 320 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame", 321 "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source" 322 ].keySet(); 323 324 // These tags we do not allow editing their tag name. 325 WebInspector.ElementsTreeElement.EditTagBlacklist = [ 326 "html", "head", "body" 327 ].keySet(); 328 329 WebInspector.ElementsTreeElement.prototype = { 330 highlightSearchResults: function(searchQuery) 331 { 332 if (this._searchQuery === searchQuery) 333 return; 334 335 if (searchQuery) 336 delete this._searchHighlightedHTML; // A new search query (not clear-the-current-highlighting). 337 338 this._searchQuery = searchQuery; 339 this.updateTitle(true); 340 }, 341 342 get hovered() 343 { 344 return this._hovered; 345 }, 346 347 set hovered(x) 348 { 349 if (this._hovered === x) 350 return; 351 352 this._hovered = x; 353 354 if (this.listItemElement) { 355 if (x) { 356 this.updateSelection(); 357 this.listItemElement.addStyleClass("hovered"); 358 } else { 359 this.listItemElement.removeStyleClass("hovered"); 360 } 361 } 362 }, 363 364 get expandedChildrenLimit() 365 { 366 return this._expandedChildrenLimit; 367 }, 368 369 set expandedChildrenLimit(x) 370 { 371 if (this._expandedChildrenLimit === x) 372 return; 373 374 this._expandedChildrenLimit = x; 375 if (this.treeOutline && !this._updateChildrenInProgress) 376 this._updateChildren(true); 377 }, 378 379 get expandedChildCount() 380 { 381 var count = this.children.length; 382 if (count && this.children[count - 1]._elementCloseTag) 383 count--; 384 if (count && this.children[count - 1].expandAllButton) 385 count--; 386 return count; 387 }, 388 389 showChild: function(index) 390 { 391 if (this._elementCloseTag) 392 return; 393 394 if (index >= this.expandedChildrenLimit) { 395 this._expandedChildrenLimit = index + 1; 396 this._updateChildren(true); 397 } 398 399 // Whether index-th child is visible in the children tree 400 return this.expandedChildCount > index; 401 }, 402 403 _createTooltipForNode: function() 404 { 405 var node = this.representedObject; 406 if (!node.nodeName() || node.nodeName().toLowerCase() !== "img") 407 return; 408 409 function setTooltip(error, result) 410 { 411 if (error || !result || result.type !== "string") 412 return; 413 414 try { 415 var properties = JSON.parse(result.description); 416 var offsetWidth = properties[0]; 417 var offsetHeight = properties[1]; 418 var naturalWidth = properties[2]; 419 var naturalHeight = properties[3]; 420 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth) 421 this.tooltip = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight); 422 else 423 this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight); 424 } catch (e) { 425 console.error(e); 426 } 427 } 428 429 function resolvedNode(object) 430 { 431 if (!object) 432 return; 433 434 object.evaluate("return '[' + this.offsetWidth + ',' + this.offsetHeight + ',' + this.naturalWidth + ',' + this.naturalHeight + ']'", setTooltip.bind(this)); 435 object.release(); 436 } 437 WebInspector.RemoteObject.resolveNode(node, resolvedNode.bind(this)); 438 }, 439 440 updateSelection: function() 441 { 442 var listItemElement = this.listItemElement; 443 if (!listItemElement) 444 return; 445 446 if (document.body.offsetWidth <= 0) { 447 // The stylesheet hasn't loaded yet or the window is closed, 448 // so we can't calculate what is need. Return early. 449 return; 450 } 451 452 if (!this.selectionElement) { 453 this.selectionElement = document.createElement("div"); 454 this.selectionElement.className = "selection selected"; 455 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); 456 } 457 458 this.selectionElement.style.height = listItemElement.offsetHeight + "px"; 459 }, 460 461 onattach: function() 462 { 463 if (this._hovered) { 464 this.updateSelection(); 465 this.listItemElement.addStyleClass("hovered"); 466 } 467 468 this.updateTitle(); 469 470 this._preventFollowingLinksOnDoubleClick(); 471 }, 472 473 _preventFollowingLinksOnDoubleClick: function() 474 { 475 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"); 476 if (!links) 477 return; 478 479 for (var i = 0; i < links.length; ++i) 480 links[i].preventFollowOnDoubleClick = true; 481 }, 482 483 onpopulate: function() 484 { 485 if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag) 486 return; 487 488 this.updateChildren(); 489 }, 490 491 updateChildren: function(fullRefresh) 492 { 493 if (this._elementCloseTag) 494 return; 495 this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh)); 496 }, 497 498 insertChildElement: function(child, index, closingTag) 499 { 500 var newElement = new WebInspector.ElementsTreeElement(child, closingTag); 501 newElement.selectable = this.treeOutline.selectEnabled; 502 this.insertChild(newElement, index); 503 return newElement; 504 }, 505 506 moveChild: function(child, targetIndex) 507 { 508 var wasSelected = child.selected; 509 this.removeChild(child); 510 this.insertChild(child, targetIndex); 511 if (wasSelected) 512 child.select(); 513 }, 514 515 _updateChildren: function(fullRefresh) 516 { 517 if (this._updateChildrenInProgress) 518 return; 519 520 this._updateChildrenInProgress = true; 521 var focusedNode = this.treeOutline.focusedDOMNode; 522 var originalScrollTop; 523 if (fullRefresh) { 524 var treeOutlineContainerElement = this.treeOutline.element.parentNode; 525 originalScrollTop = treeOutlineContainerElement.scrollTop; 526 var selectedTreeElement = this.treeOutline.selectedTreeElement; 527 if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) 528 this.select(); 529 this.removeChildren(); 530 } 531 532 var treeElement = this; 533 var treeChildIndex = 0; 534 var elementToSelect; 535 536 function updateChildrenOfNode(node) 537 { 538 var treeOutline = treeElement.treeOutline; 539 var child = node.firstChild; 540 while (child) { 541 var currentTreeElement = treeElement.children[treeChildIndex]; 542 if (!currentTreeElement || currentTreeElement.representedObject !== child) { 543 // Find any existing element that is later in the children list. 544 var existingTreeElement = null; 545 for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) { 546 if (treeElement.children[i].representedObject === child) { 547 existingTreeElement = treeElement.children[i]; 548 break; 549 } 550 } 551 552 if (existingTreeElement && existingTreeElement.parent === treeElement) { 553 // If an existing element was found and it has the same parent, just move it. 554 treeElement.moveChild(existingTreeElement, treeChildIndex); 555 } else { 556 // No existing element found, insert a new element. 557 if (treeChildIndex < treeElement.expandedChildrenLimit) { 558 var newElement = treeElement.insertChildElement(child, treeChildIndex); 559 if (child === focusedNode) 560 elementToSelect = newElement; 561 if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit) 562 treeElement.expandedChildrenLimit++; 563 } 564 } 565 } 566 567 child = child.nextSibling; 568 ++treeChildIndex; 569 } 570 } 571 572 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent. 573 for (var i = (this.children.length - 1); i >= 0; --i) { 574 var currentChild = this.children[i]; 575 var currentNode = currentChild.representedObject; 576 var currentParentNode = currentNode.parentNode; 577 578 if (currentParentNode === this.representedObject) 579 continue; 580 581 var selectedTreeElement = this.treeOutline.selectedTreeElement; 582 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) 583 this.select(); 584 585 this.removeChildAtIndex(i); 586 } 587 588 updateChildrenOfNode(this.representedObject); 589 this.adjustCollapsedRange(false); 590 591 var lastChild = this.children[this.children.length - 1]; 592 if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag)) 593 this.insertChildElement(this.representedObject, this.children.length, true); 594 595 // We want to restore the original selection and tree scroll position after a full refresh, if possible. 596 if (fullRefresh && elementToSelect) { 597 elementToSelect.select(); 598 if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight) 599 treeOutlineContainerElement.scrollTop = originalScrollTop; 600 } 601 602 delete this._updateChildrenInProgress; 603 }, 604 605 adjustCollapsedRange: function() 606 { 607 // Ensure precondition: only the tree elements for node children are found in the tree 608 // (not the Expand All button or the closing tag). 609 if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent) 610 this.removeChild(this.expandAllButtonElement.__treeElement); 611 612 const node = this.representedObject; 613 if (!node.children) 614 return; 615 const childNodeCount = node.children.length; 616 617 // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom. 618 for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i) 619 this.insertChildElement(node.children[i], i); 620 621 const expandedChildCount = this.expandedChildCount; 622 if (childNodeCount > this.expandedChildCount) { 623 var targetButtonIndex = expandedChildCount; 624 if (!this.expandAllButtonElement) { 625 var item = new TreeElement(null, null, false); 626 item.titleHTML = "<button class=\"show-all-nodes\" value=\"\" />"; 627 item.selectable = false; 628 item.expandAllButton = true; 629 this.insertChild(item, targetButtonIndex); 630 this.expandAllButtonElement = item.listItemElement.firstChild; 631 this.expandAllButtonElement.__treeElement = item; 632 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false); 633 } else if (!this.expandAllButtonElement.__treeElement.parent) 634 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex); 635 this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount); 636 } else if (this.expandAllButtonElement) 637 delete this.expandAllButtonElement; 638 }, 639 640 handleLoadAllChildren: function() 641 { 642 this.expandedChildrenLimit = Math.max(this.representedObject._childNodeCount, this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit); 643 }, 644 645 onexpand: function() 646 { 647 if (this._elementCloseTag) 648 return; 649 650 this.updateTitle(); 651 this.treeOutline.updateSelection(); 652 }, 653 654 oncollapse: function() 655 { 656 if (this._elementCloseTag) 657 return; 658 659 this.updateTitle(); 660 this.treeOutline.updateSelection(); 661 }, 662 663 onreveal: function() 664 { 665 if (this.listItemElement) { 666 var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name"); 667 if (tagSpans.length) 668 tagSpans[0].scrollIntoViewIfNeeded(false); 669 else 670 this.listItemElement.scrollIntoViewIfNeeded(false); 671 } 672 }, 673 674 onselect: function(treeElement, selectedByUser) 675 { 676 this.treeOutline.suppressRevealAndSelect = true; 677 this.treeOutline.focusedDOMNode = this.representedObject; 678 if (selectedByUser) 679 WebInspector.highlightDOMNode(this.representedObject.id); 680 this.updateSelection(); 681 this.treeOutline.suppressRevealAndSelect = false; 682 }, 683 684 ondelete: function() 685 { 686 var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject); 687 startTagTreeElement ? startTagTreeElement.remove() : this.remove(); 688 return true; 689 }, 690 691 onenter: function() 692 { 693 // On Enter or Return start editing the first attribute 694 // or create a new attribute on the selected element. 695 if (this.treeOutline.editing) 696 return false; 697 698 this._startEditing(); 699 700 // prevent a newline from being immediately inserted 701 return true; 702 }, 703 704 selectOnMouseDown: function(event) 705 { 706 TreeElement.prototype.selectOnMouseDown.call(this, event); 707 708 if (this._editing) 709 return; 710 711 if (this.treeOutline.showInElementsPanelEnabled) { 712 WebInspector.showPanel("elements"); 713 WebInspector.panels.elements.focusedDOMNode = this.representedObject; 714 } 715 716 // Prevent selecting the nearest word on double click. 717 if (event.detail >= 2) 718 event.preventDefault(); 719 }, 720 721 ondblclick: function(event) 722 { 723 if (this._editing || this._elementCloseTag) 724 return; 725 726 if (this._startEditingTarget(event.target)) 727 return; 728 729 if (this.hasChildren && !this.expanded) 730 this.expand(); 731 }, 732 733 _insertInLastAttributePosition: function(tag, node) 734 { 735 if (tag.getElementsByClassName("webkit-html-attribute").length > 0) 736 tag.insertBefore(node, tag.lastChild); 737 else { 738 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1]; 739 tag.textContent = ''; 740 tag.appendChild(document.createTextNode('<'+nodeName)); 741 tag.appendChild(node); 742 tag.appendChild(document.createTextNode('>')); 743 } 744 745 this.updateSelection(); 746 }, 747 748 _startEditingTarget: function(eventTarget) 749 { 750 if (this.treeOutline.focusedDOMNode != this.representedObject) 751 return; 752 753 if (this.representedObject.nodeType() != Node.ELEMENT_NODE && this.representedObject.nodeType() != Node.TEXT_NODE) 754 return false; 755 756 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 757 if (textNode) 758 return this._startEditingTextNode(textNode); 759 760 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 761 if (attribute) 762 return this._startEditingAttribute(attribute, eventTarget); 763 764 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name"); 765 if (tagName) 766 return this._startEditingTagName(tagName); 767 768 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute"); 769 if (newAttribute) 770 return this._addNewAttribute(); 771 772 return false; 773 }, 774 775 _populateTagContextMenu: function(contextMenu, event) 776 { 777 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 778 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); 779 780 // Add attribute-related actions. 781 contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this)); 782 if (attribute && !newAttribute) 783 contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target)); 784 contextMenu.appendSeparator(); 785 786 // Add free-form node-related actions. 787 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this)); 788 contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this)); 789 contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this)); 790 791 if (Preferences.nativeInstrumentationEnabled) { 792 // Add debbuging-related actions 793 contextMenu.appendSeparator(); 794 var pane = WebInspector.panels.elements.sidebarPanes.domBreakpoints; 795 pane.populateNodeContextMenu(this.representedObject, contextMenu); 796 } 797 }, 798 799 _populateTextContextMenu: function(contextMenu, textNode) 800 { 801 contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode)); 802 }, 803 804 _startEditing: function() 805 { 806 if (this.treeOutline.focusedDOMNode !== this.representedObject) 807 return; 808 809 var listItem = this._listItemNode; 810 811 if (this._canAddAttributes) { 812 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0]; 813 if (attribute) 814 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]); 815 816 return this._addNewAttribute(); 817 } 818 819 if (this.representedObject.nodeType() === Node.TEXT_NODE) { 820 var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0]; 821 if (textNode) 822 return this._startEditingTextNode(textNode); 823 return; 824 } 825 }, 826 827 _addNewAttribute: function() 828 { 829 // Cannot just convert the textual html into an element without 830 // a parent node. Use a temporary span container for the HTML. 831 var container = document.createElement("span"); 832 container.innerHTML = this._attributeHTML(" ", ""); 833 var attr = container.firstChild; 834 attr.style.marginLeft = "2px"; // overrides the .editing margin rule 835 attr.style.marginRight = "2px"; // overrides the .editing margin rule 836 837 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0]; 838 this._insertInLastAttributePosition(tag, attr); 839 return this._startEditingAttribute(attr, attr); 840 }, 841 842 _triggerEditAttribute: function(attributeName) 843 { 844 var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name"); 845 for (var i = 0, len = attributeElements.length; i < len; ++i) { 846 if (attributeElements[i].textContent === attributeName) { 847 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) { 848 if (elem.nodeType !== Node.ELEMENT_NODE) 849 continue; 850 851 if (elem.hasStyleClass("webkit-html-attribute-value")) 852 return this._startEditingAttribute(elem.parentNode, elem); 853 } 854 } 855 } 856 }, 857 858 _startEditingAttribute: function(attribute, elementForSelection) 859 { 860 if (WebInspector.isBeingEdited(attribute)) 861 return true; 862 863 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; 864 if (!attributeNameElement) 865 return false; 866 867 var attributeName = attributeNameElement.textContent; 868 869 function removeZeroWidthSpaceRecursive(node) 870 { 871 if (node.nodeType === Node.TEXT_NODE) { 872 node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); 873 return; 874 } 875 876 if (node.nodeType !== Node.ELEMENT_NODE) 877 return; 878 879 for (var child = node.firstChild; child; child = child.nextSibling) 880 removeZeroWidthSpaceRecursive(child); 881 } 882 883 // Remove zero-width spaces that were added by nodeTitleInfo. 884 removeZeroWidthSpaceRecursive(attribute); 885 886 this._editing = WebInspector.startEditing(attribute, { 887 context: attributeName, 888 commitHandler: this._attributeEditingCommitted.bind(this), 889 cancelHandler: this._editingCancelled.bind(this) 890 }); 891 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1); 892 893 return true; 894 }, 895 896 _startEditingTextNode: function(textNode) 897 { 898 if (WebInspector.isBeingEdited(textNode)) 899 return true; 900 901 this._editing = WebInspector.startEditing(textNode, { 902 context: null, 903 commitHandler: this._textNodeEditingCommitted.bind(this), 904 cancelHandler: this._editingCancelled.bind(this) 905 }); 906 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1); 907 908 return true; 909 }, 910 911 _startEditingTagName: function(tagNameElement) 912 { 913 if (!tagNameElement) { 914 tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0]; 915 if (!tagNameElement) 916 return false; 917 } 918 919 var tagName = tagNameElement.textContent; 920 if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()]) 921 return false; 922 923 if (WebInspector.isBeingEdited(tagNameElement)) 924 return true; 925 926 var closingTagElement = this._distinctClosingTagElement(); 927 928 function keyupListener(event) 929 { 930 if (closingTagElement) 931 closingTagElement.textContent = "</" + tagNameElement.textContent + ">"; 932 } 933 934 function editingComitted(element, newTagName) 935 { 936 tagNameElement.removeEventListener('keyup', keyupListener, false); 937 this._tagNameEditingCommitted.apply(this, arguments); 938 } 939 940 function editingCancelled() 941 { 942 tagNameElement.removeEventListener('keyup', keyupListener, false); 943 this._editingCancelled.apply(this, arguments); 944 } 945 946 tagNameElement.addEventListener('keyup', keyupListener, false); 947 948 this._editing = WebInspector.startEditing(tagNameElement, { 949 context: tagName, 950 commitHandler: editingComitted.bind(this), 951 cancelHandler: editingCancelled.bind(this) 952 }); 953 window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1); 954 return true; 955 }, 956 957 _startEditingAsHTML: function(commitCallback, error, initialValue) 958 { 959 if (error) 960 return; 961 if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement)) 962 return; 963 964 this._htmlEditElement = document.createElement("div"); 965 this._htmlEditElement.className = "source-code elements-tree-editor"; 966 this._htmlEditElement.textContent = initialValue; 967 968 // Hide header items. 969 var child = this.listItemElement.firstChild; 970 while (child) { 971 child.style.display = "none"; 972 child = child.nextSibling; 973 } 974 // Hide children item. 975 if (this._childrenListNode) 976 this._childrenListNode.style.display = "none"; 977 // Append editor. 978 this.listItemElement.appendChild(this._htmlEditElement); 979 980 this.updateSelection(); 981 982 function commit() 983 { 984 commitCallback(this._htmlEditElement.textContent); 985 dispose.call(this); 986 } 987 988 function dispose() 989 { 990 delete this._editing; 991 992 // Remove editor. 993 this.listItemElement.removeChild(this._htmlEditElement); 994 delete this._htmlEditElement; 995 // Unhide children item. 996 if (this._childrenListNode) 997 this._childrenListNode.style.removeProperty("display"); 998 // Unhide header items. 999 var child = this.listItemElement.firstChild; 1000 while (child) { 1001 child.style.removeProperty("display"); 1002 child = child.nextSibling; 1003 } 1004 1005 this.updateSelection(); 1006 } 1007 1008 this._editing = WebInspector.startEditing(this._htmlEditElement, { 1009 context: null, 1010 commitHandler: commit.bind(this), 1011 cancelHandler: dispose.bind(this), 1012 multiline: true 1013 }); 1014 }, 1015 1016 _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection) 1017 { 1018 delete this._editing; 1019 1020 // Before we do anything, determine where we should move 1021 // next based on the current element's settings 1022 var moveToAttribute, moveToTagName, moveToNewAttribute; 1023 if (moveDirection) { 1024 var found = false; 1025 1026 // Search for the attribute's position, and then decide where to move to. 1027 var attributes = this.representedObject.attributes(); 1028 for (var i = 0; i < attributes.length; ++i) { 1029 if (attributes[i].name === attributeName) { 1030 found = true; 1031 if (moveDirection === "backward") { 1032 if (i === 0) 1033 moveToTagName = true; 1034 else 1035 moveToAttribute = attributes[i - 1].name; 1036 } else if (moveDirection === "forward") { 1037 if (i === attributes.length - 1) 1038 moveToNewAttribute = true; 1039 else 1040 moveToAttribute = attributes[i + 1].name; 1041 } 1042 } 1043 } 1044 1045 // Moving From the "New Attribute" position. 1046 if (!found) { 1047 if (moveDirection === "backward" && attributes.length > 0) 1048 moveToAttribute = attributes[attributes.length - 1].name; 1049 else if (moveDirection === "forward") { 1050 if (!/^\s*$/.test(newText)) 1051 moveToNewAttribute = true; 1052 else 1053 moveToTagName = true; 1054 } 1055 } 1056 } 1057 1058 function moveToNextAttributeIfNeeded() 1059 { 1060 // Cleanup empty new attribute sections. 1061 if (element.textContent.trim().length === 0) 1062 element.parentNode.removeChild(element); 1063 1064 // Make the move. 1065 if (moveToAttribute) 1066 this._triggerEditAttribute(moveToAttribute); 1067 else if (moveToNewAttribute) 1068 this._addNewAttribute(); 1069 else if (moveToTagName) 1070 this._startEditingTagName(); 1071 } 1072 1073 function regenerateStyledAttribute(name, value) 1074 { 1075 var previous = element.previousSibling; 1076 if (!previous || previous.nodeType !== Node.TEXT_NODE) 1077 element.parentNode.insertBefore(document.createTextNode(" "), element); 1078 element.outerHTML = this._attributeHTML(name, value); 1079 } 1080 1081 var parseContainerElement = document.createElement("span"); 1082 parseContainerElement.innerHTML = "<span " + newText + "></span>"; 1083 var parseElement = parseContainerElement.firstChild; 1084 1085 if (!parseElement) { 1086 this._editingCancelled(element, attributeName); 1087 moveToNextAttributeIfNeeded.call(this); 1088 return; 1089 } 1090 1091 if (!parseElement.hasAttributes()) { 1092 this.representedObject.removeAttribute(attributeName, this.updateTitle.bind(this)); 1093 this.treeOutline.focusedNodeChanged(true); 1094 moveToNextAttributeIfNeeded.call(this); 1095 return; 1096 } 1097 1098 var foundOriginalAttribute = false; 1099 for (var i = 0; i < parseElement.attributes.length; ++i) { 1100 var attr = parseElement.attributes[i]; 1101 foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName; 1102 try { 1103 this.representedObject.setAttribute(attr.name, attr.value, this.updateTitle.bind(this)); 1104 regenerateStyledAttribute.call(this, attr.name, attr.value); 1105 } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can) 1106 } 1107 1108 if (!foundOriginalAttribute) 1109 this.representedObject.removeAttribute(attributeName, this.updateTitle.bind(this)); 1110 1111 this.treeOutline.focusedNodeChanged(true); 1112 1113 moveToNextAttributeIfNeeded.call(this); 1114 }, 1115 1116 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection) 1117 { 1118 delete this._editing; 1119 var self = this; 1120 1121 function cancel() 1122 { 1123 var closingTagElement = self._distinctClosingTagElement(); 1124 if (closingTagElement) 1125 closingTagElement.textContent = "</" + tagName + ">"; 1126 1127 self._editingCancelled(element, tagName); 1128 moveToNextAttributeIfNeeded.call(self); 1129 } 1130 1131 function moveToNextAttributeIfNeeded() 1132 { 1133 if (moveDirection !== "forward") { 1134 this._addNewAttribute(); 1135 return; 1136 } 1137 1138 var attributes = this.representedObject.attributes(); 1139 if (attributes.length > 0) 1140 this._triggerEditAttribute(attributes[0].name); 1141 else 1142 this._addNewAttribute(); 1143 } 1144 1145 newText = newText.trim(); 1146 if (newText === oldText) { 1147 cancel(); 1148 return; 1149 } 1150 1151 var treeOutline = this.treeOutline; 1152 var wasExpanded = this.expanded; 1153 1154 function changeTagNameCallback(error, nodeId) 1155 { 1156 if (error || !nodeId) { 1157 cancel(); 1158 return; 1159 } 1160 1161 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. 1162 WebInspector.panels.elements.updateModifiedNodes(); 1163 1164 WebInspector.updateFocusedNode(nodeId); 1165 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId)); 1166 if (wasExpanded) 1167 newTreeItem.expand(); 1168 1169 moveToNextAttributeIfNeeded.call(newTreeItem); 1170 } 1171 1172 this.representedObject.setNodeName(newText, changeTagNameCallback); 1173 }, 1174 1175 _textNodeEditingCommitted: function(element, newText) 1176 { 1177 delete this._editing; 1178 1179 var textNode; 1180 if (this.representedObject.nodeType() === Node.ELEMENT_NODE) { 1181 // We only show text nodes inline in elements if the element only 1182 // has a single child, and that child is a text node. 1183 textNode = this.representedObject.firstChild; 1184 } else if (this.representedObject.nodeType() == Node.TEXT_NODE) 1185 textNode = this.representedObject; 1186 1187 textNode.setNodeValue(newText, this.updateTitle.bind(this)); 1188 }, 1189 1190 _editingCancelled: function(element, context) 1191 { 1192 delete this._editing; 1193 1194 // Need to restore attributes structure. 1195 this.updateTitle(); 1196 }, 1197 1198 _distinctClosingTagElement: function() 1199 { 1200 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM 1201 1202 // For an expanded element, it will be the last element with class "close" 1203 // in the child element list. 1204 if (this.expanded) { 1205 var closers = this._childrenListNode.querySelectorAll(".close"); 1206 return closers[closers.length-1]; 1207 } 1208 1209 // Remaining cases are single line non-expanded elements with a closing 1210 // tag, or HTML elements without a closing tag (such as <br>). Return 1211 // null in the case where there isn't a closing tag. 1212 var tags = this.listItemElement.getElementsByClassName("webkit-html-tag"); 1213 return (tags.length === 1 ? null : tags[tags.length-1]); 1214 }, 1215 1216 updateTitle: function(onlySearchQueryChanged) 1217 { 1218 // If we are editing, return early to prevent canceling the edit. 1219 // After editing is committed updateTitle will be called. 1220 if (this._editing) 1221 return; 1222 1223 if (onlySearchQueryChanged && this._normalHTML) 1224 this.titleHTML = this._normalHTML; 1225 else { 1226 delete this._normalHTML; 1227 this.titleHTML = "<span class=\"highlight\">" + this._nodeTitleInfo(WebInspector.linkifyURL).titleHTML + "</span>"; 1228 } 1229 1230 delete this.selectionElement; 1231 this.updateSelection(); 1232 this._preventFollowingLinksOnDoubleClick(); 1233 this._highlightSearchResults(); 1234 }, 1235 1236 _attributeHTML: function(name, value, node, linkify) 1237 { 1238 var hasText = (value.length > 0); 1239 var html = "<span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + name.escapeHTML() + "</span>"; 1240 1241 if (hasText) 1242 html += "=​\""; 1243 1244 if (linkify && (name === "src" || name === "href")) { 1245 var rewrittenHref = WebInspector.resourceURLForRelatedNode(node, value); 1246 value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B"); 1247 html += linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"); 1248 } else { 1249 value = value.escapeHTML().replace(/([\/;:\)\]\}])/g, "$1​"); 1250 html += "<span class=\"webkit-html-attribute-value\">" + value + "</span>"; 1251 } 1252 1253 if (hasText) 1254 html += "\""; 1255 1256 html += "</span>"; 1257 return html; 1258 }, 1259 1260 _tagHTML: function(tagName, isClosingTag, isDistinctTreeElement, linkify) 1261 { 1262 var node = this.representedObject; 1263 var result = "<span class=\"webkit-html-tag" + (isClosingTag && isDistinctTreeElement ? " close" : "") + "\"><"; 1264 result += "<span " + (isClosingTag ? "" : "class=\"webkit-html-tag-name\"") + ">" + (isClosingTag ? "/" : "") + tagName + "</span>"; 1265 if (!isClosingTag && node.hasAttributes()) { 1266 var attributes = node.attributes(); 1267 for (var i = 0; i < attributes.length; ++i) { 1268 var attr = attributes[i]; 1269 result += " " + this._attributeHTML(attr.name, attr.value, node, linkify); 1270 } 1271 } 1272 result += "></span>​"; 1273 1274 return result; 1275 }, 1276 1277 _nodeTitleInfo: function(linkify) 1278 { 1279 var node = this.representedObject; 1280 var info = {titleHTML: "", hasChildren: this.hasChildren}; 1281 1282 switch (node.nodeType()) { 1283 case Node.DOCUMENT_NODE: 1284 info.titleHTML = "Document"; 1285 break; 1286 1287 case Node.DOCUMENT_FRAGMENT_NODE: 1288 info.titleHTML = "Document Fragment"; 1289 break; 1290 1291 case Node.ATTRIBUTE_NODE: 1292 var value = node.value || "\u200B"; // Zero width space to force showing an empty value. 1293 info.titleHTML = this._attributeHTML(node.name, value); 1294 break; 1295 1296 case Node.ELEMENT_NODE: 1297 var tagName = this.treeOutline.nodeNameToCorrectCase(node.nodeName()).escapeHTML(); 1298 if (this._elementCloseTag) { 1299 info.titleHTML = this._tagHTML(tagName, true, true); 1300 info.hasChildren = false; 1301 break; 1302 } 1303 1304 var titleHTML = this._tagHTML(tagName, false, false, linkify); 1305 1306 var textChild = this._singleTextChild(node); 1307 var showInlineText = textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength; 1308 1309 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) { 1310 if (this.hasChildren) 1311 titleHTML += "<span class=\"webkit-html-text-node\">…</span>​"; 1312 titleHTML += this._tagHTML(tagName, true, false); 1313 } 1314 1315 // If this element only has a single child that is a text node, 1316 // just show that text and the closing tag inline rather than 1317 // create a subtree for them 1318 if (showInlineText) { 1319 titleHTML += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue().escapeHTML() + "</span>​" + this._tagHTML(tagName, true, false); 1320 info.hasChildren = false; 1321 } 1322 info.titleHTML = titleHTML; 1323 break; 1324 1325 case Node.TEXT_NODE: 1326 if (isNodeWhitespace.call(node)) 1327 info.titleHTML = "(whitespace)"; 1328 else { 1329 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") { 1330 var newNode = document.createElement("span"); 1331 newNode.textContent = node.nodeValue(); 1332 1333 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript"); 1334 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode); 1335 1336 info.titleHTML = "<span class=\"webkit-html-text-node webkit-html-js-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>"; 1337 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") { 1338 var newNode = document.createElement("span"); 1339 newNode.textContent = node.nodeValue(); 1340 1341 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css"); 1342 cssSyntaxHighlighter.syntaxHighlightNode(newNode); 1343 1344 info.titleHTML = "<span class=\"webkit-html-text-node webkit-html-css-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>"; 1345 } else 1346 info.titleHTML = "\"<span class=\"webkit-html-text-node\">" + node.nodeValue().escapeHTML() + "</span>\""; 1347 } 1348 break; 1349 1350 case Node.COMMENT_NODE: 1351 info.titleHTML = "<span class=\"webkit-html-comment\"><!--" + node.nodeValue().escapeHTML() + "--></span>"; 1352 break; 1353 1354 case Node.DOCUMENT_TYPE_NODE: 1355 var titleHTML = "<span class=\"webkit-html-doctype\"><!DOCTYPE " + node.nodeName(); 1356 if (node.publicId) { 1357 titleHTML += " PUBLIC \"" + node.publicId + "\""; 1358 if (node.systemId) 1359 titleHTML += " \"" + node.systemId + "\""; 1360 } else if (node.systemId) 1361 titleHTML += " SYSTEM \"" + node.systemId + "\""; 1362 if (node.internalSubset) 1363 titleHTML += " [" + node.internalSubset + "]"; 1364 titleHTML += "></span>"; 1365 info.titleHTML = titleHTML; 1366 break; 1367 1368 case Node.CDATA_SECTION_NODE: 1369 info.titleHTML = "<span class=\"webkit-html-text-node\"><![CDATA[" + node.nodeValue().escapeHTML() + "]]></span>"; 1370 break; 1371 default: 1372 info.titleHTML = this.treeOutline.nodeNameToCorrectCase(node.nodeName()).collapseWhitespace().escapeHTML(); 1373 } 1374 1375 return info; 1376 }, 1377 1378 _singleTextChild: function(node) 1379 { 1380 if (!node) 1381 return null; 1382 1383 var firstChild = node.firstChild; 1384 if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE) 1385 return null; 1386 1387 var sibling = firstChild.nextSibling; 1388 return sibling ? null : firstChild; 1389 }, 1390 1391 _showInlineText: function(node) 1392 { 1393 if (node.nodeType() === Node.ELEMENT_NODE) { 1394 var textChild = this._singleTextChild(node); 1395 if (textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength) 1396 return true; 1397 } 1398 return false; 1399 }, 1400 1401 remove: function() 1402 { 1403 var parentElement = this.parent; 1404 if (!parentElement) 1405 return; 1406 1407 var self = this; 1408 function removeNodeCallback(error, removedNodeId) 1409 { 1410 if (error) 1411 return; 1412 1413 parentElement.removeChild(self); 1414 parentElement.adjustCollapsedRange(true); 1415 } 1416 1417 this.representedObject.removeNode(removeNodeCallback); 1418 }, 1419 1420 _editAsHTML: function() 1421 { 1422 var treeOutline = this.treeOutline; 1423 var node = this.representedObject; 1424 var wasExpanded = this.expanded; 1425 1426 function selectNode(error, nodeId) 1427 { 1428 if (error || !nodeId) 1429 return; 1430 1431 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. 1432 WebInspector.panels.elements.updateModifiedNodes(); 1433 1434 WebInspector.updateFocusedNode(nodeId); 1435 if (wasExpanded) { 1436 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId)); 1437 if (newTreeItem) 1438 newTreeItem.expand(); 1439 } 1440 } 1441 1442 function commitChange(value) 1443 { 1444 node.setOuterHTML(value, selectNode); 1445 } 1446 1447 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange)); 1448 }, 1449 1450 _copyHTML: function() 1451 { 1452 this.representedObject.copyNode(); 1453 }, 1454 1455 _highlightSearchResults: function() 1456 { 1457 if (!this._searchQuery) 1458 return; 1459 if (this._searchHighlightedHTML) { 1460 this.listItemElement.innerHTML = this._searchHighlightedHTML; 1461 return; 1462 } 1463 1464 if (!this._normalHTML) 1465 this._normalHTML = this.titleHTML; 1466 1467 var text = this.listItemElement.textContent; 1468 var regexObject = createSearchRegex(this._searchQuery, "g"); 1469 1470 var offset = 0; 1471 var match = regexObject.exec(text); 1472 var matchRanges = []; 1473 while (match) { 1474 matchRanges.push({ offset: match.index, length: match[0].length }); 1475 match = regexObject.exec(text); 1476 } 1477 1478 // Fall back for XPath, etc. matches. 1479 if (!matchRanges.length) 1480 matchRanges.push({ offset: 0, length: text.length }); 1481 1482 highlightSearchResults(this.listItemElement, matchRanges); 1483 this._searchHighlightedHTML = this.listItemElement.innerHTML; 1484 } 1485 } 1486 1487 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype; 1488