1 /* 2 * Copyright (C) 2011 Google 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 are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.NativeBreakpointsSidebarPane} 34 * @implements {WebInspector.TargetManager.Observer} 35 */ 36 WebInspector.DOMBreakpointsSidebarPane = function() 37 { 38 WebInspector.NativeBreakpointsSidebarPane.call(this, WebInspector.UIString("DOM Breakpoints")); 39 40 this._breakpointElements = {}; 41 42 this._breakpointTypes = { 43 SubtreeModified: "subtree-modified", 44 AttributeModified: "attribute-modified", 45 NodeRemoved: "node-removed" 46 }; 47 this._breakpointTypeLabels = {}; 48 this._breakpointTypeLabels[this._breakpointTypes.SubtreeModified] = WebInspector.UIString("Subtree Modified"); 49 this._breakpointTypeLabels[this._breakpointTypes.AttributeModified] = WebInspector.UIString("Attribute Modified"); 50 this._breakpointTypeLabels[this._breakpointTypes.NodeRemoved] = WebInspector.UIString("Node Removed"); 51 52 this._contextMenuLabels = {}; 53 this._contextMenuLabels[this._breakpointTypes.SubtreeModified] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Subtree modifications" : "Subtree Modifications"); 54 this._contextMenuLabels[this._breakpointTypes.AttributeModified] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Attributes modifications" : "Attributes Modifications"); 55 this._contextMenuLabels[this._breakpointTypes.NodeRemoved] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Node removal" : "Node Removal"); 56 57 WebInspector.targetManager.observeTargets(this); 58 } 59 60 WebInspector.DOMBreakpointsSidebarPane.prototype = { 61 /** 62 * @param {!WebInspector.Target} target 63 */ 64 targetAdded: function(target) 65 { 66 target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._inspectedURLChanged, this); 67 target.domModel.addEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); 68 }, 69 70 /** 71 * @param {!WebInspector.Target} target 72 */ 73 targetRemoved: function(target) 74 { 75 target.resourceTreeModel.removeEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._inspectedURLChanged, this); 76 target.domModel.removeEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); 77 }, 78 79 _inspectedURLChanged: function(event) 80 { 81 this._breakpointElements = {}; 82 this._reset(); 83 var url = /** @type {string} */ (event.data); 84 this._inspectedURL = url.removeURLFragment(); 85 }, 86 87 /** 88 * @param {!WebInspector.DOMNode} node 89 * @param {!WebInspector.ContextMenu} contextMenu 90 */ 91 populateNodeContextMenu: function(node, contextMenu) 92 { 93 if (node.pseudoType()) 94 return; 95 96 var nodeBreakpoints = {}; 97 for (var id in this._breakpointElements) { 98 var element = this._breakpointElements[id]; 99 if (element._node === node) 100 nodeBreakpoints[element._type] = true; 101 } 102 103 /** 104 * @param {string} type 105 * @this {WebInspector.DOMBreakpointsSidebarPane} 106 */ 107 function toggleBreakpoint(type) 108 { 109 if (!nodeBreakpoints[type]) 110 this._setBreakpoint(node, type, true); 111 else 112 this._removeBreakpoint(node, type); 113 this._saveBreakpoints(); 114 } 115 116 var breakPointSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Break on...")); 117 for (var key in this._breakpointTypes) { 118 var type = this._breakpointTypes[key]; 119 var label = this._contextMenuLabels[type]; 120 breakPointSubMenu.appendCheckboxItem(label, toggleBreakpoint.bind(this, type), nodeBreakpoints[type]); 121 } 122 }, 123 124 /** 125 * @param {!WebInspector.DebuggerPausedDetails} details 126 * @param {function(!Element)} callback 127 */ 128 createBreakpointHitStatusMessage: function(details, callback) 129 { 130 var auxData = /** @type {!Object} */ (details.auxData); 131 var domModel = details.target().domModel; 132 if (auxData.type === this._breakpointTypes.SubtreeModified) { 133 var targetNodeObject = details.target().runtimeModel.createRemoteObject(auxData["targetNode"]); 134 targetNodeObject.pushNodeToFrontend(didPushNodeToFrontend.bind(this)); 135 } else { 136 this._doCreateBreakpointHitStatusMessage(auxData, domModel.nodeForId(auxData.nodeId), null, callback); 137 } 138 139 /** 140 * @param {?WebInspector.DOMNode} targetNode 141 * @this {WebInspector.DOMBreakpointsSidebarPane} 142 */ 143 function didPushNodeToFrontend(targetNode) 144 { 145 if (targetNode) 146 targetNodeObject.release(); 147 this._doCreateBreakpointHitStatusMessage(auxData, domModel.nodeForId(auxData.nodeId), targetNode, callback); 148 } 149 }, 150 151 /** 152 * @param {!Object} auxData 153 * @param {?WebInspector.DOMNode} node 154 * @param {?WebInspector.DOMNode} targetNode 155 * @param {function(!Element)} callback 156 */ 157 _doCreateBreakpointHitStatusMessage: function(auxData, node, targetNode, callback) 158 { 159 var message; 160 var typeLabel = this._breakpointTypeLabels[auxData.type]; 161 var linkifiedNode = WebInspector.DOMPresentationUtils.linkifyNodeReference(node); 162 var substitutions = [typeLabel, linkifiedNode]; 163 var targetNodeLink = ""; 164 if (targetNode) 165 targetNodeLink = WebInspector.DOMPresentationUtils.linkifyNodeReference(targetNode); 166 167 if (auxData.type === this._breakpointTypes.SubtreeModified) { 168 if (auxData.insertion) { 169 if (targetNode !== node) { 170 message = "Paused on a \"%s\" breakpoint set on %s, because a new child was added to its descendant %s."; 171 substitutions.push(targetNodeLink); 172 } else 173 message = "Paused on a \"%s\" breakpoint set on %s, because a new child was added to that node."; 174 } else { 175 message = "Paused on a \"%s\" breakpoint set on %s, because its descendant %s was removed."; 176 substitutions.push(targetNodeLink); 177 } 178 } else 179 message = "Paused on a \"%s\" breakpoint set on %s."; 180 181 var element = document.createElement("span"); 182 var formatters = { 183 s: function(substitution) 184 { 185 return substitution; 186 } 187 }; 188 function append(a, b) 189 { 190 if (typeof b === "string") 191 b = document.createTextNode(b); 192 element.appendChild(b); 193 } 194 WebInspector.formatLocalized(message, substitutions, formatters, "", append); 195 196 callback(element); 197 }, 198 199 _nodeRemoved: function(event) 200 { 201 var node = event.data.node; 202 this._removeBreakpointsForNode(event.data.node); 203 var children = node.children(); 204 if (!children) 205 return; 206 for (var i = 0; i < children.length; ++i) 207 this._removeBreakpointsForNode(children[i]); 208 this._saveBreakpoints(); 209 }, 210 211 _removeBreakpointsForNode: function(node) 212 { 213 for (var id in this._breakpointElements) { 214 var element = this._breakpointElements[id]; 215 if (element._node === node) 216 this._removeBreakpoint(element._node, element._type); 217 } 218 }, 219 220 _setBreakpoint: function(node, type, enabled) 221 { 222 var breakpointId = this._createBreakpointId(node.id, type); 223 if (breakpointId in this._breakpointElements) 224 return; 225 226 var element = document.createElement("li"); 227 element._node = node; 228 element._type = type; 229 element.addEventListener("contextmenu", this._contextMenu.bind(this, node, type), true); 230 231 var checkboxElement = document.createElement("input"); 232 checkboxElement.className = "checkbox-elem"; 233 checkboxElement.type = "checkbox"; 234 checkboxElement.checked = enabled; 235 checkboxElement.addEventListener("click", this._checkboxClicked.bind(this, node, type), false); 236 element._checkboxElement = checkboxElement; 237 element.appendChild(checkboxElement); 238 239 var labelElement = document.createElement("span"); 240 element.appendChild(labelElement); 241 242 var linkifiedNode = WebInspector.DOMPresentationUtils.linkifyNodeReference(node); 243 linkifiedNode.classList.add("monospace"); 244 labelElement.appendChild(linkifiedNode); 245 246 var description = document.createElement("div"); 247 description.className = "source-text"; 248 description.textContent = this._breakpointTypeLabels[type]; 249 labelElement.appendChild(description); 250 251 var currentElement = this.listElement.firstChild; 252 while (currentElement) { 253 if (currentElement._type && currentElement._type < element._type) 254 break; 255 currentElement = currentElement.nextSibling; 256 } 257 this._addListElement(element, currentElement); 258 this._breakpointElements[breakpointId] = element; 259 if (enabled) 260 DOMDebuggerAgent.setDOMBreakpoint(node.id, type); 261 }, 262 263 _removeAllBreakpoints: function() 264 { 265 for (var id in this._breakpointElements) { 266 var element = this._breakpointElements[id]; 267 this._removeBreakpoint(element._node, element._type); 268 } 269 this._saveBreakpoints(); 270 }, 271 272 _removeBreakpoint: function(node, type) 273 { 274 var breakpointId = this._createBreakpointId(node.id, type); 275 var element = this._breakpointElements[breakpointId]; 276 if (!element) 277 return; 278 279 this._removeListElement(element); 280 delete this._breakpointElements[breakpointId]; 281 if (element._checkboxElement.checked) 282 DOMDebuggerAgent.removeDOMBreakpoint(node.id, type); 283 }, 284 285 _contextMenu: function(node, type, event) 286 { 287 var contextMenu = new WebInspector.ContextMenu(event); 288 289 /** 290 * @this {WebInspector.DOMBreakpointsSidebarPane} 291 */ 292 function removeBreakpoint() 293 { 294 this._removeBreakpoint(node, type); 295 this._saveBreakpoints(); 296 } 297 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove breakpoint" : "Remove Breakpoint"), removeBreakpoint.bind(this)); 298 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove all DOM breakpoints" : "Remove All DOM Breakpoints"), this._removeAllBreakpoints.bind(this)); 299 contextMenu.show(); 300 }, 301 302 _checkboxClicked: function(node, type, event) 303 { 304 if (event.target.checked) 305 DOMDebuggerAgent.setDOMBreakpoint(node.id, type); 306 else 307 DOMDebuggerAgent.removeDOMBreakpoint(node.id, type); 308 this._saveBreakpoints(); 309 }, 310 311 highlightBreakpoint: function(auxData) 312 { 313 var breakpointId = this._createBreakpointId(auxData.nodeId, auxData.type); 314 var element = this._breakpointElements[breakpointId]; 315 if (!element) 316 return; 317 this.expand(); 318 element.classList.add("breakpoint-hit"); 319 this._highlightedElement = element; 320 }, 321 322 clearBreakpointHighlight: function() 323 { 324 if (this._highlightedElement) { 325 this._highlightedElement.classList.remove("breakpoint-hit"); 326 delete this._highlightedElement; 327 } 328 }, 329 330 _createBreakpointId: function(nodeId, type) 331 { 332 return nodeId + ":" + type; 333 }, 334 335 _saveBreakpoints: function() 336 { 337 var breakpoints = []; 338 var storedBreakpoints = WebInspector.settings.domBreakpoints.get(); 339 for (var i = 0; i < storedBreakpoints.length; ++i) { 340 var breakpoint = storedBreakpoints[i]; 341 if (breakpoint.url !== this._inspectedURL) 342 breakpoints.push(breakpoint); 343 } 344 for (var id in this._breakpointElements) { 345 var element = this._breakpointElements[id]; 346 breakpoints.push({ url: this._inspectedURL, path: element._node.path(), type: element._type, enabled: element._checkboxElement.checked }); 347 } 348 WebInspector.settings.domBreakpoints.set(breakpoints); 349 }, 350 351 /** 352 * @param {!WebInspector.Target} target 353 */ 354 restoreBreakpoints: function(target) 355 { 356 var pathToBreakpoints = {}; 357 358 /** 359 * @param {string} path 360 * @param {?DOMAgent.NodeId} nodeId 361 * @this {WebInspector.DOMBreakpointsSidebarPane} 362 */ 363 function didPushNodeByPathToFrontend(path, nodeId) 364 { 365 var node = nodeId ? target.domModel.nodeForId(nodeId) : null; 366 if (!node) 367 return; 368 369 var breakpoints = pathToBreakpoints[path]; 370 for (var i = 0; i < breakpoints.length; ++i) 371 this._setBreakpoint(node, breakpoints[i].type, breakpoints[i].enabled); 372 } 373 374 var breakpoints = WebInspector.settings.domBreakpoints.get(); 375 for (var i = 0; i < breakpoints.length; ++i) { 376 var breakpoint = breakpoints[i]; 377 if (breakpoint.url !== this._inspectedURL) 378 continue; 379 var path = breakpoint.path; 380 if (!pathToBreakpoints[path]) { 381 pathToBreakpoints[path] = []; 382 target.domModel.pushNodeByPathToFrontend(path, didPushNodeByPathToFrontend.bind(this, path)); 383 } 384 pathToBreakpoints[path].push(breakpoint); 385 } 386 }, 387 388 /** 389 * @param {!WebInspector.Panel} panel 390 * @return {!WebInspector.DOMBreakpointsSidebarPane.Proxy} 391 */ 392 createProxy: function(panel) 393 { 394 var proxy = new WebInspector.DOMBreakpointsSidebarPane.Proxy(this, panel); 395 if (!this._proxies) 396 this._proxies = []; 397 this._proxies.push(proxy); 398 return proxy; 399 }, 400 401 onContentReady: function() 402 { 403 for (var i = 0; i != this._proxies.length; i++) 404 this._proxies[i].onContentReady(); 405 }, 406 407 __proto__: WebInspector.NativeBreakpointsSidebarPane.prototype 408 } 409 410 /** 411 * @constructor 412 * @extends {WebInspector.SidebarPane} 413 * @param {!WebInspector.DOMBreakpointsSidebarPane} pane 414 * @param {!WebInspector.Panel} panel 415 */ 416 WebInspector.DOMBreakpointsSidebarPane.Proxy = function(pane, panel) 417 { 418 WebInspector.View._assert(!pane.titleElement.firstChild, "Cannot create proxy for a sidebar pane with a toolbar"); 419 420 WebInspector.SidebarPane.call(this, pane.title()); 421 this.registerRequiredCSS("breakpointsList.css"); 422 423 this._wrappedPane = pane; 424 this._panel = panel; 425 426 this.bodyElement.remove(); 427 this.bodyElement = this._wrappedPane.bodyElement; 428 } 429 430 WebInspector.DOMBreakpointsSidebarPane.Proxy.prototype = { 431 expand: function() 432 { 433 this._wrappedPane.expand(); 434 }, 435 436 onContentReady: function() 437 { 438 if (this._panel.isShowing()) 439 this._reattachBody(); 440 441 WebInspector.SidebarPane.prototype.onContentReady.call(this); 442 }, 443 444 wasShown: function() 445 { 446 WebInspector.SidebarPane.prototype.wasShown.call(this); 447 this._reattachBody(); 448 }, 449 450 _reattachBody: function() 451 { 452 if (this.bodyElement.parentNode !== this.element) 453 this.element.appendChild(this.bodyElement); 454 }, 455 456 __proto__: WebInspector.SidebarPane.prototype 457 } 458 459 /** 460 * @type {!WebInspector.DOMBreakpointsSidebarPane} 461 */ 462 WebInspector.domBreakpointsSidebarPane; 463