1/* 2 * Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Matt Lilek <webkit@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 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 31WebInspector.DOMTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled) 32{ 33 this.element = document.createElement("ol"); 34 this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); 35 this.element.addEventListener("mousemove", this._onmousemove.bind(this), false); 36 this.element.addEventListener("mouseout", this._onmouseout.bind(this), false); 37 this.element.addEventListener("dragstart", this._ondragstart.bind(this), false); 38 this.element.addEventListener("dragover", this._ondragover.bind(this), false); 39 this.element.addEventListener("dragleave", this._ondragleave.bind(this), false); 40 this.element.addEventListener("drop", this._ondrop.bind(this), false); 41 this.element.addEventListener("dragend", this._ondragend.bind(this), false); 42 43 this.element.classList.add(WebInspector.DOMTreeOutline.StyleClassName); 44 this.element.classList.add(WebInspector.SyntaxHighlightedStyleClassName); 45 46 TreeOutline.call(this, this.element); 47 48 this._includeRootDOMNode = !omitRootDOMNode; 49 this._selectEnabled = selectEnabled; 50 this._showInElementsPanelEnabled = showInElementsPanelEnabled; 51 this._rootDOMNode = null; 52 this._selectedDOMNode = null; 53 this._eventSupport = new WebInspector.Object(); 54 this._editing = false; 55 56 this._visible = false; 57 58 this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true); 59 60 this._hideElementKeyboardShortcut = new WebInspector.KeyboardShortcut(null, "H", this._hideElement.bind(this), this.element); 61 this._hideElementKeyboardShortcut.implicitlyPreventsDefault = false; 62 63 WebInspector.showShadowDOMSetting.addEventListener(WebInspector.Setting.Event.Changed, this._showShadowDOMSettingChanged, this); 64} 65 66WebInspector.Object.addConstructorFunctions(WebInspector.DOMTreeOutline); 67 68WebInspector.DOMTreeOutline.StyleClassName = "dom-tree-outline"; 69 70WebInspector.DOMTreeOutline.Event = { 71 SelectedNodeChanged: "dom-tree-outline-selected-node-changed" 72} 73 74WebInspector.DOMTreeOutline.prototype = { 75 constructor: WebInspector.DOMTreeOutline, 76 77 wireToDomAgent: function() 78 { 79 this._elementsTreeUpdater = new WebInspector.DOMTreeUpdater(this); 80 }, 81 82 close: function() 83 { 84 if (this._elementsTreeUpdater) { 85 this._elementsTreeUpdater.close(); 86 this._elementsTreeUpdater = null; 87 } 88 }, 89 90 setVisible: function(visible, omitFocus) 91 { 92 this._visible = visible; 93 if (!this._visible) 94 return; 95 96 this._updateModifiedNodes(); 97 if (this._selectedDOMNode) 98 this._revealAndSelectNode(this._selectedDOMNode, omitFocus); 99 }, 100 101 addEventListener: function(eventType, listener, thisObject) 102 { 103 this._eventSupport.addEventListener(eventType, listener, thisObject); 104 }, 105 106 removeEventListener: function(eventType, listener, thisObject) 107 { 108 this._eventSupport.removeEventListener(eventType, listener, thisObject); 109 }, 110 111 get rootDOMNode() 112 { 113 return this._rootDOMNode; 114 }, 115 116 set rootDOMNode(x) 117 { 118 if (this._rootDOMNode === x) 119 return; 120 121 this._rootDOMNode = x; 122 123 this._isXMLMimeType = x && x.isXMLNode(); 124 125 this.update(); 126 }, 127 128 get isXMLMimeType() 129 { 130 return this._isXMLMimeType; 131 }, 132 133 selectedDOMNode: function() 134 { 135 return this._selectedDOMNode; 136 }, 137 138 selectDOMNode: function(node, focus) 139 { 140 if (this._selectedDOMNode === node) { 141 this._revealAndSelectNode(node, !focus); 142 return; 143 } 144 145 this._selectedDOMNode = node; 146 this._revealAndSelectNode(node, !focus); 147 148 // The _revealAndSelectNode() method might find a different element if there is inlined text, 149 // and the select() call would change the selectedDOMNode and reenter this setter. So to 150 // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same 151 // node as the one passed in. 152 // Note that _revealAndSelectNode will not do anything for a null node. 153 if (!node || this._selectedDOMNode === node) 154 this._selectedNodeChanged(); 155 }, 156 157 get editing() 158 { 159 return this._editing; 160 }, 161 162 update: function() 163 { 164 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; 165 166 this.removeChildren(); 167 168 if (!this.rootDOMNode) 169 return; 170 171 var treeElement; 172 if (this._includeRootDOMNode) { 173 treeElement = new WebInspector.DOMTreeElement(this.rootDOMNode); 174 treeElement.selectable = this._selectEnabled; 175 this.appendChild(treeElement); 176 } else { 177 // FIXME: this could use findTreeElement to reuse a tree element if it already exists 178 var node = this.rootDOMNode.firstChild; 179 while (node) { 180 treeElement = new WebInspector.DOMTreeElement(node); 181 treeElement.selectable = this._selectEnabled; 182 this.appendChild(treeElement); 183 node = node.nextSibling; 184 } 185 } 186 187 if (selectedNode) 188 this._revealAndSelectNode(selectedNode, true); 189 }, 190 191 updateSelection: function() 192 { 193 if (!this.selectedTreeElement) 194 return; 195 var element = this.treeOutline.selectedTreeElement; 196 element.updateSelection(); 197 }, 198 199 _selectedNodeChanged: function() 200 { 201 this._eventSupport.dispatchEventToListeners(WebInspector.DOMTreeOutline.Event.SelectedNodeChanged); 202 }, 203 204 findTreeElement: function(node) 205 { 206 function isAncestorNode(ancestor, node) 207 { 208 return ancestor.isAncestor(node); 209 } 210 211 function parentNode(node) 212 { 213 return node.parentNode; 214 } 215 216 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode); 217 if (!treeElement && node.nodeType() === Node.TEXT_NODE) { 218 // The text node might have been inlined if it was short, so try to find the parent element. 219 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode); 220 } 221 222 return treeElement; 223 }, 224 225 createTreeElementFor: function(node) 226 { 227 var treeElement = this.findTreeElement(node); 228 if (treeElement) 229 return treeElement; 230 if (!node.parentNode) 231 return null; 232 233 treeElement = this.createTreeElementFor(node.parentNode); 234 if (treeElement && treeElement.showChild(node.index)) 235 return treeElement.children[node.index]; 236 237 return null; 238 }, 239 240 set suppressRevealAndSelect(x) 241 { 242 if (this._suppressRevealAndSelect === x) 243 return; 244 this._suppressRevealAndSelect = x; 245 }, 246 247 _revealAndSelectNode: function(node, omitFocus) 248 { 249 if (!node || this._suppressRevealAndSelect) 250 return; 251 252 var treeElement = this.createTreeElementFor(node); 253 if (!treeElement) 254 return; 255 256 treeElement.revealAndSelect(omitFocus); 257 }, 258 259 _treeElementFromEvent: function(event) 260 { 261 var scrollContainer = this.element.parentElement; 262 263 // We choose this X coordinate based on the knowledge that our list 264 // items extend at least to the right edge of the outer <ol> container. 265 // In the no-word-wrap mode the outer <ol> may be wider than the tree container 266 // (and partially hidden), in which case we are left to use only its right boundary. 267 var x = scrollContainer.totalOffsetLeft + scrollContainer.offsetWidth - 36; 268 269 var y = event.pageY; 270 271 // Our list items have 1-pixel cracks between them vertically. We avoid 272 // the cracks by checking slightly above and slightly below the mouse 273 // and seeing if we hit the same element each time. 274 var elementUnderMouse = this.treeElementFromPoint(x, y); 275 var elementAboveMouse = this.treeElementFromPoint(x, y - 2); 276 var element; 277 if (elementUnderMouse === elementAboveMouse) 278 element = elementUnderMouse; 279 else 280 element = this.treeElementFromPoint(x, y + 2); 281 282 return element; 283 }, 284 285 _onmousedown: function(event) 286 { 287 var element = this._treeElementFromEvent(event); 288 if (!element || element.isEventWithinDisclosureTriangle(event)) { 289 event.preventDefault(); 290 return; 291 } 292 293 element.select(); 294 }, 295 296 _onmousemove: function(event) 297 { 298 var element = this._treeElementFromEvent(event); 299 if (element && this._previousHoveredElement === element) 300 return; 301 302 if (this._previousHoveredElement) { 303 this._previousHoveredElement.hovered = false; 304 delete this._previousHoveredElement; 305 } 306 307 if (element) { 308 element.hovered = true; 309 this._previousHoveredElement = element; 310 311 // Lazily compute tag-specific tooltips. 312 if (element.representedObject && !element.tooltip && element._createTooltipForNode) 313 element._createTooltipForNode(); 314 } 315 316 WebInspector.domTreeManager.highlightDOMNode(element ? element.representedObject.id : 0); 317 }, 318 319 _onmouseout: function(event) 320 { 321 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 322 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element)) 323 return; 324 325 if (this._previousHoveredElement) { 326 this._previousHoveredElement.hovered = false; 327 delete this._previousHoveredElement; 328 } 329 330 WebInspector.domTreeManager.hideDOMNodeHighlight(); 331 }, 332 333 _ondragstart: function(event) 334 { 335 var treeElement = this._treeElementFromEvent(event); 336 if (!treeElement) 337 return false; 338 339 if (!this._isValidDragSourceOrTarget(treeElement)) 340 return false; 341 342 if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD") 343 return false; 344 345 event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent); 346 event.dataTransfer.effectAllowed = "copyMove"; 347 this._nodeBeingDragged = treeElement.representedObject; 348 349 WebInspector.domTreeManager.hideDOMNodeHighlight(); 350 351 return true; 352 }, 353 354 _ondragover: function(event) 355 { 356 if (!this._nodeBeingDragged) 357 return false; 358 359 var treeElement = this._treeElementFromEvent(event); 360 if (!this._isValidDragSourceOrTarget(treeElement)) 361 return false; 362 363 var node = treeElement.representedObject; 364 while (node) { 365 if (node === this._nodeBeingDragged) 366 return false; 367 node = node.parentNode; 368 } 369 370 treeElement.updateSelection(); 371 treeElement.listItemElement.classList.add("elements-drag-over"); 372 this._dragOverTreeElement = treeElement; 373 event.preventDefault(); 374 event.dataTransfer.dropEffect = "move"; 375 return false; 376 }, 377 378 _ondragleave: function(event) 379 { 380 this._clearDragOverTreeElementMarker(); 381 event.preventDefault(); 382 return false; 383 }, 384 385 _isValidDragSourceOrTarget: function(treeElement) 386 { 387 if (!treeElement) 388 return false; 389 390 var node = treeElement.representedObject; 391 if (!(node instanceof WebInspector.DOMNode)) 392 return false; 393 394 if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) 395 return false; 396 397 return true; 398 }, 399 400 _ondrop: function(event) 401 { 402 event.preventDefault(); 403 var treeElement = this._treeElementFromEvent(event); 404 if (this._nodeBeingDragged && treeElement) { 405 var parentNode; 406 var anchorNode; 407 408 if (treeElement._elementCloseTag) { 409 // Drop onto closing tag -> insert as last child. 410 parentNode = treeElement.representedObject; 411 } else { 412 var dragTargetNode = treeElement.representedObject; 413 parentNode = dragTargetNode.parentNode; 414 anchorNode = dragTargetNode; 415 } 416 417 function callback(error, newNodeId) 418 { 419 if (error) 420 return; 421 422 this._updateModifiedNodes(); 423 var newNode = WebInspector.domTreeManager.nodeForId(newNodeId); 424 if (newNode) 425 this.selectDOMNode(newNode, true); 426 } 427 this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this)); 428 } 429 430 delete this._nodeBeingDragged; 431 }, 432 433 _ondragend: function(event) 434 { 435 event.preventDefault(); 436 this._clearDragOverTreeElementMarker(); 437 delete this._nodeBeingDragged; 438 }, 439 440 _clearDragOverTreeElementMarker: function() 441 { 442 if (this._dragOverTreeElement) { 443 this._dragOverTreeElement.updateSelection(); 444 this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over"); 445 delete this._dragOverTreeElement; 446 } 447 }, 448 449 _contextMenuEventFired: function(event) 450 { 451 var treeElement = this._treeElementFromEvent(event); 452 if (!treeElement) 453 return; 454 455 var contextMenu = new WebInspector.ContextMenu(event); 456 this.populateContextMenu(contextMenu, event); 457 contextMenu.show(); 458 }, 459 460 populateContextMenu: function(contextMenu, event) 461 { 462 var treeElement = this._treeElementFromEvent(event); 463 if (!treeElement) 464 return false; 465 466 var tag = event.target.enclosingNodeOrSelfWithClass("html-tag"); 467 var textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node"); 468 var commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment"); 469 var populated = false; 470 if (tag && treeElement._populateTagContextMenu) { 471 if (populated) 472 contextMenu.appendSeparator(); 473 treeElement._populateTagContextMenu(contextMenu, event); 474 populated = true; 475 } else if (textNode && treeElement._populateTextContextMenu) { 476 if (populated) 477 contextMenu.appendSeparator(); 478 treeElement._populateTextContextMenu(contextMenu, textNode); 479 populated = true; 480 } else if (commentNode && treeElement._populateNodeContextMenu) { 481 if (populated) 482 contextMenu.appendSeparator(); 483 treeElement._populateNodeContextMenu(contextMenu, textNode); 484 populated = true; 485 } 486 487 return populated; 488 }, 489 490 adjustCollapsedRange: function() 491 { 492 }, 493 494 _updateModifiedNodes: function() 495 { 496 if (this._elementsTreeUpdater) 497 this._elementsTreeUpdater._updateModifiedNodes(); 498 }, 499 500 _populateContextMenu: function(contextMenu, domNode) 501 { 502 if (!this._showInElementsPanelEnabled) 503 return; 504 505 function revealElement() 506 { 507 WebInspector.domTreeManager.inspectElement(domNode.id); 508 } 509 510 contextMenu.appendSeparator(); 511 contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement); 512 }, 513 514 _showShadowDOMSettingChanged: function(event) 515 { 516 var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null; 517 while (nodeToSelect) { 518 if (!nodeToSelect.isInShadowTree()) 519 break; 520 nodeToSelect = nodeToSelect.parentNode; 521 } 522 523 this.children.forEach(function(child) { 524 child.updateChildren(true); 525 }); 526 527 if (nodeToSelect) 528 this.selectDOMNode(nodeToSelect); 529 }, 530 531 _hideElement: function(event, keyboardShortcut) 532 { 533 if (!this.selectedTreeElement || WebInspector.isEditingAnyField()) 534 return; 535 536 event.preventDefault(); 537 538 var selectedNode = this.selectedTreeElement.representedObject; 539 console.assert(selectedNode); 540 if (!selectedNode) 541 return; 542 543 if (selectedNode.nodeType() !== Node.ELEMENT_NODE) 544 return; 545 546 if (this._togglePending) 547 return; 548 this._togglePending = true; 549 550 function toggleProperties() 551 { 552 nodeStyles.removeEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this); 553 554 var opacityProperty = nodeStyles.inlineStyle.propertyForName("opacity"); 555 opacityProperty.value = "0"; 556 opacityProperty.important = true; 557 558 var pointerEventsProperty = nodeStyles.inlineStyle.propertyForName("pointer-events"); 559 pointerEventsProperty.value = "none"; 560 pointerEventsProperty.important = true; 561 562 if (opacityProperty.enabled && pointerEventsProperty.enabled) { 563 opacityProperty.remove(); 564 pointerEventsProperty.remove(); 565 } else { 566 opacityProperty.add(); 567 pointerEventsProperty.add(); 568 } 569 570 delete this._togglePending; 571 } 572 573 var nodeStyles = WebInspector.cssStyleManager.stylesForNode(selectedNode); 574 if (nodeStyles.needsRefresh) { 575 nodeStyles.addEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this); 576 nodeStyles.refresh(); 577 } else 578 toggleProperties.call(this); 579 } 580} 581 582WebInspector.DOMTreeOutline.prototype.__proto__ = TreeOutline.prototype; 583