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 32WebInspector.DOMNode = function(doc, payload) { 33 this.ownerDocument = doc; 34 35 this.id = payload.id; 36 this._nodeType = payload.nodeType; 37 this._nodeName = payload.nodeName; 38 this._localName = payload.localName; 39 this._nodeValue = payload.nodeValue; 40 41 this._attributes = []; 42 this._attributesMap = {}; 43 if (payload.attributes) 44 this._setAttributesPayload(payload.attributes); 45 46 this._childNodeCount = payload.childNodeCount; 47 this.children = null; 48 49 this.nextSibling = null; 50 this.prevSibling = null; 51 this.firstChild = null; 52 this.lastChild = null; 53 this.parentNode = null; 54 55 if (payload.children) 56 this._setChildrenPayload(payload.children); 57 58 this._computedStyle = null; 59 this.style = null; 60 this._matchedCSSRules = []; 61 62 if (this._nodeType === Node.ELEMENT_NODE) { 63 // HTML and BODY from internal iframes should not overwrite top-level ones. 64 if (!this.ownerDocument.documentElement && this._nodeName === "HTML") 65 this.ownerDocument.documentElement = this; 66 if (!this.ownerDocument.body && this._nodeName === "BODY") 67 this.ownerDocument.body = this; 68 if (payload.documentURL) 69 this.documentURL = payload.documentURL; 70 } else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) { 71 this.publicId = payload.publicId; 72 this.systemId = payload.systemId; 73 this.internalSubset = payload.internalSubset; 74 } else if (this._nodeType === Node.DOCUMENT_NODE) { 75 this.documentURL = payload.documentURL; 76 } else if (this._nodeType === Node.ATTRIBUTE_NODE) { 77 this.name = payload.name; 78 this.value = payload.value; 79 } 80} 81 82WebInspector.DOMNode.prototype = { 83 hasAttributes: function() 84 { 85 return this._attributes.length > 0; 86 }, 87 88 hasChildNodes: function() 89 { 90 return this._childNodeCount > 0; 91 }, 92 93 nodeType: function() 94 { 95 return this._nodeType; 96 }, 97 98 nodeName: function() 99 { 100 return this._nodeName; 101 }, 102 103 setNodeName: function(name, callback) 104 { 105 DOMAgent.setNodeName(this.id, name, callback); 106 }, 107 108 localName: function() 109 { 110 return this._localName; 111 }, 112 113 nodeValue: function() 114 { 115 return this._nodeValue; 116 }, 117 118 setNodeValue: function(value, callback) 119 { 120 DOMAgent.setNodeValue(this.id, value, callback); 121 }, 122 123 getAttribute: function(name) 124 { 125 var attr = this._attributesMap[name]; 126 return attr ? attr.value : undefined; 127 }, 128 129 setAttribute: function(name, value, callback) 130 { 131 function mycallback(error) 132 { 133 if (!error) { 134 var attr = this._attributesMap[name]; 135 if (attr) 136 attr.value = value; 137 else 138 attr = this._addAttribute(name, value); 139 } 140 141 if (callback) 142 callback(); 143 } 144 DOMAgent.setAttribute(this.id, name, value, mycallback.bind(this)); 145 }, 146 147 attributes: function() 148 { 149 return this._attributes; 150 }, 151 152 removeAttribute: function(name, callback) 153 { 154 function mycallback(error, success) 155 { 156 if (!error) { 157 delete this._attributesMap[name]; 158 for (var i = 0; i < this._attributes.length; ++i) { 159 if (this._attributes[i].name === name) { 160 this._attributes.splice(i, 1); 161 break; 162 } 163 } 164 } 165 166 if (callback) 167 callback(); 168 } 169 DOMAgent.removeAttribute(this.id, name, mycallback.bind(this)); 170 }, 171 172 getChildNodes: function(callback) 173 { 174 if (this.children) { 175 if (callback) 176 callback(this.children); 177 return; 178 } 179 180 function mycallback(error) { 181 if (!error && callback) 182 callback(this.children); 183 } 184 DOMAgent.getChildNodes(this.id, mycallback.bind(this)); 185 }, 186 187 getOuterHTML: function(callback) 188 { 189 DOMAgent.getOuterHTML(this.id, callback); 190 }, 191 192 setOuterHTML: function(html, callback) 193 { 194 DOMAgent.setOuterHTML(this.id, html, callback); 195 }, 196 197 removeNode: function(callback) 198 { 199 DOMAgent.removeNode(this.id, callback); 200 }, 201 202 copyNode: function(callback) 203 { 204 DOMAgent.copyNode(this.id, callback); 205 }, 206 207 eventListeners: function(callback) 208 { 209 DOMAgent.getEventListenersForNode(this.id, callback); 210 }, 211 212 path: function() 213 { 214 var path = []; 215 var node = this; 216 while (node && "index" in node && node._nodeName.length) { 217 path.push([node.index, node._nodeName]); 218 node = node.parentNode; 219 } 220 path.reverse(); 221 return path.join(","); 222 }, 223 224 appropriateSelectorFor: function(justSelector) 225 { 226 var lowerCaseName = this.localName() || node.nodeName().toLowerCase(); 227 228 var id = this.getAttribute("id"); 229 if (id) { 230 var selector = "#" + id; 231 return (justSelector ? selector : lowerCaseName + selector); 232 } 233 234 var className = this.getAttribute("class"); 235 if (className) { 236 var selector = "." + className.replace(/\s+/, "."); 237 return (justSelector ? selector : lowerCaseName + selector); 238 } 239 240 if (lowerCaseName === "input" && this.getAttribute("type")) 241 return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]"; 242 243 return lowerCaseName; 244 }, 245 246 _setAttributesPayload: function(attrs) 247 { 248 this._attributes = []; 249 this._attributesMap = {}; 250 for (var i = 0; i < attrs.length; i += 2) 251 this._addAttribute(attrs[i], attrs[i + 1]); 252 }, 253 254 _insertChild: function(prev, payload) 255 { 256 var node = new WebInspector.DOMNode(this.ownerDocument, payload); 257 if (!prev) { 258 if (!this.children) { 259 // First node 260 this.children = [ node ]; 261 } else 262 this.children.unshift(node); 263 } else 264 this.children.splice(this.children.indexOf(prev) + 1, 0, node); 265 this._renumber(); 266 return node; 267 }, 268 269 removeChild_: function(node) 270 { 271 this.children.splice(this.children.indexOf(node), 1); 272 node.parentNode = null; 273 this._renumber(); 274 }, 275 276 _setChildrenPayload: function(payloads) 277 { 278 this.children = []; 279 for (var i = 0; i < payloads.length; ++i) { 280 var payload = payloads[i]; 281 var node = new WebInspector.DOMNode(this.ownerDocument, payload); 282 this.children.push(node); 283 } 284 this._renumber(); 285 }, 286 287 _renumber: function() 288 { 289 this._childNodeCount = this.children.length; 290 if (this._childNodeCount == 0) { 291 this.firstChild = null; 292 this.lastChild = null; 293 return; 294 } 295 this.firstChild = this.children[0]; 296 this.lastChild = this.children[this._childNodeCount - 1]; 297 for (var i = 0; i < this._childNodeCount; ++i) { 298 var child = this.children[i]; 299 child.index = i; 300 child.nextSibling = i + 1 < this._childNodeCount ? this.children[i + 1] : null; 301 child.prevSibling = i - 1 >= 0 ? this.children[i - 1] : null; 302 child.parentNode = this; 303 } 304 }, 305 306 _addAttribute: function(name, value) 307 { 308 var attr = { 309 "name": name, 310 "value": value, 311 "_node": this 312 }; 313 this._attributesMap[name] = attr; 314 this._attributes.push(attr); 315 }, 316 317 ownerDocumentElement: function() 318 { 319 // document element is the child of the document / frame owner node that has documentURL property. 320 // FIXME: return document nodes as a part of the DOM tree structure. 321 var node = this; 322 while (node.parentNode && !node.parentNode.documentURL) 323 node = node.parentNode; 324 return node; 325 } 326} 327 328WebInspector.DOMDocument = function(domAgent, payload) 329{ 330 WebInspector.DOMNode.call(this, this, payload); 331 this._listeners = {}; 332 this._domAgent = domAgent; 333} 334 335WebInspector.DOMDocument.prototype.__proto__ = WebInspector.DOMNode.prototype; 336 337WebInspector.DOMAgent = function() { 338 this._idToDOMNode = null; 339 this._document = null; 340 InspectorBackend.registerDomainDispatcher("DOM", new WebInspector.DOMDispatcher(this)); 341} 342 343WebInspector.DOMAgent.Events = { 344 AttrModified: "AttrModified", 345 CharacterDataModified: "CharacterDataModified", 346 NodeInserted: "NodeInserted", 347 NodeRemoved: "NodeRemoved", 348 DocumentUpdated: "DocumentUpdated", 349 ChildNodeCountUpdated: "ChildNodeCountUpdated" 350} 351 352WebInspector.DOMAgent.prototype = { 353 requestDocument: function(callback) 354 { 355 if (this._document) { 356 if (callback) 357 callback(this._document); 358 return; 359 } 360 361 if (this._pendingDocumentRequestCallbacks) { 362 this._pendingDocumentRequestCallbacks.push(callback); 363 return; 364 } 365 366 this._pendingDocumentRequestCallbacks = [callback]; 367 368 function onDocumentAvailable(error, root) 369 { 370 if (!error) 371 this._setDocument(root); 372 373 for (var i = 0; i < this._pendingDocumentRequestCallbacks.length; ++i) { 374 var callback = this._pendingDocumentRequestCallbacks[i]; 375 if (callback) 376 callback(this._document); 377 } 378 delete this._pendingDocumentRequestCallbacks; 379 } 380 381 DOMAgent.getDocument(onDocumentAvailable.bind(this)); 382 }, 383 384 pushNodeToFrontend: function(objectId, callback) 385 { 386 this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeToFrontend.bind(DOMAgent), objectId, callback); 387 }, 388 389 pushNodeByPathToFrontend: function(path, callback) 390 { 391 this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent), path, callback); 392 }, 393 394 _wrapClientCallback: function(callback) 395 { 396 if (!callback) 397 return; 398 return function(error, result) 399 { 400 if (error) 401 console.error("Error during DOMAgent operation: " + error); 402 callback(error ? null : result); 403 } 404 }, 405 406 _dispatchWhenDocumentAvailable: function(action) 407 { 408 var requestArguments = Array.prototype.slice.call(arguments, 1); 409 var callbackWrapper; 410 411 if (typeof requestArguments[requestArguments.length - 1] === "function") { 412 var callback = requestArguments.pop(); 413 callbackWrapper = this._wrapClientCallback(callback); 414 requestArguments.push(callbackWrapper); 415 } 416 function onDocumentAvailable() 417 { 418 if (this._document) 419 action.apply(null, requestArguments); 420 else { 421 if (callbackWrapper) 422 callbackWrapper("No document"); 423 } 424 } 425 this.requestDocument(onDocumentAvailable.bind(this)); 426 }, 427 428 _attributesUpdated: function(nodeId, attrsArray) 429 { 430 var node = this._idToDOMNode[nodeId]; 431 node._setAttributesPayload(attrsArray); 432 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.AttrModified, node); 433 }, 434 435 _characterDataModified: function(nodeId, newValue) 436 { 437 var node = this._idToDOMNode[nodeId]; 438 node._nodeValue = newValue; 439 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.CharacterDataModified, node); 440 }, 441 442 nodeForId: function(nodeId) 443 { 444 return this._idToDOMNode[nodeId]; 445 }, 446 447 _documentUpdated: function() 448 { 449 this._setDocument(null); 450 this.requestDocument(); 451 }, 452 453 _setDocument: function(payload) 454 { 455 this._idToDOMNode = {}; 456 if (payload && "id" in payload) { 457 this._document = new WebInspector.DOMDocument(this, payload); 458 this._idToDOMNode[payload.id] = this._document; 459 if (this._document.children) 460 this._bindNodes(this._document.children); 461 } else 462 this._document = null; 463 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.DocumentUpdated, this._document); 464 }, 465 466 _setDetachedRoot: function(payload) 467 { 468 var root = new WebInspector.DOMNode(this._document, payload); 469 this._idToDOMNode[payload.id] = root; 470 }, 471 472 _setChildNodes: function(parentId, payloads) 473 { 474 if (!parentId && payloads.length) { 475 this._setDetachedRoot(payloads[0]); 476 return; 477 } 478 479 var parent = this._idToDOMNode[parentId]; 480 parent._setChildrenPayload(payloads); 481 this._bindNodes(parent.children); 482 }, 483 484 _bindNodes: function(children) 485 { 486 for (var i = 0; i < children.length; ++i) { 487 var child = children[i]; 488 this._idToDOMNode[child.id] = child; 489 if (child.children) 490 this._bindNodes(child.children); 491 } 492 }, 493 494 _childNodeCountUpdated: function(nodeId, newValue) 495 { 496 var node = this._idToDOMNode[nodeId]; 497 node._childNodeCount = newValue; 498 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, node); 499 }, 500 501 _childNodeInserted: function(parentId, prevId, payload) 502 { 503 var parent = this._idToDOMNode[parentId]; 504 var prev = this._idToDOMNode[prevId]; 505 var node = parent._insertChild(prev, payload); 506 this._idToDOMNode[node.id] = node; 507 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeInserted, node); 508 }, 509 510 _childNodeRemoved: function(parentId, nodeId) 511 { 512 var parent = this._idToDOMNode[parentId]; 513 var node = this._idToDOMNode[nodeId]; 514 parent.removeChild_(node); 515 this.dispatchEventToListeners(WebInspector.DOMAgent.Events.NodeRemoved, {node:node, parent:parent}); 516 delete this._idToDOMNode[nodeId]; 517 if (Preferences.nativeInstrumentationEnabled) 518 WebInspector.panels.elements.sidebarPanes.domBreakpoints.nodeRemoved(node); 519 }, 520 521 performSearch: function(query, searchResultCollector, searchSynchronously) 522 { 523 this._searchResultCollector = searchResultCollector; 524 DOMAgent.performSearch(query, !!searchSynchronously); 525 }, 526 527 cancelSearch: function() 528 { 529 delete this._searchResultCollector; 530 DOMAgent.cancelSearch(); 531 }, 532 533 querySelector: function(nodeId, selectors, callback) 534 { 535 DOMAgent.querySelector(nodeId, selectors, this._wrapClientCallback(callback)); 536 }, 537 538 querySelectorAll: function(nodeId, selectors, callback) 539 { 540 DOMAgent.querySelectorAll(nodeId, selectors, this._wrapClientCallback(callback)); 541 } 542} 543 544WebInspector.DOMAgent.prototype.__proto__ = WebInspector.Object.prototype; 545 546WebInspector.DOMDispatcher = function(domAgent) 547{ 548 this._domAgent = domAgent; 549} 550 551WebInspector.DOMDispatcher.prototype = { 552 documentUpdated: function() 553 { 554 this._domAgent._documentUpdated(); 555 }, 556 557 attributesUpdated: function(nodeId, attrsArray) 558 { 559 this._domAgent._attributesUpdated(nodeId, attrsArray); 560 }, 561 562 characterDataModified: function(nodeId, newValue) 563 { 564 this._domAgent._characterDataModified(nodeId, newValue); 565 }, 566 567 setChildNodes: function(parentId, payloads) 568 { 569 this._domAgent._setChildNodes(parentId, payloads); 570 }, 571 572 childNodeCountUpdated: function(nodeId, newValue) 573 { 574 this._domAgent._childNodeCountUpdated(nodeId, newValue); 575 }, 576 577 childNodeInserted: function(parentId, prevId, payload) 578 { 579 this._domAgent._childNodeInserted(parentId, prevId, payload); 580 }, 581 582 childNodeRemoved: function(parentId, nodeId) 583 { 584 this._domAgent._childNodeRemoved(parentId, nodeId); 585 }, 586 587 inspectElementRequested: function(nodeId) 588 { 589 WebInspector.updateFocusedNode(nodeId); 590 }, 591 592 searchResults: function(nodeIds) 593 { 594 if (this._domAgent._searchResultCollector) 595 this._domAgent._searchResultCollector(nodeIds); 596 } 597} 598