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 this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true); 46 this.element.addEventListener("keydown", this._keyDown.bind(this), true); 47 } 48 49 WebInspector.ElementsTreeOutline.prototype = { 50 get rootDOMNode() 51 { 52 return this._rootDOMNode; 53 }, 54 55 set rootDOMNode(x) 56 { 57 if (this._rootDOMNode === x) 58 return; 59 60 this._rootDOMNode = x; 61 62 this.update(); 63 }, 64 65 get focusedDOMNode() 66 { 67 return this._focusedDOMNode; 68 }, 69 70 set focusedDOMNode(x) 71 { 72 if (this._focusedDOMNode === x) { 73 this.revealAndSelectNode(x); 74 return; 75 } 76 77 this._focusedDOMNode = x; 78 79 this.revealAndSelectNode(x); 80 81 // The revealAndSelectNode() method might find a different element if there is inlined text, 82 // and the select() call would change the focusedDOMNode and reenter this setter. So to 83 // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same 84 // node as the one passed in. 85 if (this._focusedDOMNode === x) { 86 this.focusedNodeChanged(); 87 88 if (x && !this.suppressSelectHighlight) { 89 InspectorBackend.highlightDOMNode(x.id); 90 91 if ("_restorePreviousHighlightNodeTimeout" in this) 92 clearTimeout(this._restorePreviousHighlightNodeTimeout); 93 94 function restoreHighlightToHoveredNode() 95 { 96 var hoveredNode = WebInspector.hoveredDOMNode; 97 if (hoveredNode) 98 InspectorBackend.highlightDOMNode(hoveredNode.id); 99 else 100 InspectorBackend.hideDOMNodeHighlight(); 101 } 102 103 this._restorePreviousHighlightNodeTimeout = setTimeout(restoreHighlightToHoveredNode, 2000); 104 } 105 } 106 }, 107 108 update: function() 109 { 110 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; 111 112 this.removeChildren(); 113 114 if (!this.rootDOMNode) 115 return; 116 117 var treeElement; 118 if (this.includeRootDOMNode) { 119 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode); 120 treeElement.selectable = this.selectEnabled; 121 this.appendChild(treeElement); 122 } else { 123 // FIXME: this could use findTreeElement to reuse a tree element if it already exists 124 var node = this.rootDOMNode.firstChild; 125 while (node) { 126 treeElement = new WebInspector.ElementsTreeElement(node); 127 treeElement.selectable = this.selectEnabled; 128 this.appendChild(treeElement); 129 node = node.nextSibling; 130 } 131 } 132 133 if (selectedNode) 134 this.revealAndSelectNode(selectedNode); 135 }, 136 137 updateSelection: function() 138 { 139 if (!this.selectedTreeElement) 140 return; 141 var element = this.treeOutline.selectedTreeElement; 142 element.updateSelection(); 143 }, 144 145 focusedNodeChanged: function(forceUpdate) {}, 146 147 findTreeElement: function(node) 148 { 149 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode); 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, isAncestorNode, parentNode); 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 _keyDown: function(event) 196 { 197 if (event.target !== this.treeOutline.element) 198 return; 199 200 var selectedElement = this.selectedTreeElement; 201 if (!selectedElement) 202 return; 203 204 if (event.keyCode === WebInspector.KeyboardShortcut.KeyCodes.Backspace || 205 event.keyCode === WebInspector.KeyboardShortcut.KeyCodes.Delete) { 206 selectedElement.remove(); 207 event.preventDefault(); 208 event.stopPropagation(); 209 return; 210 } 211 212 // On Enter or Return start editing the first attribute 213 // or create a new attribute on the selected element. 214 if (isEnterKey(event)) { 215 if (this._editing) 216 return; 217 218 selectedElement._startEditing(); 219 220 // prevent a newline from being immediately inserted 221 event.preventDefault(); 222 event.stopPropagation(); 223 return; 224 } 225 }, 226 227 _onmousedown: function(event) 228 { 229 var element = this._treeElementFromEvent(event); 230 231 if (!element || element.isEventWithinDisclosureTriangle(event)) 232 return; 233 234 element.select(); 235 }, 236 237 _onmousemove: function(event) 238 { 239 var element = this._treeElementFromEvent(event); 240 if (element && this._previousHoveredElement === element) 241 return; 242 243 if (this._previousHoveredElement) { 244 this._previousHoveredElement.hovered = false; 245 delete this._previousHoveredElement; 246 } 247 248 if (element && !element.elementCloseTag) { 249 element.hovered = true; 250 this._previousHoveredElement = element; 251 } 252 253 WebInspector.hoveredDOMNode = (element && !element.elementCloseTag ? element.representedObject : null); 254 }, 255 256 _onmouseout: function(event) 257 { 258 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 259 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element)) 260 return; 261 262 if (this._previousHoveredElement) { 263 this._previousHoveredElement.hovered = false; 264 delete this._previousHoveredElement; 265 } 266 267 WebInspector.hoveredDOMNode = null; 268 }, 269 270 _contextMenuEventFired: function(event) 271 { 272 var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI"); 273 if (!listItem || !listItem.treeElement) 274 return; 275 276 var contextMenu = new WebInspector.ContextMenu(); 277 278 var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag"); 279 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 280 if (tag) 281 listItem.treeElement._populateTagContextMenu(contextMenu, event); 282 else if (textNode) 283 listItem.treeElement._populateTextContextMenu(contextMenu, textNode); 284 contextMenu.show(event); 285 } 286 } 287 288 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype; 289 290 WebInspector.ElementsTreeElement = function(node) 291 { 292 var hasChildrenOverride = node.hasChildNodes() && !this._showInlineText(node); 293 294 // The title will be updated in onattach. 295 TreeElement.call(this, "", node, hasChildrenOverride); 296 297 if (this.representedObject.nodeType == Node.ELEMENT_NODE) 298 this._canAddAttributes = true; 299 } 300 301 WebInspector.ElementsTreeElement.prototype = { 302 get highlighted() 303 { 304 return this._highlighted; 305 }, 306 307 set highlighted(x) 308 { 309 if (this._highlighted === x) 310 return; 311 312 this._highlighted = x; 313 314 if (this.listItemElement) { 315 if (x) 316 this.listItemElement.addStyleClass("highlighted"); 317 else 318 this.listItemElement.removeStyleClass("highlighted"); 319 } 320 }, 321 322 get hovered() 323 { 324 return this._hovered; 325 }, 326 327 set hovered(x) 328 { 329 if (this._hovered === x) 330 return; 331 332 this._hovered = x; 333 334 if (this.listItemElement) { 335 if (x) { 336 this.updateSelection(); 337 this.listItemElement.addStyleClass("hovered"); 338 } else { 339 this.listItemElement.removeStyleClass("hovered"); 340 } 341 } 342 }, 343 344 createTooltipForImageNode: function(node, callback) 345 { 346 function createTooltipThenCallback(properties) 347 { 348 if (!properties) { 349 callback(); 350 return; 351 } 352 353 var tooltipText = null; 354 if (properties.offsetHeight === properties.naturalHeight && properties.offsetWidth === properties.naturalWidth) 355 tooltipText = WebInspector.UIString("%d %d pixels", properties.offsetWidth, properties.offsetHeight); 356 else 357 tooltipText = WebInspector.UIString("%d %d pixels (Natural: %d %d pixels)", properties.offsetWidth, properties.offsetHeight, properties.naturalWidth, properties.naturalHeight); 358 callback(tooltipText); 359 } 360 var objectProxy = new WebInspector.ObjectProxy(node.injectedScriptId, node.id); 361 WebInspector.ObjectProxy.getPropertiesAsync(objectProxy, ["naturalHeight", "naturalWidth", "offsetHeight", "offsetWidth"], createTooltipThenCallback); 362 }, 363 364 updateSelection: function() 365 { 366 var listItemElement = this.listItemElement; 367 if (!listItemElement) 368 return; 369 370 if (document.body.offsetWidth <= 0) { 371 // The stylesheet hasn't loaded yet or the window is closed, 372 // so we can't calculate what is need. Return early. 373 return; 374 } 375 376 if (!this.selectionElement) { 377 this.selectionElement = document.createElement("div"); 378 this.selectionElement.className = "selection selected"; 379 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); 380 } 381 382 this.selectionElement.style.height = listItemElement.offsetHeight + "px"; 383 }, 384 385 onattach: function() 386 { 387 this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false); 388 389 if (this._highlighted) 390 this.listItemElement.addStyleClass("highlighted"); 391 392 if (this._hovered) { 393 this.updateSelection(); 394 this.listItemElement.addStyleClass("hovered"); 395 } 396 397 this.updateTitle(); 398 399 this._preventFollowingLinksOnDoubleClick(); 400 }, 401 402 _preventFollowingLinksOnDoubleClick: function() 403 { 404 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"); 405 if (!links) 406 return; 407 408 for (var i = 0; i < links.length; ++i) 409 links[i].preventFollowOnDoubleClick = true; 410 }, 411 412 onpopulate: function() 413 { 414 if (this.children.length || this._showInlineText(this.representedObject)) 415 return; 416 417 this.updateChildren(); 418 }, 419 420 updateChildren: function(fullRefresh) 421 { 422 WebInspector.domAgent.getChildNodesAsync(this.representedObject, this._updateChildren.bind(this, fullRefresh)); 423 }, 424 425 _updateChildren: function(fullRefresh) 426 { 427 if (fullRefresh) { 428 var selectedTreeElement = this.treeOutline.selectedTreeElement; 429 if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) 430 this.select(); 431 this.removeChildren(); 432 } 433 434 var treeElement = this; 435 var treeChildIndex = 0; 436 437 function updateChildrenOfNode(node) 438 { 439 var treeOutline = treeElement.treeOutline; 440 var child = node.firstChild; 441 while (child) { 442 var currentTreeElement = treeElement.children[treeChildIndex]; 443 if (!currentTreeElement || currentTreeElement.representedObject !== child) { 444 // Find any existing element that is later in the children list. 445 var existingTreeElement = null; 446 for (var i = (treeChildIndex + 1); i < treeElement.children.length; ++i) { 447 if (treeElement.children[i].representedObject === child) { 448 existingTreeElement = treeElement.children[i]; 449 break; 450 } 451 } 452 453 if (existingTreeElement && existingTreeElement.parent === treeElement) { 454 // If an existing element was found and it has the same parent, just move it. 455 var wasSelected = existingTreeElement.selected; 456 treeElement.removeChild(existingTreeElement); 457 treeElement.insertChild(existingTreeElement, treeChildIndex); 458 if (wasSelected) 459 existingTreeElement.select(); 460 } else { 461 // No existing element found, insert a new element. 462 var newElement = new WebInspector.ElementsTreeElement(child); 463 newElement.selectable = treeOutline.selectEnabled; 464 treeElement.insertChild(newElement, treeChildIndex); 465 } 466 } 467 468 child = child.nextSibling; 469 ++treeChildIndex; 470 } 471 } 472 473 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent. 474 for (var i = (this.children.length - 1); i >= 0; --i) { 475 if ("elementCloseTag" in this.children[i]) 476 continue; 477 478 var currentChild = this.children[i]; 479 var currentNode = currentChild.representedObject; 480 var currentParentNode = currentNode.parentNode; 481 482 if (currentParentNode === this.representedObject) 483 continue; 484 485 var selectedTreeElement = this.treeOutline.selectedTreeElement; 486 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) 487 this.select(); 488 489 this.removeChildAtIndex(i); 490 } 491 492 updateChildrenOfNode(this.representedObject); 493 494 var lastChild = this.children[this.children.length - 1]; 495 if (this.representedObject.nodeType == Node.ELEMENT_NODE && (!lastChild || !lastChild.elementCloseTag)) { 496 var title = "<span class=\"webkit-html-tag close\"></" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "></span>"; 497 var item = new TreeElement(title, null, false); 498 item.selectable = false; 499 item.elementCloseTag = true; 500 this.appendChild(item); 501 } 502 }, 503 504 onexpand: function() 505 { 506 this.treeOutline.updateSelection(); 507 }, 508 509 oncollapse: function() 510 { 511 this.treeOutline.updateSelection(); 512 }, 513 514 onreveal: function() 515 { 516 if (this.listItemElement) 517 this.listItemElement.scrollIntoViewIfNeeded(false); 518 }, 519 520 onselect: function() 521 { 522 this.treeOutline.focusedDOMNode = this.representedObject; 523 this.updateSelection(); 524 }, 525 526 onmousedown: function(event) 527 { 528 if (this._editing) 529 return; 530 531 if (this.isEventWithinDisclosureTriangle(event)) 532 return; 533 534 if (this.treeOutline.showInElementsPanelEnabled) { 535 WebInspector.showElementsPanel(); 536 WebInspector.panels.elements.focusedDOMNode = this.representedObject; 537 } 538 539 // Prevent selecting the nearest word on double click. 540 if (event.detail >= 2) 541 event.preventDefault(); 542 }, 543 544 ondblclick: function(event) 545 { 546 if (this._editing) 547 return; 548 549 if (this._startEditingFromEvent(event)) 550 return; 551 552 if (this.hasChildren && !this.expanded) 553 this.expand(); 554 }, 555 556 _insertInLastAttributePosition: function(tag, node) 557 { 558 if (tag.getElementsByClassName("webkit-html-attribute").length > 0) 559 tag.insertBefore(node, tag.lastChild); 560 else { 561 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1]; 562 tag.textContent = ''; 563 tag.appendChild(document.createTextNode('<'+nodeName)); 564 tag.appendChild(node); 565 tag.appendChild(document.createTextNode('>')); 566 } 567 568 this.updateSelection(); 569 }, 570 571 _startEditingFromEvent: function(event) 572 { 573 if (this.treeOutline.focusedDOMNode != this.representedObject) 574 return; 575 576 if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE) 577 return false; 578 579 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 580 if (textNode) 581 return this._startEditingTextNode(textNode); 582 583 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 584 if (attribute) 585 return this._startEditingAttribute(attribute, event.target); 586 587 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); 588 if (newAttribute) 589 return this._addNewAttribute(); 590 591 return false; 592 }, 593 594 _populateTagContextMenu: function(contextMenu, event) 595 { 596 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 597 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); 598 599 // Add attribute-related actions. 600 contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this)); 601 if (attribute && !newAttribute) 602 contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target)); 603 contextMenu.appendSeparator(); 604 605 // Add node-related actions. 606 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this)); 607 contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this)); 608 contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this)); 609 }, 610 611 _populateTextContextMenu: function(contextMenu, textNode) 612 { 613 contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode)); 614 }, 615 616 _startEditing: function() 617 { 618 if (this.treeOutline.focusedDOMNode !== this.representedObject) 619 return; 620 621 var listItem = this._listItemNode; 622 623 if (this._canAddAttributes) { 624 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0]; 625 if (attribute) 626 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]); 627 628 return this._addNewAttribute(); 629 } 630 631 if (this.representedObject.nodeType === Node.TEXT_NODE) { 632 var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0]; 633 if (textNode) 634 return this._startEditingTextNode(textNode); 635 return; 636 } 637 }, 638 639 _addNewAttribute: function() 640 { 641 var attr = document.createElement("span"); 642 attr.className = "webkit-html-attribute"; 643 attr.style.marginLeft = "2px"; // overrides the .editing margin rule 644 attr.style.marginRight = "2px"; // overrides the .editing margin rule 645 var name = document.createElement("span"); 646 name.className = "webkit-html-attribute-name new-attribute"; 647 name.textContent = " "; 648 var value = document.createElement("span"); 649 value.className = "webkit-html-attribute-value"; 650 attr.appendChild(name); 651 attr.appendChild(value); 652 653 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0]; 654 this._insertInLastAttributePosition(tag, attr); 655 return this._startEditingAttribute(attr, attr); 656 }, 657 658 _triggerEditAttribute: function(attributeName) 659 { 660 var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name"); 661 for (var i = 0, len = attributeElements.length; i < len; ++i) { 662 if (attributeElements[i].textContent === attributeName) { 663 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) { 664 if (elem.nodeType !== Node.ELEMENT_NODE) 665 continue; 666 667 if (elem.hasStyleClass("webkit-html-attribute-value")) 668 return this._startEditingAttribute(attributeElements[i].parentNode, elem); 669 } 670 } 671 } 672 }, 673 674 _startEditingAttribute: function(attribute, elementForSelection) 675 { 676 if (WebInspector.isBeingEdited(attribute)) 677 return true; 678 679 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; 680 if (!attributeNameElement) 681 return false; 682 683 var attributeName = attributeNameElement.innerText; 684 685 function removeZeroWidthSpaceRecursive(node) 686 { 687 if (node.nodeType === Node.TEXT_NODE) { 688 node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); 689 return; 690 } 691 692 if (node.nodeType !== Node.ELEMENT_NODE) 693 return; 694 695 for (var child = node.firstChild; child; child = child.nextSibling) 696 removeZeroWidthSpaceRecursive(child); 697 } 698 699 // Remove zero-width spaces that were added by nodeTitleInfo. 700 removeZeroWidthSpaceRecursive(attribute); 701 702 this._editing = true; 703 704 WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName); 705 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1); 706 707 return true; 708 }, 709 710 _startEditingTextNode: function(textNode) 711 { 712 if (WebInspector.isBeingEdited(textNode)) 713 return true; 714 715 this._editing = true; 716 717 WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this)); 718 window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1); 719 720 return true; 721 }, 722 723 _startEditingAsHTML: function(commitCallback, initialValue) 724 { 725 if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement)) 726 return true; 727 728 this._editing = true; 729 730 this._htmlEditElement = document.createElement("div"); 731 this._htmlEditElement.className = "source-code elements-tree-editor"; 732 this._htmlEditElement.textContent = initialValue; 733 734 // Hide header items. 735 var child = this.listItemElement.firstChild; 736 while (child) { 737 child.style.display = "none"; 738 child = child.nextSibling; 739 } 740 // Hide children item. 741 if (this._childrenListNode) 742 this._childrenListNode.style.display = "none"; 743 // Append editor. 744 this.listItemElement.appendChild(this._htmlEditElement); 745 746 this.updateSelection(); 747 748 function commit() 749 { 750 commitCallback(this._htmlEditElement.textContent); 751 dispose.call(this); 752 } 753 754 function dispose() 755 { 756 delete this._editing; 757 758 // Remove editor. 759 this.listItemElement.removeChild(this._htmlEditElement); 760 delete this._htmlEditElement; 761 // Unhide children item. 762 if (this._childrenListNode) 763 this._childrenListNode.style.removeProperty("display"); 764 // Unhide header items. 765 var child = this.listItemElement.firstChild; 766 while (child) { 767 child.style.removeProperty("display"); 768 child = child.nextSibling; 769 } 770 771 this.updateSelection(); 772 } 773 774 WebInspector.startEditing(this._htmlEditElement, commit.bind(this), dispose.bind(this), null, true); 775 }, 776 777 _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection) 778 { 779 delete this._editing; 780 781 // Before we do anything, determine where we should move 782 // next based on the current element's settings 783 var moveToAttribute; 784 var newAttribute; 785 if (moveDirection) { 786 var found = false; 787 var attributes = this.representedObject.attributes; 788 for (var i = 0, len = attributes.length; i < len; ++i) { 789 if (attributes[i].name === attributeName) { 790 found = true; 791 if (moveDirection === "backward" && i > 0) 792 moveToAttribute = attributes[i - 1].name; 793 else if (moveDirection === "forward" && i < attributes.length - 1) 794 moveToAttribute = attributes[i + 1].name; 795 else if (moveDirection === "forward" && i === attributes.length - 1) 796 newAttribute = true; 797 } 798 } 799 800 if (!found && moveDirection === "backward" && attributes.length > 0) 801 moveToAttribute = attributes[attributes.length - 1].name; 802 else if (!found && moveDirection === "forward" && !/^\s*$/.test(newText)) 803 newAttribute = true; 804 } 805 806 function moveToNextAttributeIfNeeded() { 807 if (moveToAttribute) 808 this._triggerEditAttribute(moveToAttribute); 809 else if (newAttribute) 810 this._addNewAttribute(this.listItemElement); 811 } 812 813 var parseContainerElement = document.createElement("span"); 814 parseContainerElement.innerHTML = "<span " + newText + "></span>"; 815 var parseElement = parseContainerElement.firstChild; 816 817 if (!parseElement) { 818 this._editingCancelled(element, attributeName); 819 moveToNextAttributeIfNeeded.call(this); 820 return; 821 } 822 823 if (!parseElement.hasAttributes()) { 824 this.representedObject.removeAttribute(attributeName); 825 moveToNextAttributeIfNeeded.call(this); 826 return; 827 } 828 829 var foundOriginalAttribute = false; 830 for (var i = 0; i < parseElement.attributes.length; ++i) { 831 var attr = parseElement.attributes[i]; 832 foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName; 833 try { 834 this.representedObject.setAttribute(attr.name, attr.value); 835 } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can) 836 } 837 838 if (!foundOriginalAttribute) 839 this.representedObject.removeAttribute(attributeName); 840 841 this.treeOutline.focusedNodeChanged(true); 842 843 moveToNextAttributeIfNeeded.call(this); 844 }, 845 846 _textNodeEditingCommitted: function(element, newText) 847 { 848 delete this._editing; 849 850 var textNode; 851 if (this.representedObject.nodeType == Node.ELEMENT_NODE) { 852 // We only show text nodes inline in elements if the element only 853 // has a single child, and that child is a text node. 854 textNode = this.representedObject.firstChild; 855 } else if (this.representedObject.nodeType == Node.TEXT_NODE) 856 textNode = this.representedObject; 857 858 textNode.nodeValue = newText; 859 860 // Need to restore attributes / node structure. 861 this.updateTitle(); 862 }, 863 864 _editingCancelled: function(element, context) 865 { 866 delete this._editing; 867 868 // Need to restore attributes structure. 869 this.updateTitle(); 870 }, 871 872 updateTitle: function() 873 { 874 // If we are editing, return early to prevent canceling the edit. 875 // After editing is committed updateTitle will be called. 876 if (this._editing) 877 return; 878 879 var self = this; 880 function callback(tooltipText) 881 { 882 var title = self._nodeTitleInfo(self.representedObject, self.hasChildren, WebInspector.linkifyURL, tooltipText).title; 883 self.title = "<span class=\"highlight\">" + title + "</span>"; 884 delete self.selectionElement; 885 self.updateSelection(); 886 self._preventFollowingLinksOnDoubleClick(); 887 }; 888 889 // TODO: Replace with InjectedScriptAccess.getBasicProperties(obj, [names]). 890 if (this.representedObject.nodeName.toLowerCase() !== "img") 891 callback(); 892 else 893 this.createTooltipForImageNode(this.representedObject, callback); 894 }, 895 896 _rewriteAttrHref: function(node, hrefValue) 897 { 898 if (!hrefValue || hrefValue.indexOf("://") > 0) 899 return hrefValue; 900 901 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) { 902 if (frameOwnerCandidate.documentURL) { 903 var result = WebInspector.completeURL(frameOwnerCandidate.documentURL, hrefValue); 904 if (result) 905 return result; 906 break; 907 } 908 } 909 910 // documentURL not found or has bad value 911 for (var url in WebInspector.resourceURLMap) { 912 var match = url.match(WebInspector.URLRegExp); 913 if (match && match[4] === hrefValue) 914 return url; 915 } 916 return hrefValue; 917 }, 918 919 _nodeTitleInfo: function(node, hasChildren, linkify, tooltipText) 920 { 921 var info = {title: "", hasChildren: hasChildren}; 922 923 switch (node.nodeType) { 924 case Node.DOCUMENT_NODE: 925 info.title = "Document"; 926 break; 927 928 case Node.DOCUMENT_FRAGMENT_NODE: 929 info.title = "Document Fragment"; 930 break; 931 932 case Node.ELEMENT_NODE: 933 info.title = "<span class=\"webkit-html-tag\"><" + node.nodeName.toLowerCase().escapeHTML(); 934 935 if (node.hasAttributes()) { 936 for (var i = 0; i < node.attributes.length; ++i) { 937 var attr = node.attributes[i]; 938 info.title += " <span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=​\""; 939 940 var value = attr.value; 941 if (linkify && (attr.name === "src" || attr.name === "href")) { 942 var value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B"); 943 info.title += linkify(this._rewriteAttrHref(node, attr.value), value, "webkit-html-attribute-value", node.nodeName.toLowerCase() == "a", tooltipText); 944 } else { 945 var value = value.escapeHTML(); 946 value = value.replace(/([\/;:\)\]\}])/g, "$1​"); 947 info.title += "<span class=\"webkit-html-attribute-value\">" + value + "</span>"; 948 } 949 info.title += "\"</span>"; 950 } 951 } 952 info.title += "></span>​"; 953 954 // If this element only has a single child that is a text node, 955 // just show that text and the closing tag inline rather than 956 // create a subtree for them 957 958 var textChild = onlyTextChild.call(node); 959 var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength; 960 961 if (showInlineText) { 962 info.title += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue.escapeHTML() + "</span>​<span class=\"webkit-html-tag\"></" + node.nodeName.toLowerCase().escapeHTML() + "></span>"; 963 info.hasChildren = false; 964 } 965 break; 966 967 case Node.TEXT_NODE: 968 if (isNodeWhitespace.call(node)) 969 info.title = "(whitespace)"; 970 else { 971 if (node.parentNode && node.parentNode.nodeName.toLowerCase() == "script") { 972 var newNode = document.createElement("span"); 973 newNode.textContent = node.textContent; 974 975 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript"); 976 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode); 977 978 info.title = "<span class=\"webkit-html-text-node webkit-html-js-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>"; 979 } else if (node.parentNode && node.parentNode.nodeName.toLowerCase() == "style") { 980 var newNode = document.createElement("span"); 981 newNode.textContent = node.textContent; 982 983 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css"); 984 cssSyntaxHighlighter.syntaxHighlightNode(newNode); 985 986 info.title = "<span class=\"webkit-html-text-node webkit-html-css-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>"; 987 } else { 988 info.title = "\"<span class=\"webkit-html-text-node\">" + node.nodeValue.escapeHTML() + "</span>\""; 989 } 990 } 991 break; 992 993 case Node.COMMENT_NODE: 994 info.title = "<span class=\"webkit-html-comment\"><!--" + node.nodeValue.escapeHTML() + "--></span>"; 995 break; 996 997 case Node.DOCUMENT_TYPE_NODE: 998 info.title = "<span class=\"webkit-html-doctype\"><!DOCTYPE " + node.nodeName; 999 if (node.publicId) { 1000 info.title += " PUBLIC \"" + node.publicId + "\""; 1001 if (node.systemId) 1002 info.title += " \"" + node.systemId + "\""; 1003 } else if (node.systemId) 1004 info.title += " SYSTEM \"" + node.systemId + "\""; 1005 if (node.internalSubset) 1006 info.title += " [" + node.internalSubset + "]"; 1007 info.title += "></span>"; 1008 break; 1009 default: 1010 info.title = node.nodeName.toLowerCase().collapseWhitespace().escapeHTML(); 1011 } 1012 1013 return info; 1014 }, 1015 1016 _showInlineText: function(node) 1017 { 1018 if (node.nodeType === Node.ELEMENT_NODE) { 1019 var textChild = onlyTextChild.call(node); 1020 if (textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength) 1021 return true; 1022 } 1023 return false; 1024 }, 1025 1026 remove: function() 1027 { 1028 var parentElement = this.parent; 1029 if (!parentElement) 1030 return; 1031 1032 var self = this; 1033 function removeNodeCallback(removedNodeId) 1034 { 1035 // -1 is an error code, which means removing the node from the DOM failed, 1036 // so we shouldn't remove it from the tree. 1037 if (removedNodeId === -1) 1038 return; 1039 1040 parentElement.removeChild(self); 1041 } 1042 1043 var callId = WebInspector.Callback.wrap(removeNodeCallback); 1044 InspectorBackend.removeNode(callId, this.representedObject.id); 1045 }, 1046 1047 _editAsHTML: function() 1048 { 1049 var treeOutline = this.treeOutline; 1050 var node = this.representedObject; 1051 var wasExpanded = this.expanded; 1052 1053 function selectNode(nodeId) 1054 { 1055 if (!nodeId) 1056 return; 1057 1058 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. 1059 WebInspector.panels.elements.updateModifiedNodes(); 1060 1061 WebInspector.updateFocusedNode(nodeId); 1062 if (wasExpanded) { 1063 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId)); 1064 if (newTreeItem) 1065 newTreeItem.expand(); 1066 } 1067 } 1068 1069 function commitChange(value) 1070 { 1071 InjectedScriptAccess.get(node.injectedScriptId).setOuterHTML(node.id, value, wasExpanded, selectNode.bind(this)); 1072 } 1073 1074 InjectedScriptAccess.get(node.injectedScriptId).getNodePropertyValue(node.id, "outerHTML", this._startEditingAsHTML.bind(this, commitChange)); 1075 }, 1076 1077 _copyHTML: function() 1078 { 1079 InspectorBackend.copyNode(this.representedObject.id); 1080 } 1081 } 1082 1083 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype; 1084 1085 WebInspector.didRemoveNode = WebInspector.Callback.processCallback; 1086