1 /* 2 * Copyright (C) 2009, 2010 Google Inc. All rights reserved. 3 * Copyright (C) 2009 Joseph Pecoraro 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 /** 33 * @constructor 34 * @param {WebInspector.DOMAgent} domAgent 35 * @param {?WebInspector.DOMDocument} doc 36 * @param {boolean} isInShadowTree 37 * @param {DOMAgent.Node} payload 38 */ 39 WebInspector.DOMNode = function(domAgent, doc, isInShadowTree, payload) { 40 this._domAgent = domAgent; 41 this.ownerDocument = doc; 42 this._isInShadowTree = isInShadowTree; 43 44 this.id = payload.nodeId; 45 domAgent._idToDOMNode[this.id] = this; 46 this._nodeType = payload.nodeType; 47 this._nodeName = payload.nodeName; 48 this._localName = payload.localName; 49 this._nodeValue = payload.nodeValue; 50 51 this._shadowRoots = []; 52 53 this._attributes = []; 54 this._attributesMap = {}; 55 if (payload.attributes) 56 this._setAttributesPayload(payload.attributes); 57 58 this._userProperties = {}; 59 this._descendantUserPropertyCounters = {}; 60 61 this._childNodeCount = payload.childNodeCount || 0; 62 this._children = null; 63 64 this.nextSibling = null; 65 this.previousSibling = null; 66 this.firstChild = null; 67 this.lastChild = null; 68 this.parentNode = null; 69 70 if (payload.shadowRoots) { 71 for (var i = 0; i < payload.shadowRoots.length; ++i) { 72 var root = payload.shadowRoots[i]; 73 var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, true, root); 74 this._shadowRoots.push(node); 75 node.parentNode = this; 76 } 77 } 78 79 if (payload.templateContent) { 80 this._templateContent = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, true, payload.templateContent); 81 this._templateContent.parentNode = this; 82 } 83 84 if (payload.children) 85 this._setChildrenPayload(payload.children); 86 87 if (payload.contentDocument) { 88 this._contentDocument = new WebInspector.DOMDocument(domAgent, payload.contentDocument); 89 this._children = [this._contentDocument]; 90 this._renumber(); 91 } 92 93 if (this._nodeType === Node.ELEMENT_NODE) { 94 // HTML and BODY from internal iframes should not overwrite top-level ones. 95 if (this.ownerDocument && !this.ownerDocument.documentElement && this._nodeName === "HTML") 96 this.ownerDocument.documentElement = this; 97 if (this.ownerDocument && !this.ownerDocument.body && this._nodeName === "BODY") 98 this.ownerDocument.body = this; 99 } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) { 100 this.publicId = payload.publicId; 101 this.systemId = payload.systemId; 102 this.internalSubset = payload.internalSubset; 103 } else if (this._nodeType === Node.ATTRIBUTE_NODE) { 104 this.name = payload.name; 105 this.value = payload.value; 106 } 107 } 108 109 /** 110 * @constructor 111 * @param {string} value 112 * @param {boolean} optimized 113 */ 114 WebInspector.DOMNode.XPathStep = function(value, optimized) 115 { 116 this.value = value; 117 this.optimized = optimized; 118 } 119 120 WebInspector.DOMNode.XPathStep.prototype = { 121 toString: function() 122 { 123 return this.value; 124 } 125 } 126 127 WebInspector.DOMNode.prototype = { 128 /** 129 * @return {Array.<WebInspector.DOMNode>} 130 */ 131 children: function() 132 { 133 return this._children ? this._children.slice() : null; 134 }, 135 136 /** 137 * @return {boolean} 138 */ 139 hasAttributes: function() 140 { 141 return this._attributes.length > 0; 142 }, 143 144 /** 145 * @return {number} 146 */ 147 childNodeCount: function() 148 { 149 return this._childNodeCount; 150 }, 151 152 /** 153 * @return {boolean} 154 */ 155 hasShadowRoots: function() 156 { 157 return !!this._shadowRoots.length; 158 }, 159 160 /** 161 * @return {Array.<WebInspector.DOMNode>} 162 */ 163 shadowRoots: function() 164 { 165 return this._shadowRoots.slice(); 166 }, 167 168 /** 169 * @return {WebInspector.DOMNode} 170 */ 171 templateContent: function() 172 { 173 return this._templateContent; 174 }, 175 176 /** 177 * @return {number} 178 */ 179 nodeType: function() 180 { 181 return this._nodeType; 182 }, 183 184 /** 185 * @return {string} 186 */ 187 nodeName: function() 188 { 189 return this._nodeName; 190 }, 191 192 /** 193 * @return {boolean} 194 */ 195 isInShadowTree: function() 196 { 197 return this._isInShadowTree; 198 }, 199 200 /** 201 * @return {string} 202 */ 203 nodeNameInCorrectCase: function() 204 { 205 return this.isXMLNode() ? this.nodeName() : this.nodeName().toLowerCase(); 206 }, 207 208 /** 209 * @param {string} name 210 * @param {function(?Protocol.Error)=} callback 211 */ 212 setNodeName: function(name, callback) 213 { 214 DOMAgent.setNodeName(this.id, name, WebInspector.domAgent._markRevision(this, callback)); 215 }, 216 217 /** 218 * @return {string} 219 */ 220 localName: function() 221 { 222 return this._localName; 223 }, 224 225 /** 226 * @return {string} 227 */ 228 nodeValue: function() 229 { 230 return this._nodeValue; 231 }, 232 233 /** 234 * @param {string} value 235 * @param {function(?Protocol.Error)=} callback 236 */ 237 setNodeValue: function(value, callback) 238 { 239 DOMAgent.setNodeValue(this.id, value, WebInspector.domAgent._markRevision(this, callback)); 240 }, 241 242 /** 243 * @param {string} name 244 * @return {string} 245 */ 246 getAttribute: function(name) 247 { 248 var attr = this._attributesMap[name]; 249 return attr ? attr.value : undefined; 250 }, 251 252 /** 253 * @param {string} name 254 * @param {string} text 255 * @param {function(?Protocol.Error)=} callback 256 */ 257 setAttribute: function(name, text, callback) 258 { 259 DOMAgent.setAttributesAsText(this.id, text, name, WebInspector.domAgent._markRevision(this, callback)); 260 }, 261 262 /** 263 * @param {string} name 264 * @param {string} value 265 * @param {function(?Protocol.Error)=} callback 266 */ 267 setAttributeValue: function(name, value, callback) 268 { 269 DOMAgent.setAttributeValue(this.id, name, value, WebInspector.domAgent._markRevision(this, callback)); 270 }, 271 272 /** 273 * @return {Object} 274 */ 275 attributes: function() 276 { 277 return this._attributes; 278 }, 279 280 /** 281 * @param {string} name 282 * @param {function(?Protocol.Error)=} callback 283 */ 284 removeAttribute: function(name, callback) 285 { 286 /** 287 * @param {?Protocol.Error} error 288 */ 289 function mycallback(error) 290 { 291 if (!error) { 292 delete this._attributesMap[name]; 293 for (var i = 0; i < this._attributes.length; ++i) { 294 if (this._attributes[i].name === name) { 295 this._attributes.splice(i, 1); 296 break; 297 } 298 } 299 } 300 301 WebInspector.domAgent._markRevision(this, callback)(error); 302 } 303 DOMAgent.removeAttribute(this.id, name, mycallback.bind(this)); 304 }, 305 306 /** 307 * @param {function(Array.<WebInspector.DOMNode>)=} callback 308 */ 309 getChildNodes: function(callback) 310 { 311 if (this._children) { 312 if (callback) 313 callback(this.children()); 314 return; 315 } 316 317 /** 318 * @this {WebInspector.DOMNode} 319 * @param {?Protocol.Error} error 320 */ 321 function mycallback(error) 322 { 323 if (!error && callback) 324 callback(this.children()); 325 } 326 327 DOMAgent.requestChildNodes(this.id, undefined, mycallback.bind(this)); 328 }, 329 330 /** 331 * @param {number} depth 332 * @param {function(Array.<WebInspector.DOMNode>)=} callback 333 */ 334 getSubtree: function(depth, callback) 335 { 336 /** 337 * @this {WebInspector.DOMNode} 338 * @param {?Protocol.Error} error 339 */ 340 function mycallback(error) 341 { 342 if (callback) 343 callback(error ? null : this._children); 344 } 345 346 DOMAgent.requestChildNodes(this.id, depth, mycallback.bind(this)); 347 }, 348 349 /** 350 * @param {function(?Protocol.Error)=} callback 351 */ 352 getOuterHTML: function(callback) 353 { 354 DOMAgent.getOuterHTML(this.id, callback); 355 }, 356 357 /** 358 * @param {string} html 359 * @param {function(?Protocol.Error)=} callback 360 */ 361 setOuterHTML: function(html, callback) 362 { 363 DOMAgent.setOuterHTML(this.id, html, WebInspector.domAgent._markRevision(this, callback)); 364 }, 365 366 /** 367 * @param {function(?Protocol.Error)=} callback 368 */ 369 removeNode: function(callback) 370 { 371 DOMAgent.removeNode(this.id, WebInspector.domAgent._markRevision(this, callback)); 372 }, 373 374 copyNode: function() 375 { 376 function copy(error, text) 377 { 378 if (!error) 379 InspectorFrontendHost.copyText(text); 380 } 381 DOMAgent.getOuterHTML(this.id, copy); 382 }, 383 384 /** 385 * @param {boolean} optimized 386 */ 387 copyXPath: function(optimized) 388 { 389 InspectorFrontendHost.copyText(this.xPath(optimized)); 390 }, 391 392 /** 393 * @param {string} objectGroupId 394 * @param {function(?Protocol.Error)=} callback 395 */ 396 eventListeners: function(objectGroupId, callback) 397 { 398 DOMAgent.getEventListenersForNode(this.id, objectGroupId, callback); 399 }, 400 401 /** 402 * @return {string} 403 */ 404 path: function() 405 { 406 var path = []; 407 var node = this; 408 while (node && "index" in node && node._nodeName.length) { 409 path.push([node.index, node._nodeName]); 410 node = node.parentNode; 411 } 412 path.reverse(); 413 return path.join(","); 414 }, 415 416 /** 417 * @param {boolean} justSelector 418 * @return {string} 419 */ 420 appropriateSelectorFor: function(justSelector) 421 { 422 var lowerCaseName = this.localName() || this.nodeName().toLowerCase(); 423 424 var id = this.getAttribute("id"); 425 if (id) { 426 var selector = "#" + id; 427 return (justSelector ? selector : lowerCaseName + selector); 428 } 429 430 var className = this.getAttribute("class"); 431 if (className) { 432 var selector = "." + className.trim().replace(/\s+/g, "."); 433 return (justSelector ? selector : lowerCaseName + selector); 434 } 435 436 if (lowerCaseName === "input" && this.getAttribute("type")) 437 return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]"; 438 439 return lowerCaseName; 440 }, 441 442 /** 443 * @param {WebInspector.DOMNode} node 444 * @return {boolean} 445 */ 446 isAncestor: function(node) 447 { 448 if (!node) 449 return false; 450 451 var currentNode = node.parentNode; 452 while (currentNode) { 453 if (this === currentNode) 454 return true; 455 currentNode = currentNode.parentNode; 456 } 457 return false; 458 }, 459 460 /** 461 * @param {WebInspector.DOMNode} descendant 462 * @return {boolean} 463 */ 464 isDescendant: function(descendant) 465 { 466 return descendant !== null && descendant.isAncestor(this); 467 }, 468 469 /** 470 * @param {Array.<string>} attrs 471 * @return {boolean} 472 */ 473 _setAttributesPayload: function(attrs) 474 { 475 var attributesChanged = !this._attributes || attrs.length !== this._attributes.length * 2; 476 var oldAttributesMap = this._attributesMap || {}; 477 478 this._attributes = []; 479 this._attributesMap = {}; 480 481 for (var i = 0; i < attrs.length; i += 2) { 482 var name = attrs[i]; 483 var value = attrs[i + 1]; 484 this._addAttribute(name, value); 485 486 if (attributesChanged) 487 continue; 488 489 if (!oldAttributesMap[name] || oldAttributesMap[name].value !== value) 490 attributesChanged = true; 491 } 492 return attributesChanged; 493 }, 494 495 /** 496 * @param {WebInspector.DOMNode} prev 497 * @param {DOMAgent.Node} payload 498 * @return {WebInspector.DOMNode} 499 */ 500 _insertChild: function(prev, payload) 501 { 502 var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, this._isInShadowTree, payload); 503 this._children.splice(this._children.indexOf(prev) + 1, 0, node); 504 this._renumber(); 505 return node; 506 }, 507 508 /** 509 * @param {WebInspector.DOMNode} node 510 */ 511 _removeChild: function(node) 512 { 513 this._children.splice(this._children.indexOf(node), 1); 514 node.parentNode = null; 515 node._updateChildUserPropertyCountsOnRemoval(this); 516 this._renumber(); 517 }, 518 519 /** 520 * @param {Array.<DOMAgent.Node>} payloads 521 */ 522 _setChildrenPayload: function(payloads) 523 { 524 // We set children in the constructor. 525 if (this._contentDocument) 526 return; 527 528 this._children = []; 529 for (var i = 0; i < payloads.length; ++i) { 530 var payload = payloads[i]; 531 var node = new WebInspector.DOMNode(this._domAgent, this.ownerDocument, this._isInShadowTree, payload); 532 this._children.push(node); 533 } 534 this._renumber(); 535 }, 536 537 _renumber: function() 538 { 539 this._childNodeCount = this._children.length; 540 if (this._childNodeCount == 0) { 541 this.firstChild = null; 542 this.lastChild = null; 543 return; 544 } 545 this.firstChild = this._children[0]; 546 this.lastChild = this._children[this._childNodeCount - 1]; 547 for (var i = 0; i < this._childNodeCount; ++i) { 548 var child = this._children[i]; 549 child.index = i; 550 child.nextSibling = i + 1 < this._childNodeCount ? this._children[i + 1] : null; 551 child.previousSibling = i - 1 >= 0 ? this._children[i - 1] : null; 552 child.parentNode = this; 553 } 554 }, 555 556 /** 557 * @param {string} name 558 * @param {string} value 559 */ 560 _addAttribute: function(name, value) 561 { 562 var attr = { 563 name: name, 564 value: value, 565 _node: this 566 }; 567 this._attributesMap[name] = attr; 568 this._attributes.push(attr); 569 }, 570 571 /** 572 * @param {string} name 573 * @param {string} value 574 */ 575 _setAttribute: function(name, value) 576 { 577 var attr = this._attributesMap[name]; 578 if (attr) 579 attr.value = value; 580 else 581 this._addAttribute(name, value); 582 }, 583 584 /** 585 * @param {string} name 586 */ 587 _removeAttribute: function(name) 588 { 589 var attr = this._attributesMap[name]; 590 if (attr) { 591 this._attributes.remove(attr); 592 delete this._attributesMap[name]; 593 } 594 }, 595 596 /** 597 * @param {WebInspector.DOMNode} targetNode 598 * @param {?WebInspector.DOMNode} anchorNode 599 * @param {function(?Protocol.Error)=} callback 600 */ 601 moveTo: function(targetNode, anchorNode, callback) 602 { 603 DOMAgent.moveTo(this.id, targetNode.id, anchorNode ? anchorNode.id : undefined, WebInspector.domAgent._markRevision(this, callback)); 604 }, 605 606 /** 607 * @return {boolean} 608 */ 609 isXMLNode: function() 610 { 611 return !!this.ownerDocument && !!this.ownerDocument.xmlVersion; 612 }, 613 614 /** 615 * @param {boolean} optimized 616 * @return {string} 617 */ 618 xPath: function(optimized) 619 { 620 if (this._nodeType === Node.DOCUMENT_NODE) 621 return "/"; 622 623 var steps = []; 624 var contextNode = this; 625 while (contextNode) { 626 var step = contextNode._xPathValue(optimized); 627 if (!step) 628 break; // Error - bail out early. 629 steps.push(step); 630 if (step.optimized) 631 break; 632 contextNode = contextNode.parentNode; 633 } 634 635 steps.reverse(); 636 return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/"); 637 }, 638 639 /** 640 * @param {boolean} optimized 641 * @return {WebInspector.DOMNode.XPathStep} 642 */ 643 _xPathValue: function(optimized) 644 { 645 var ownValue; 646 var ownIndex = this._xPathIndex(); 647 if (ownIndex === -1) 648 return null; // Error. 649 650 switch (this._nodeType) { 651 case Node.ELEMENT_NODE: 652 if (optimized && this.getAttribute("id")) 653 return new WebInspector.DOMNode.XPathStep("//*[@id=\"" + this.getAttribute("id") + "\"]", true); 654 ownValue = this._localName; 655 break; 656 case Node.ATTRIBUTE_NODE: 657 ownValue = "@" + this._nodeName; 658 break; 659 case Node.TEXT_NODE: 660 case Node.CDATA_SECTION_NODE: 661 ownValue = "text()"; 662 break; 663 case Node.PROCESSING_INSTRUCTION_NODE: 664 ownValue = "processing-instruction()"; 665 break; 666 case Node.COMMENT_NODE: 667 ownValue = "comment()"; 668 break; 669 case Node.DOCUMENT_NODE: 670 ownValue = ""; 671 break; 672 default: 673 ownValue = ""; 674 break; 675 } 676 677 if (ownIndex > 0) 678 ownValue += "[" + ownIndex + "]"; 679 680 return new WebInspector.DOMNode.XPathStep(ownValue, this._nodeType === Node.DOCUMENT_NODE); 681 }, 682 683 /** 684 * @return {number} 685 */ 686 _xPathIndex: function() 687 { 688 // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise. 689 function areNodesSimilar(left, right) 690 { 691 if (left === right) 692 return true; 693 694 if (left._nodeType === Node.ELEMENT_NODE && right._nodeType === Node.ELEMENT_NODE) 695 return left._localName === right._localName; 696 697 if (left._nodeType === right._nodeType) 698 return true; 699 700 // XPath treats CDATA as text nodes. 701 var leftType = left._nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left._nodeType; 702 var rightType = right._nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right._nodeType; 703 return leftType === rightType; 704 } 705 706 var siblings = this.parentNode ? this.parentNode._children : null; 707 if (!siblings) 708 return 0; // Root node - no siblings. 709 var hasSameNamedElements; 710 for (var i = 0; i < siblings.length; ++i) { 711 if (areNodesSimilar(this, siblings[i]) && siblings[i] !== this) { 712 hasSameNamedElements = true; 713 break; 714 } 715 } 716 if (!hasSameNamedElements) 717 return 0; 718 var ownIndex = 1; // XPath indices start with 1. 719 for (var i = 0; i < siblings.length; ++i) { 720 if (areNodesSimilar(this, siblings[i])) { 721 if (siblings[i] === this) 722 return ownIndex; 723 ++ownIndex; 724 } 725 } 726 return -1; // An error occurred: |this| not found in parent's children. 727 }, 728 729 _updateChildUserPropertyCountsOnRemoval: function(parentNode) 730 { 731 var result = {}; 732 if (this._userProperties) { 733 for (var name in this._userProperties) 734 result[name] = (result[name] || 0) + 1; 735 } 736 737 if (this._descendantUserPropertyCounters) { 738 for (var name in this._descendantUserPropertyCounters) { 739 var counter = this._descendantUserPropertyCounters[name]; 740 result[name] = (result[name] || 0) + counter; 741 } 742 } 743 744 for (var name in result) 745 parentNode._updateDescendantUserPropertyCount(name, -result[name]); 746 }, 747 748 _updateDescendantUserPropertyCount: function(name, delta) 749 { 750 if (!this._descendantUserPropertyCounters.hasOwnProperty(name)) 751 this._descendantUserPropertyCounters[name] = 0; 752 this._descendantUserPropertyCounters[name] += delta; 753 if (!this._descendantUserPropertyCounters[name]) 754 delete this._descendantUserPropertyCounters[name]; 755 if (this.parentNode) 756 this.parentNode._updateDescendantUserPropertyCount(name, delta); 757 }, 758 759 setUserProperty: function(name, value) 760 { 761 if (value === null) { 762 this.removeUserProperty(name); 763 return; 764 } 765 766 if (this.parentNode && !this._userProperties.hasOwnProperty(name)) 767 this.parentNode._updateDescendantUserPropertyCount(name, 1); 768 769 this._userProperties[name] = value; 770 }, 771 772 removeUserProperty: function(name) 773 { 774 if (!this._userProperties.hasOwnProperty(name)) 775 return; 776 777 delete this._userProperties[name]; 778 if (this.parentNode) 779 this.parentNode._updateDescendantUserPropertyCount(name, -1); 780 }, 781 782 getUserProperty: function(name) 783 { 784 return this._userProperties ? this._userProperties[name] : null; 785 }, 786 787 descendantUserPropertyCount: function(name) 788 { 789 return this._descendantUserPropertyCounters && this._descendantUserPropertyCounters[name] ? this._descendantUserPropertyCounters[name] : 0; 790 }, 791 792 /** 793 * @param {string} url 794 * @return {?string} 795 */ 796 resolveURL: function(url) 797 { 798 if (!url) 799 return url; 800 for (var frameOwnerCandidate = this; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) { 801 if (frameOwnerCandidate.baseURL) 802 return WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, url); 803 } 804 return null; 805 } 806 } 807 808 /** 809 * @extends {WebInspector.DOMNode} 810 * @constructor 811 * @param {WebInspector.DOMAgent} domAgent 812 * @param {DOMAgent.Node} payload 813 */ 814 WebInspector.DOMDocument = function(domAgent, payload) 815 { 816 WebInspector.DOMNode.call(this, domAgent, this, false, payload); 817 this.documentURL = payload.documentURL || ""; 818 this.baseURL = /** @type {string} */ (payload.baseURL); 819 console.assert(this.baseURL); 820 this.xmlVersion = payload.xmlVersion; 821 this._listeners = {}; 822 } 823 824 WebInspector.DOMDocument.prototype = { 825 __proto__: WebInspector.DOMNode.prototype 826 } 827 828 /** 829 * @extends {WebInspector.Object} 830 * @constructor 831 */ 832 WebInspector.DOMAgent = function() { 833 /** @type {Object|undefined} */ 834 this._idToDOMNode = {}; 835 this._document = null; 836 this._attributeLoadNodeIds = {}; 837 InspectorBackend.registerDOMDispatcher(new WebInspector.DOMDispatcher(this)); 838 } 839 840 WebInspector.DOMAgent.Events = { 841 AttrModified: "AttrModified", 842 AttrRemoved: "AttrRemoved", 843 CharacterDataModified: "CharacterDataModified", 844 NodeInserted: "NodeInserted", 845 NodeRemoved: "NodeRemoved", 846 DocumentUpdated: "DocumentUpdated", 847 ChildNodeCountUpdated: "ChildNodeCountUpdated", 848 UndoRedoRequested: "UndoRedoRequested", 849 UndoRedoCompleted: "UndoRedoCompleted", 850 InspectNodeRequested: "InspectNodeRequested" 851 } 852 853 WebInspector.DOMAgent.prototype = { 854 /** 855 * @param {function(WebInspector.DOMDocument)=} callback 856 */ 857 requestDocument: function(callback) 858 { 859 if (this._document) { 860 if (callback) 861 callback(this._document); 862 return; 863 } 864 865 if (this._pendingDocumentRequestCallbacks) { 866 this._pendingDocumentRequestCallbacks.push(callback); 867 return; 868 } 869 870 this._pendingDocumentRequestCallbacks = [callback]; 871 872 /** 873 * @this {WebInspector.DOMAgent} 874 * @param {?Protocol.Error} error 875 * @param {DOMAgent.Node} root 876 */ 877 function onDocumentAvailable(error, root) 878 { 879 if (!error) 880 this._setDocument(root); 881 882 for (var i = 0; i < this._pendingDocumentRequestCallbacks.length; ++i) { 883 var callback = this._pendingDocumentRequestCallbacks[i]; 884 if (callback) 885 callback(this._document); 886 } 887 delete this._pendingDocumentRequestCallbacks; 888 } 889 890 DOMAgent.getDocument(onDocumentAvailable.bind(this)); 891 }, 892 893 /** 894 * @return {WebInspector.DOMDocument?} 895 */ 896 existingDocument: function() 897 { 898 return this._document; 899 }, 900 901 /** 902 * @param {RuntimeAgent.RemoteObjectId} objectId 903 * @param {function(?DOMAgent.NodeId)=} callback 904 */ 905 pushNodeToFrontend: function(objectId, callback) 906 { 907 this._dispatchWhenDocumentAvailable(DOMAgent.requestNode.bind(DOMAgent, objectId), callback); 908 }, 909 910 /** 911 * @param {string} path 912 * @param {function(?number)=} callback 913 */ 914 pushNodeByPathToFrontend: function(path, callback) 915 { 916 this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent, path), callback); 917 }, 918 919 /** 920 * @param {number} backendNodeId 921 * @param {function(?number)=} callback 922 */ 923 pushNodeByBackendIdToFrontend: function(backendNodeId, callback) 924 { 925 this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByBackendIdToFrontend.bind(DOMAgent, backendNodeId), callback); 926 }, 927 928 /** 929 * @param {function(T)=} callback 930 * @return {function(?Protocol.Error, T=)|undefined} 931 * @template T 932 */ 933 _wrapClientCallback: function(callback) 934 { 935 if (!callback) 936 return; 937 /** 938 * @param {?Protocol.Error} error 939 * @param {*=} result 940 */ 941 return function(error, result) 942 { 943 // Caller is responsible for handling the actual error. 944 callback(error ? null : result); 945 } 946 }, 947 948 /** 949 * @param {function(function(?Protocol.Error, T=)=)} func 950 * @param {function(T)=} callback 951 * @template T 952 */ 953 _dispatchWhenDocumentAvailable: function(func, callback) 954 { 955 var callbackWrapper = this._wrapClientCallback(callback); 956 957 function onDocumentAvailable() 958 { 959 if (this._document) 960 func(callbackWrapper); 961 else { 962 if (callbackWrapper) 963 callbackWrapper("No document"); 964 } 965 } 966 this.requestDocument(onDocumentAvailable.bind(this)); 967 }, 968 969 /** 970 * @param {DOMAgent.NodeId} nodeId 971 * @param {string} name 972 * @param {string} value 973 */ 974 _attributeModified: function(nodeId, name, value) 975 { 976 var node = this._idToDOMNode[nodeId]; 977 if (!node) 978 return; 979 980 node._setAttribute(name, value); 981 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, { node: node, name: name }); 982 }, 983 984 /** 985 * @param {DOMAgent.NodeId} nodeId 986 * @param {string} name 987 */ 988 _attributeRemoved: function(nodeId, name) 989 { 990 var node = this._idToDOMNode[nodeId]; 991 if (!node) 992 return; 993 node._removeAttribute(name); 994 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrRemoved, { node: node, name: name }); 995 }, 996 997 /** 998 * @param {Array.<DOMAgent.NodeId>} nodeIds 999 */ 1000 _inlineStyleInvalidated: function(nodeIds) 1001 { 1002 for (var i = 0; i < nodeIds.length; ++i) 1003 this._attributeLoadNodeIds[nodeIds[i]] = true; 1004 if ("_loadNodeAttributesTimeout" in this) 1005 return; 1006 this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0); 1007 }, 1008 1009 _loadNodeAttributes: function() 1010 { 1011 /** 1012 * @this {WebInspector.DOMAgent} 1013 * @param {DOMAgent.NodeId} nodeId 1014 * @param {?Protocol.Error} error 1015 * @param {Array.<string>} attributes 1016 */ 1017 function callback(nodeId, error, attributes) 1018 { 1019 if (error) { 1020 // We are calling _loadNodeAttributes asynchronously, it is ok if node is not found. 1021 return; 1022 } 1023 var node = this._idToDOMNode[nodeId]; 1024 if (node) { 1025 if (node._setAttributesPayload(attributes)) 1026 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, { node: node, name: "style" }); 1027 } 1028 } 1029 1030 delete this._loadNodeAttributesTimeout; 1031 1032 for (var nodeId in this._attributeLoadNodeIds) { 1033 var nodeIdAsNumber = parseInt(nodeId, 10); 1034 DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber)); 1035 } 1036 this._attributeLoadNodeIds = {}; 1037 }, 1038 1039 /** 1040 * @param {DOMAgent.NodeId} nodeId 1041 * @param {string} newValue 1042 */ 1043 _characterDataModified: function(nodeId, newValue) 1044 { 1045 var node = this._idToDOMNode[nodeId]; 1046 node._nodeValue = newValue; 1047 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.CharacterDataModified, node); 1048 }, 1049 1050 /** 1051 * @param {DOMAgent.NodeId} nodeId 1052 * @return {WebInspector.DOMNode|undefined} 1053 */ 1054 nodeForId: function(nodeId) 1055 { 1056 return this._idToDOMNode[nodeId]; 1057 }, 1058 1059 _documentUpdated: function() 1060 { 1061 this._setDocument(null); 1062 }, 1063 1064 /** 1065 * @param {DOMAgent.Node} payload 1066 */ 1067 _setDocument: function(payload) 1068 { 1069 this._idToDOMNode = {}; 1070 if (payload && "nodeId" in payload) 1071 this._document = new WebInspector.DOMDocument(this, payload); 1072 else 1073 this._document = null; 1074 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.DocumentUpdated, this._document); 1075 }, 1076 1077 /** 1078 * @param {DOMAgent.Node} payload 1079 */ 1080 _setDetachedRoot: function(payload) 1081 { 1082 if (payload.nodeName === "#document") 1083 new WebInspector.DOMDocument(this, payload); 1084 else 1085 new WebInspector.DOMNode(this, null, false, payload); 1086 }, 1087 1088 /** 1089 * @param {DOMAgent.NodeId} parentId 1090 * @param {Array.<DOMAgent.Node>} payloads 1091 */ 1092 _setChildNodes: function(parentId, payloads) 1093 { 1094 if (!parentId && payloads.length) { 1095 this._setDetachedRoot(payloads[0]); 1096 return; 1097 } 1098 1099 var parent = this._idToDOMNode[parentId]; 1100 parent._setChildrenPayload(payloads); 1101 }, 1102 1103 /** 1104 * @param {DOMAgent.NodeId} nodeId 1105 * @param {number} newValue 1106 */ 1107 _childNodeCountUpdated: function(nodeId, newValue) 1108 { 1109 var node = this._idToDOMNode[nodeId]; 1110 node._childNodeCount = newValue; 1111 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, node); 1112 }, 1113 1114 /** 1115 * @param {DOMAgent.NodeId} parentId 1116 * @param {DOMAgent.NodeId} prevId 1117 * @param {DOMAgent.Node} payload 1118 */ 1119 _childNodeInserted: function(parentId, prevId, payload) 1120 { 1121 var parent = this._idToDOMNode[parentId]; 1122 var prev = this._idToDOMNode[prevId]; 1123 var node = parent._insertChild(prev, payload); 1124 this._idToDOMNode[node.id] = node; 1125 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeInserted, node); 1126 }, 1127 1128 /** 1129 * @param {DOMAgent.NodeId} parentId 1130 * @param {DOMAgent.NodeId} nodeId 1131 */ 1132 _childNodeRemoved: function(parentId, nodeId) 1133 { 1134 var parent = this._idToDOMNode[parentId]; 1135 var node = this._idToDOMNode[nodeId]; 1136 parent._removeChild(node); 1137 this._unbind(node); 1138 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeRemoved, {node: node, parent: parent}); 1139 }, 1140 1141 /** 1142 * @param {DOMAgent.NodeId} hostId 1143 * @param {DOMAgent.Node} root 1144 */ 1145 _shadowRootPushed: function(hostId, root) 1146 { 1147 var host = this._idToDOMNode[hostId]; 1148 if (!host) 1149 return; 1150 var node = new WebInspector.DOMNode(this, host.ownerDocument, true, root); 1151 node.parentNode = host; 1152 this._idToDOMNode[node.id] = node; 1153 host._shadowRoots.push(node); 1154 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeInserted, node); 1155 }, 1156 1157 /** 1158 * @param {DOMAgent.NodeId} hostId 1159 * @param {DOMAgent.NodeId} rootId 1160 */ 1161 _shadowRootPopped: function(hostId, rootId) 1162 { 1163 var host = this._idToDOMNode[hostId]; 1164 if (!host) 1165 return; 1166 var root = this._idToDOMNode[rootId]; 1167 if (!root) 1168 return; 1169 host._shadowRoots.remove(root); 1170 this._unbind(root); 1171 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeRemoved, {node: root, parent: host}); 1172 }, 1173 1174 /** 1175 * @param {WebInspector.DOMNode} node 1176 */ 1177 _unbind: function(node) 1178 { 1179 delete this._idToDOMNode[node.id]; 1180 for (var i = 0; node._children && i < node._children.length; ++i) 1181 this._unbind(node._children[i]); 1182 for (var i = 0; i < node._shadowRoots.length; ++i) 1183 this._unbind(node._shadowRoots[i]); 1184 if (node._templateContent) 1185 this._unbind(node._templateContent); 1186 }, 1187 1188 /** 1189 * @param {number} nodeId 1190 */ 1191 inspectElement: function(nodeId) 1192 { 1193 var node = this._idToDOMNode[nodeId]; 1194 if (node) 1195 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.InspectNodeRequested, nodeId); 1196 }, 1197 1198 /** 1199 * @param {DOMAgent.NodeId} nodeId 1200 */ 1201 _inspectNodeRequested: function(nodeId) 1202 { 1203 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.InspectNodeRequested, nodeId); 1204 }, 1205 1206 /** 1207 * @param {string} query 1208 * @param {function(number)} searchCallback 1209 */ 1210 performSearch: function(query, searchCallback) 1211 { 1212 this.cancelSearch(); 1213 1214 /** 1215 * @param {?Protocol.Error} error 1216 * @param {string} searchId 1217 * @param {number} resultsCount 1218 */ 1219 function callback(error, searchId, resultsCount) 1220 { 1221 this._searchId = searchId; 1222 searchCallback(resultsCount); 1223 } 1224 DOMAgent.performSearch(query, callback.bind(this)); 1225 }, 1226 1227 /** 1228 * @param {number} index 1229 * @param {?function(DOMAgent.Node)} callback 1230 */ 1231 searchResult: function(index, callback) 1232 { 1233 if (this._searchId) { 1234 /** 1235 * @param {?Protocol.Error} error 1236 * @param {Array.<number>} nodeIds 1237 */ 1238 function mycallback(error, nodeIds) 1239 { 1240 if (error) { 1241 console.error(error); 1242 callback(null); 1243 return; 1244 } 1245 if (nodeIds.length != 1) 1246 return; 1247 1248 callback(this._idToDOMNode[nodeIds[0]]); 1249 } 1250 DOMAgent.getSearchResults(this._searchId, index, index + 1, mycallback.bind(this)); 1251 } else 1252 callback(null); 1253 }, 1254 1255 cancelSearch: function() 1256 { 1257 if (this._searchId) { 1258 DOMAgent.discardSearchResults(this._searchId); 1259 delete this._searchId; 1260 } 1261 }, 1262 1263 /** 1264 * @param {DOMAgent.NodeId} nodeId 1265 * @param {string} selectors 1266 * @param {function(?DOMAgent.NodeId)=} callback 1267 */ 1268 querySelector: function(nodeId, selectors, callback) 1269 { 1270 DOMAgent.querySelector(nodeId, selectors, this._wrapClientCallback(callback)); 1271 }, 1272 1273 /** 1274 * @param {DOMAgent.NodeId} nodeId 1275 * @param {string} selectors 1276 * @param {function(?Array.<DOMAgent.NodeId>)=} callback 1277 */ 1278 querySelectorAll: function(nodeId, selectors, callback) 1279 { 1280 DOMAgent.querySelectorAll(nodeId, selectors, this._wrapClientCallback(callback)); 1281 }, 1282 1283 /** 1284 * @param {DOMAgent.NodeId=} nodeId 1285 * @param {string=} mode 1286 * @param {RuntimeAgent.RemoteObjectId=} objectId 1287 */ 1288 highlightDOMNode: function(nodeId, mode, objectId) 1289 { 1290 if (this._hideDOMNodeHighlightTimeout) { 1291 clearTimeout(this._hideDOMNodeHighlightTimeout); 1292 delete this._hideDOMNodeHighlightTimeout; 1293 } 1294 1295 if (objectId || nodeId) 1296 DOMAgent.highlightNode(this._buildHighlightConfig(mode), objectId ? undefined : nodeId, objectId); 1297 else 1298 DOMAgent.hideHighlight(); 1299 }, 1300 1301 hideDOMNodeHighlight: function() 1302 { 1303 this.highlightDOMNode(0); 1304 }, 1305 1306 /** 1307 * @param {DOMAgent.NodeId} nodeId 1308 */ 1309 highlightDOMNodeForTwoSeconds: function(nodeId) 1310 { 1311 this.highlightDOMNode(nodeId); 1312 this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000); 1313 }, 1314 1315 /** 1316 * @param {boolean} enabled 1317 * @param {boolean} inspectShadowDOM 1318 * @param {function(?Protocol.Error)=} callback 1319 */ 1320 setInspectModeEnabled: function(enabled, inspectShadowDOM, callback) 1321 { 1322 this._dispatchWhenDocumentAvailable(DOMAgent.setInspectModeEnabled.bind(DOMAgent, enabled, inspectShadowDOM, this._buildHighlightConfig()), callback); 1323 }, 1324 1325 /** 1326 * @param {string=} mode 1327 */ 1328 _buildHighlightConfig: function(mode) 1329 { 1330 mode = mode || "all"; 1331 var highlightConfig = { showInfo: mode === "all", showRulers: WebInspector.settings.showMetricsRulers.get() }; 1332 if (mode === "all" || mode === "content") 1333 highlightConfig.contentColor = WebInspector.Color.PageHighlight.Content.toProtocolRGBA(); 1334 1335 if (mode === "all" || mode === "padding") 1336 highlightConfig.paddingColor = WebInspector.Color.PageHighlight.Padding.toProtocolRGBA(); 1337 1338 if (mode === "all" || mode === "border") 1339 highlightConfig.borderColor = WebInspector.Color.PageHighlight.Border.toProtocolRGBA(); 1340 1341 if (mode === "all" || mode === "margin") 1342 highlightConfig.marginColor = WebInspector.Color.PageHighlight.Margin.toProtocolRGBA(); 1343 1344 if (mode === "all") 1345 highlightConfig.eventTargetColor = WebInspector.Color.PageHighlight.EventTarget.toProtocolRGBA(); 1346 1347 return highlightConfig; 1348 }, 1349 1350 /** 1351 * @param {WebInspector.DOMNode} node 1352 * @param {function(?Protocol.Error)=} callback 1353 * @return {function(?Protocol.Error)} 1354 */ 1355 _markRevision: function(node, callback) 1356 { 1357 function wrapperFunction(error) 1358 { 1359 if (!error) 1360 this.markUndoableState(); 1361 1362 if (callback) 1363 callback.apply(this, arguments); 1364 } 1365 return wrapperFunction.bind(this); 1366 }, 1367 1368 /** 1369 * @param {boolean} emulationEnabled 1370 */ 1371 emulateTouchEventObjects: function(emulationEnabled) 1372 { 1373 const injectedFunction = function() { 1374 const touchEvents = ["ontouchstart", "ontouchend", "ontouchmove", "ontouchcancel"]; 1375 var recepients = [window.__proto__, document.__proto__]; 1376 for (var i = 0; i < touchEvents.length; ++i) { 1377 for (var j = 0; j < recepients.length; ++j) { 1378 if (!(touchEvents[i] in recepients[j])) 1379 Object.defineProperty(recepients[j], touchEvents[i], { value: null, writable: true, configurable: true, enumerable: true }); 1380 } 1381 } 1382 } 1383 1384 if (emulationEnabled && !this._addTouchEventsScriptInjecting) { 1385 this._addTouchEventsScriptInjecting = true; 1386 PageAgent.addScriptToEvaluateOnLoad("(" + injectedFunction.toString() + ")()", scriptAddedCallback.bind(this)); 1387 } else { 1388 if (typeof this._addTouchEventsScriptId !== "undefined") { 1389 PageAgent.removeScriptToEvaluateOnLoad(this._addTouchEventsScriptId); 1390 delete this._addTouchEventsScriptId; 1391 } 1392 } 1393 1394 function scriptAddedCallback(error, scriptId) 1395 { 1396 delete this._addTouchEventsScriptInjecting; 1397 if (error) 1398 return; 1399 this._addTouchEventsScriptId = scriptId; 1400 } 1401 1402 PageAgent.setTouchEmulationEnabled(emulationEnabled); 1403 }, 1404 1405 markUndoableState: function() 1406 { 1407 DOMAgent.markUndoableState(); 1408 }, 1409 1410 /** 1411 * @param {function(?Protocol.Error)=} callback 1412 */ 1413 undo: function(callback) 1414 { 1415 function mycallback(error) 1416 { 1417 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoCompleted); 1418 callback(error); 1419 } 1420 1421 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoRequested); 1422 DOMAgent.undo(callback); 1423 }, 1424 1425 /** 1426 * @param {function(?Protocol.Error)=} callback 1427 */ 1428 redo: function(callback) 1429 { 1430 function mycallback(error) 1431 { 1432 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoCompleted); 1433 callback(error); 1434 } 1435 1436 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.UndoRedoRequested); 1437 DOMAgent.redo(callback); 1438 }, 1439 1440 __proto__: WebInspector.Object.prototype 1441 } 1442 1443 /** 1444 * @constructor 1445 * @implements {DOMAgent.Dispatcher} 1446 * @param {WebInspector.DOMAgent} domAgent 1447 */ 1448 WebInspector.DOMDispatcher = function(domAgent) 1449 { 1450 this._domAgent = domAgent; 1451 } 1452 1453 WebInspector.DOMDispatcher.prototype = { 1454 documentUpdated: function() 1455 { 1456 this._domAgent._documentUpdated(); 1457 }, 1458 1459 /** 1460 * @param {DOMAgent.NodeId} nodeId 1461 */ 1462 inspectNodeRequested: function(nodeId) 1463 { 1464 this._domAgent._inspectNodeRequested(nodeId); 1465 }, 1466 1467 /** 1468 * @param {DOMAgent.NodeId} nodeId 1469 * @param {string} name 1470 * @param {string} value 1471 */ 1472 attributeModified: function(nodeId, name, value) 1473 { 1474 this._domAgent._attributeModified(nodeId, name, value); 1475 }, 1476 1477 /** 1478 * @param {DOMAgent.NodeId} nodeId 1479 * @param {string} name 1480 */ 1481 attributeRemoved: function(nodeId, name) 1482 { 1483 this._domAgent._attributeRemoved(nodeId, name); 1484 }, 1485 1486 /** 1487 * @param {Array.<DOMAgent.NodeId>} nodeIds 1488 */ 1489 inlineStyleInvalidated: function(nodeIds) 1490 { 1491 this._domAgent._inlineStyleInvalidated(nodeIds); 1492 }, 1493 1494 /** 1495 * @param {DOMAgent.NodeId} nodeId 1496 * @param {string} characterData 1497 */ 1498 characterDataModified: function(nodeId, characterData) 1499 { 1500 this._domAgent._characterDataModified(nodeId, characterData); 1501 }, 1502 1503 /** 1504 * @param {DOMAgent.NodeId} parentId 1505 * @param {Array.<DOMAgent.Node>} payloads 1506 */ 1507 setChildNodes: function(parentId, payloads) 1508 { 1509 this._domAgent._setChildNodes(parentId, payloads); 1510 }, 1511 1512 /** 1513 * @param {DOMAgent.NodeId} nodeId 1514 * @param {number} childNodeCount 1515 */ 1516 childNodeCountUpdated: function(nodeId, childNodeCount) 1517 { 1518 this._domAgent._childNodeCountUpdated(nodeId, childNodeCount); 1519 }, 1520 1521 /** 1522 * @param {DOMAgent.NodeId} parentNodeId 1523 * @param {DOMAgent.NodeId} previousNodeId 1524 * @param {DOMAgent.Node} payload 1525 */ 1526 childNodeInserted: function(parentNodeId, previousNodeId, payload) 1527 { 1528 this._domAgent._childNodeInserted(parentNodeId, previousNodeId, payload); 1529 }, 1530 1531 /** 1532 * @param {DOMAgent.NodeId} parentNodeId 1533 * @param {DOMAgent.NodeId} nodeId 1534 */ 1535 childNodeRemoved: function(parentNodeId, nodeId) 1536 { 1537 this._domAgent._childNodeRemoved(parentNodeId, nodeId); 1538 }, 1539 1540 /** 1541 * @param {DOMAgent.NodeId} hostId 1542 * @param {DOMAgent.Node} root 1543 */ 1544 shadowRootPushed: function(hostId, root) 1545 { 1546 this._domAgent._shadowRootPushed(hostId, root); 1547 }, 1548 1549 /** 1550 * @param {DOMAgent.NodeId} hostId 1551 * @param {DOMAgent.NodeId} rootId 1552 */ 1553 shadowRootPopped: function(hostId, rootId) 1554 { 1555 this._domAgent._shadowRootPopped(hostId, rootId); 1556 } 1557 } 1558 1559 /** 1560 * @type {?WebInspector.DOMAgent} 1561 */ 1562 WebInspector.domAgent = null; 1563