1/* 2 * Copyright (C) 2013 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.DOMTree = function(frame) 27{ 28 WebInspector.Object.call(this); 29 30 this._frame = frame; 31 32 this._rootDOMNode = null; 33 this._requestIdentifier = 0; 34 this._flowMap = {}; 35 36 this._frame.addEventListener(WebInspector.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextChanged, this); 37 38 WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.DocumentUpdated, this._documentUpdated, this); 39 40 // Only add extra event listeners when not the main frame. Since DocumentUpdated is enough for the main frame. 41 if (!this._frame.isMainFrame()) { 42 WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.NodeRemoved, this._nodeRemoved, this); 43 this._frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._frameMainResourceDidChange, this); 44 } 45 46 WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.ContentFlowListWasUpdated, this._contentFlowListWasUpdated, this); 47 WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.ContentFlowWasAdded, this._contentFlowWasAdded, this); 48 WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.ContentFlowWasRemoved, this._contentFlowWasRemoved, this); 49}; 50 51WebInspector.Object.addConstructorFunctions(WebInspector.DOMTree); 52 53WebInspector.DOMTree.Event = { 54 RootDOMNodeInvalidated: "dom-tree-root-dom-node-invalidated", 55 ContentFlowWasAdded: "dom-tree-content-flow-was-added", 56 ContentFlowWasRemoved: "dom-tree-content-flow-was-removed" 57}; 58 59WebInspector.DOMTree.prototype = { 60 constructor: WebInspector.DOMTree, 61 62 // Public 63 64 get frame() 65 { 66 return this._frame; 67 }, 68 69 get flowMap() 70 { 71 return this._flowMap; 72 }, 73 74 get flowsCount() 75 { 76 return Object.keys(this._flowMap).length; 77 }, 78 79 invalidate: function() 80 { 81 // Set to null so it is fetched again next time requestRootDOMNode is called. 82 this._rootDOMNode = null; 83 84 // Clear the pending callbacks. It is the responsibility of the client to listen for 85 // the RootDOMNodeInvalidated event and request the root DOM node again. 86 delete this._pendingRootDOMNodeRequests; 87 88 if (this._invalidateTimeoutIdentifier) 89 return; 90 91 function performInvalidate() 92 { 93 delete this._invalidateTimeoutIdentifier; 94 95 this.dispatchEventToListeners(WebInspector.DOMTree.Event.RootDOMNodeInvalidated); 96 } 97 98 // Delay the invalidation on a timeout to coalesce multiple calls to invalidate. 99 this._invalidateTimeoutIdentifier = setTimeout(performInvalidate.bind(this), 0); 100 }, 101 102 requestRootDOMNode: function(callback) 103 { 104 console.assert(typeof callback === "function"); 105 if (typeof callback !== "function") 106 return; 107 108 if (this._rootDOMNode) { 109 callback(this._rootDOMNode); 110 return; 111 } 112 113 if (!this._frame.isMainFrame() && WebInspector.ExecutionContext.supported() && !this._frame.pageExecutionContext) { 114 this._rootDOMNodeRequestWaitingForExecutionContext = true; 115 if (!this._pendingRootDOMNodeRequests) 116 this._pendingRootDOMNodeRequests = []; 117 this._pendingRootDOMNodeRequests.push(callback); 118 return; 119 } 120 121 if (this._pendingRootDOMNodeRequests) { 122 this._pendingRootDOMNodeRequests.push(callback); 123 return; 124 } 125 126 this._pendingRootDOMNodeRequests = [callback]; 127 this._requestRootDOMNode(); 128 }, 129 130 // Private 131 132 _requestRootDOMNode: function() 133 { 134 console.assert(this._frame.isMainFrame() || !WebInspector.ExecutionContext.supported() || this._frame.pageExecutionContext); 135 console.assert(this._pendingRootDOMNodeRequests.length); 136 137 // Bump the request identifier. This prevents pending callbacks for previous requests from completing. 138 var requestIdentifier = ++this._requestIdentifier; 139 140 function rootObjectAvailable(error, result) 141 { 142 // Check to see if we have been invalidated (if the callbacks were cleared). 143 if (!this._pendingRootDOMNodeRequests || requestIdentifier != this._requestIdentifier) 144 return; 145 146 if (error) { 147 console.error(JSON.stringify(error)); 148 149 this._rootDOMNode = null; 150 dispatchCallbacks.call(this); 151 return; 152 } 153 154 // Convert the RemoteObject to a DOMNode by asking the backend to push it to us. 155 var remoteObject = WebInspector.RemoteObject.fromPayload(result); 156 remoteObject.pushNodeToFrontend(rootDOMNodeAvailable.bind(this, remoteObject)); 157 } 158 159 function rootDOMNodeAvailable(remoteObject, nodeId) 160 { 161 remoteObject.release(); 162 163 // Check to see if we have been invalidated (if the callbacks were cleared). 164 if (!this._pendingRootDOMNodeRequests || requestIdentifier != this._requestIdentifier) 165 return; 166 167 if (!nodeId) { 168 this._rootDOMNode = null; 169 dispatchCallbacks.call(this); 170 return; 171 } 172 173 this._rootDOMNode = WebInspector.domTreeManager.nodeForId(nodeId); 174 175 console.assert(this._rootDOMNode); 176 if (!this._rootDOMNode) { 177 dispatchCallbacks.call(this); 178 return; 179 } 180 181 // Request the child nodes since the root node is often not shown in the UI, 182 // and the child nodes will be needed immediately. 183 this._rootDOMNode.getChildNodes(dispatchCallbacks.bind(this)); 184 } 185 186 function mainDocumentAvailable(document) 187 { 188 this._rootDOMNode = document; 189 190 dispatchCallbacks.call(this); 191 } 192 193 function dispatchCallbacks() 194 { 195 // Check to see if we have been invalidated (if the callbacks were cleared). 196 if (!this._pendingRootDOMNodeRequests || requestIdentifier != this._requestIdentifier) 197 return; 198 199 for (var i = 0; i < this._pendingRootDOMNodeRequests.length; ++i) 200 this._pendingRootDOMNodeRequests[i](this._rootDOMNode); 201 delete this._pendingRootDOMNodeRequests; 202 } 203 204 // For the main frame we can use the more straight forward requestDocument function. For 205 // child frames we need to do a more roundabout approach since the protocol does not include 206 // a specific way to request a document given a frame identifier. The child frame approach 207 // involves evaluating the JavaScript "document" and resolving that into a DOMNode. 208 if (this._frame.isMainFrame()) 209 WebInspector.domTreeManager.requestDocument(mainDocumentAvailable.bind(this)); 210 else { 211 // COMPATIBILITY (iOS 6): Execution context identifiers (contextId) did not exist 212 // in iOS 6. Fallback to including the frame identifier (frameId). 213 var contextId = this._frame.pageExecutionContext ? this._frame.pageExecutionContext.id : undefined; 214 RuntimeAgent.evaluate.invoke({expression: "document", objectGroup: "", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, contextId: contextId, frameId: this._frame.id, returnByValue: false, generatePreview: false}, rootObjectAvailable.bind(this)); 215 } 216 }, 217 218 _nodeRemoved: function(event) 219 { 220 console.assert(!this._frame.isMainFrame()); 221 222 if (event.data.node !== this._rootDOMNode) 223 return; 224 225 this.invalidate(); 226 }, 227 228 _documentUpdated: function(event) 229 { 230 this.invalidate(); 231 }, 232 233 _frameMainResourceDidChange: function(event) 234 { 235 console.assert(!this._frame.isMainFrame()); 236 237 this.invalidate(); 238 }, 239 240 _framePageExecutionContextChanged: function(event) 241 { 242 if (this._rootDOMNodeRequestWaitingForExecutionContext) { 243 console.assert(this._frame.pageExecutionContext); 244 console.assert(this._pendingRootDOMNodeRequests && this._pendingRootDOMNodeRequests.length); 245 246 delete this._rootDOMNodeRequestWaitingForExecutionContext; 247 248 this._requestRootDOMNode(); 249 } 250 }, 251 252 requestContentFlowList: function() 253 { 254 this.requestRootDOMNode(function(rootNode) { 255 // Let the backend know we are interested about the named flow events for this document. 256 WebInspector.domTreeManager.getNamedFlowCollection(rootNode.id); 257 }); 258 }, 259 260 _isContentFlowInCurrentDocument: function(flow) 261 { 262 return this._rootDOMNode && this._rootDOMNode.id === flow.documentNodeIdentifier; 263 }, 264 265 _contentFlowListWasUpdated: function(event) 266 { 267 if (!this._rootDOMNode || this._rootDOMNode.id !== event.data.documentNodeIdentifier) 268 return; 269 270 // Assume that all the flows have been removed. 271 var deletedFlows = {}; 272 for (var flowId in this._flowMap) 273 deletedFlows[flowId] = this._flowMap[flowId]; 274 275 var newFlows = []; 276 277 var flows = event.data.flows; 278 for (var i = 0; i < flows.length; ++i) { 279 var flow = flows[i]; 280 // All the flows received from WebKit are part of the same document. 281 console.assert(this._isContentFlowInCurrentDocument(flow)); 282 283 var flowId = flow.id; 284 if (this._flowMap.hasOwnProperty(flowId)) { 285 // Remove the flow name from the deleted list. 286 console.assert(deletedFlows.hasOwnProperty(flowId)); 287 delete deletedFlows[flowId]; 288 } else { 289 this._flowMap[flowId] = flow; 290 newFlows.push(flow); 291 } 292 } 293 294 for (var flowId in deletedFlows) { 295 delete this._flowMap[flowId]; 296 } 297 298 // Send update events to listeners. 299 300 for (var flowId in deletedFlows) 301 this.dispatchEventToListeners(WebInspector.DOMTree.Event.ContentFlowWasRemoved, {flow: deletedFlows[flowId]}); 302 303 for (var i = 0; i < newFlows.length; ++i) 304 this.dispatchEventToListeners(WebInspector.DOMTree.Event.ContentFlowWasAdded, {flow: newFlows[i]}); 305 }, 306 307 _contentFlowWasAdded: function(event) 308 { 309 var flow = event.data.flow; 310 if (!this._isContentFlowInCurrentDocument(flow)) 311 return; 312 313 var flowId = flow.id; 314 console.assert(!this._flowMap.hasOwnProperty(flowId)); 315 this._flowMap[flowId] = flow; 316 317 this.dispatchEventToListeners(WebInspector.DOMTree.Event.ContentFlowWasAdded, {flow: flow}); 318 }, 319 320 _contentFlowWasRemoved: function(event) 321 { 322 var flow = event.data.flow; 323 if (!this._isContentFlowInCurrentDocument(flow)) 324 return; 325 326 var flowId = flow.id; 327 console.assert(this._flowMap.hasOwnProperty(flowId)); 328 delete this._flowMap[flowId]; 329 330 this.dispatchEventToListeners(WebInspector.DOMTree.Event.ContentFlowWasRemoved, {flow: flow}); 331 } 332}; 333 334WebInspector.DOMTree.prototype.__proto__ = WebInspector.Object.prototype; 335