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 */
35WebInspector.DOMBreakpointsSidebarPane = function()
36{
37    WebInspector.NativeBreakpointsSidebarPane.call(this, WebInspector.UIString("DOM Breakpoints"));
38
39    this._breakpointElements = {};
40
41    this._breakpointTypes = {
42        SubtreeModified: "subtree-modified",
43        AttributeModified: "attribute-modified",
44        NodeRemoved: "node-removed"
45    };
46    this._breakpointTypeLabels = {};
47    this._breakpointTypeLabels[this._breakpointTypes.SubtreeModified] = WebInspector.UIString("Subtree Modified");
48    this._breakpointTypeLabels[this._breakpointTypes.AttributeModified] = WebInspector.UIString("Attribute Modified");
49    this._breakpointTypeLabels[this._breakpointTypes.NodeRemoved] = WebInspector.UIString("Node Removed");
50
51    this._contextMenuLabels = {};
52    this._contextMenuLabels[this._breakpointTypes.SubtreeModified] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Subtree modifications" : "Subtree Modifications");
53    this._contextMenuLabels[this._breakpointTypes.AttributeModified] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Attributes modifications" : "Attributes Modifications");
54    this._contextMenuLabels[this._breakpointTypes.NodeRemoved] = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Node removal" : "Node Removal");
55
56    WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._inspectedURLChanged, this);
57    WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
58}
59
60WebInspector.DOMBreakpointsSidebarPane.prototype = {
61    _inspectedURLChanged: function(event)
62    {
63        this._breakpointElements = {};
64        this._reset();
65        var url = /** @type {string} */ (event.data);
66        this._inspectedURL = url.removeURLFragment();
67    },
68
69    /**
70     * @param {!WebInspector.DOMNode} node
71     * @param {!WebInspector.ContextMenu} contextMenu
72     */
73    populateNodeContextMenu: function(node, contextMenu)
74    {
75        if (node.pseudoType())
76            return;
77
78        var nodeBreakpoints = {};
79        for (var id in this._breakpointElements) {
80            var element = this._breakpointElements[id];
81            if (element._node === node)
82                nodeBreakpoints[element._type] = true;
83        }
84
85        /**
86         * @param {string} type
87         * @this {WebInspector.DOMBreakpointsSidebarPane}
88         */
89        function toggleBreakpoint(type)
90        {
91            if (!nodeBreakpoints[type])
92                this._setBreakpoint(node, type, true);
93            else
94                this._removeBreakpoint(node, type);
95            this._saveBreakpoints();
96        }
97
98        var breakPointSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Break on..."));
99        for (var key in this._breakpointTypes) {
100            var type = this._breakpointTypes[key];
101            var label = this._contextMenuLabels[type];
102            breakPointSubMenu.appendCheckboxItem(label, toggleBreakpoint.bind(this, type), nodeBreakpoints[type]);
103        }
104    },
105
106    createBreakpointHitStatusMessage: function(auxData, callback)
107    {
108        if (auxData.type === this._breakpointTypes.SubtreeModified) {
109            var targetNodeObject = WebInspector.RemoteObject.fromPayload(auxData["targetNode"]);
110            targetNodeObject.pushNodeToFrontend(didPushNodeToFrontend.bind(this));
111        } else
112            this._doCreateBreakpointHitStatusMessage(auxData, null, callback);
113
114        /**
115         * @param {?DOMAgent.NodeId} targetNodeId
116         * @this {WebInspector.DOMBreakpointsSidebarPane}
117         */
118        function didPushNodeToFrontend(targetNodeId)
119        {
120            if (targetNodeId)
121                targetNodeObject.release();
122            this._doCreateBreakpointHitStatusMessage(auxData, targetNodeId, callback);
123        }
124    },
125
126    _doCreateBreakpointHitStatusMessage: function(auxData, targetNodeId, callback)
127    {
128        var message;
129        var typeLabel = this._breakpointTypeLabels[auxData.type];
130        var linkifiedNode = WebInspector.DOMPresentationUtils.linkifyNodeById(auxData.nodeId);
131        var substitutions = [typeLabel, linkifiedNode];
132        var targetNode = "";
133        if (targetNodeId)
134            targetNode = WebInspector.DOMPresentationUtils.linkifyNodeById(targetNodeId);
135
136        if (auxData.type === this._breakpointTypes.SubtreeModified) {
137            if (auxData.insertion) {
138                if (targetNodeId !== auxData.nodeId) {
139                    message = "Paused on a \"%s\" breakpoint set on %s, because a new child was added to its descendant %s.";
140                    substitutions.push(targetNode);
141                } else
142                    message = "Paused on a \"%s\" breakpoint set on %s, because a new child was added to that node.";
143            } else {
144                message = "Paused on a \"%s\" breakpoint set on %s, because its descendant %s was removed.";
145                substitutions.push(targetNode);
146            }
147        } else
148            message = "Paused on a \"%s\" breakpoint set on %s.";
149
150        var element = document.createElement("span");
151        var formatters = {
152            s: function(substitution)
153            {
154                return substitution;
155            }
156        };
157        function append(a, b)
158        {
159            if (typeof b === "string")
160                b = document.createTextNode(b);
161            element.appendChild(b);
162        }
163        WebInspector.formatLocalized(message, substitutions, formatters, "", append);
164
165        callback(element);
166    },
167
168    _nodeRemoved: function(event)
169    {
170        var node = event.data.node;
171        this._removeBreakpointsForNode(event.data.node);
172        var children = node.children();
173        if (!children)
174            return;
175        for (var i = 0; i < children.length; ++i)
176            this._removeBreakpointsForNode(children[i]);
177        this._saveBreakpoints();
178    },
179
180    _removeBreakpointsForNode: function(node)
181    {
182        for (var id in this._breakpointElements) {
183            var element = this._breakpointElements[id];
184            if (element._node === node)
185                this._removeBreakpoint(element._node, element._type);
186        }
187    },
188
189    _setBreakpoint: function(node, type, enabled)
190    {
191        var breakpointId = this._createBreakpointId(node.id, type);
192        if (breakpointId in this._breakpointElements)
193            return;
194
195        var element = document.createElement("li");
196        element._node = node;
197        element._type = type;
198        element.addEventListener("contextmenu", this._contextMenu.bind(this, node, type), true);
199
200        var checkboxElement = document.createElement("input");
201        checkboxElement.className = "checkbox-elem";
202        checkboxElement.type = "checkbox";
203        checkboxElement.checked = enabled;
204        checkboxElement.addEventListener("click", this._checkboxClicked.bind(this, node, type), false);
205        element._checkboxElement = checkboxElement;
206        element.appendChild(checkboxElement);
207
208        var labelElement = document.createElement("span");
209        element.appendChild(labelElement);
210
211        var linkifiedNode = WebInspector.DOMPresentationUtils.linkifyNodeById(node.id);
212        linkifiedNode.classList.add("monospace");
213        labelElement.appendChild(linkifiedNode);
214
215        var description = document.createElement("div");
216        description.className = "source-text";
217        description.textContent = this._breakpointTypeLabels[type];
218        labelElement.appendChild(description);
219
220        var currentElement = this.listElement.firstChild;
221        while (currentElement) {
222            if (currentElement._type && currentElement._type < element._type)
223                break;
224            currentElement = currentElement.nextSibling;
225        }
226        this._addListElement(element, currentElement);
227        this._breakpointElements[breakpointId] = element;
228        if (enabled)
229            DOMDebuggerAgent.setDOMBreakpoint(node.id, type);
230    },
231
232    _removeAllBreakpoints: function()
233    {
234        for (var id in this._breakpointElements) {
235            var element = this._breakpointElements[id];
236            this._removeBreakpoint(element._node, element._type);
237        }
238        this._saveBreakpoints();
239    },
240
241    _removeBreakpoint: function(node, type)
242    {
243        var breakpointId = this._createBreakpointId(node.id, type);
244        var element = this._breakpointElements[breakpointId];
245        if (!element)
246            return;
247
248        this._removeListElement(element);
249        delete this._breakpointElements[breakpointId];
250        if (element._checkboxElement.checked)
251            DOMDebuggerAgent.removeDOMBreakpoint(node.id, type);
252    },
253
254    _contextMenu: function(node, type, event)
255    {
256        var contextMenu = new WebInspector.ContextMenu(event);
257
258        /**
259         * @this {WebInspector.DOMBreakpointsSidebarPane}
260         */
261        function removeBreakpoint()
262        {
263            this._removeBreakpoint(node, type);
264            this._saveBreakpoints();
265        }
266        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove breakpoint" : "Remove Breakpoint"), removeBreakpoint.bind(this));
267        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove all DOM breakpoints" : "Remove All DOM Breakpoints"), this._removeAllBreakpoints.bind(this));
268        contextMenu.show();
269    },
270
271    _checkboxClicked: function(node, type, event)
272    {
273        if (event.target.checked)
274            DOMDebuggerAgent.setDOMBreakpoint(node.id, type);
275        else
276            DOMDebuggerAgent.removeDOMBreakpoint(node.id, type);
277        this._saveBreakpoints();
278    },
279
280    highlightBreakpoint: function(auxData)
281    {
282        var breakpointId = this._createBreakpointId(auxData.nodeId, auxData.type);
283        var element = this._breakpointElements[breakpointId];
284        if (!element)
285            return;
286        this.expand();
287        element.classList.add("breakpoint-hit");
288        this._highlightedElement = element;
289    },
290
291    clearBreakpointHighlight: function()
292    {
293        if (this._highlightedElement) {
294            this._highlightedElement.classList.remove("breakpoint-hit");
295            delete this._highlightedElement;
296        }
297    },
298
299    _createBreakpointId: function(nodeId, type)
300    {
301        return nodeId + ":" + type;
302    },
303
304    _saveBreakpoints: function()
305    {
306        var breakpoints = [];
307        var storedBreakpoints = WebInspector.settings.domBreakpoints.get();
308        for (var i = 0; i < storedBreakpoints.length; ++i) {
309            var breakpoint = storedBreakpoints[i];
310            if (breakpoint.url !== this._inspectedURL)
311                breakpoints.push(breakpoint);
312        }
313        for (var id in this._breakpointElements) {
314            var element = this._breakpointElements[id];
315            breakpoints.push({ url: this._inspectedURL, path: element._node.path(), type: element._type, enabled: element._checkboxElement.checked });
316        }
317        WebInspector.settings.domBreakpoints.set(breakpoints);
318    },
319
320    restoreBreakpoints: function()
321    {
322        var pathToBreakpoints = {};
323
324        /**
325         * @param {string} path
326         * @param {?DOMAgent.NodeId} nodeId
327         * @this {WebInspector.DOMBreakpointsSidebarPane}
328         */
329        function didPushNodeByPathToFrontend(path, nodeId)
330        {
331            var node = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : null;
332            if (!node)
333                return;
334
335            var breakpoints = pathToBreakpoints[path];
336            for (var i = 0; i < breakpoints.length; ++i)
337                this._setBreakpoint(node, breakpoints[i].type, breakpoints[i].enabled);
338        }
339
340        var breakpoints = WebInspector.settings.domBreakpoints.get();
341        for (var i = 0; i < breakpoints.length; ++i) {
342            var breakpoint = breakpoints[i];
343            if (breakpoint.url !== this._inspectedURL)
344                continue;
345            var path = breakpoint.path;
346            if (!pathToBreakpoints[path]) {
347                pathToBreakpoints[path] = [];
348                WebInspector.domAgent.pushNodeByPathToFrontend(path, didPushNodeByPathToFrontend.bind(this, path));
349            }
350            pathToBreakpoints[path].push(breakpoint);
351        }
352    },
353
354    /**
355     * @param {!WebInspector.Panel} panel
356     */
357    createProxy: function(panel)
358    {
359        var proxy = new WebInspector.DOMBreakpointsSidebarPane.Proxy(this, panel);
360        if (!this._proxies)
361            this._proxies = [];
362        this._proxies.push(proxy);
363        return proxy;
364    },
365
366    onContentReady: function()
367    {
368        for (var i = 0; i != this._proxies.length; i++)
369            this._proxies[i].onContentReady();
370    },
371
372    __proto__: WebInspector.NativeBreakpointsSidebarPane.prototype
373}
374
375/**
376 * @constructor
377 * @extends {WebInspector.SidebarPane}
378 * @param {!WebInspector.DOMBreakpointsSidebarPane} pane
379 * @param {!WebInspector.Panel} panel
380 */
381WebInspector.DOMBreakpointsSidebarPane.Proxy = function(pane, panel)
382{
383    WebInspector.View._assert(!pane.titleElement.firstChild, "Cannot create proxy for a sidebar pane with a toolbar");
384
385    WebInspector.SidebarPane.call(this, pane.title());
386    this.registerRequiredCSS("breakpointsList.css");
387
388    this._wrappedPane = pane;
389    this._panel = panel;
390
391    this.bodyElement.remove();
392    this.bodyElement = this._wrappedPane.bodyElement;
393}
394
395WebInspector.DOMBreakpointsSidebarPane.Proxy.prototype = {
396    expand: function()
397    {
398        this._wrappedPane.expand();
399    },
400
401    onContentReady: function()
402    {
403        if (this._panel.isShowing())
404            this._reattachBody();
405
406        WebInspector.SidebarPane.prototype.onContentReady.call(this);
407    },
408
409    wasShown: function()
410    {
411        WebInspector.SidebarPane.prototype.wasShown.call(this);
412        this._reattachBody();
413    },
414
415    _reattachBody: function()
416    {
417        if (this.bodyElement.parentNode !== this.element)
418            this.element.appendChild(this.bodyElement);
419    },
420
421    __proto__: WebInspector.SidebarPane.prototype
422}
423
424/**
425 * @type {!WebInspector.DOMBreakpointsSidebarPane}
426 */
427WebInspector.domBreakpointsSidebarPane;
428