|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 const {Cc, Ci, Cu, Cr} = require("chrome"); |
|
8 |
|
9 Cu.import("resource://gre/modules/Services.jsm"); |
|
10 |
|
11 let promise = require("devtools/toolkit/deprecated-sync-thenables"); |
|
12 let EventEmitter = require("devtools/toolkit/event-emitter"); |
|
13 let {CssLogic} = require("devtools/styleinspector/css-logic"); |
|
14 |
|
15 loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView); |
|
16 loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs); |
|
17 loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar); |
|
18 loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch); |
|
19 |
|
20 const LAYOUT_CHANGE_TIMER = 250; |
|
21 |
|
22 /** |
|
23 * Represents an open instance of the Inspector for a tab. |
|
24 * The inspector controls the breadcrumbs, the markup view, and the sidebar |
|
25 * (computed view, rule view, font view and layout view). |
|
26 * |
|
27 * Events: |
|
28 * - ready |
|
29 * Fired when the inspector panel is opened for the first time and ready to |
|
30 * use |
|
31 * - new-root |
|
32 * Fired after a new root (navigation to a new page) event was fired by |
|
33 * the walker, and taken into account by the inspector (after the markup |
|
34 * view has been reloaded) |
|
35 * - markuploaded |
|
36 * Fired when the markup-view frame has loaded |
|
37 * - layout-change |
|
38 * Fired when the layout of the inspector changes |
|
39 * - breadcrumbs-updated |
|
40 * Fired when the breadcrumb widget updates to a new node |
|
41 * - layoutview-updated |
|
42 * Fired when the layoutview (box model) updates to a new node |
|
43 * - markupmutation |
|
44 * Fired after markup mutations have been processed by the markup-view |
|
45 * - computed-view-refreshed |
|
46 * Fired when the computed rules view updates to a new node |
|
47 * - computed-view-property-expanded |
|
48 * Fired when a property is expanded in the computed rules view |
|
49 * - computed-view-property-collapsed |
|
50 * Fired when a property is collapsed in the computed rules view |
|
51 * - rule-view-refreshed |
|
52 * Fired when the rule view updates to a new node |
|
53 */ |
|
54 function InspectorPanel(iframeWindow, toolbox) { |
|
55 this._toolbox = toolbox; |
|
56 this._target = toolbox._target; |
|
57 this.panelDoc = iframeWindow.document; |
|
58 this.panelWin = iframeWindow; |
|
59 this.panelWin.inspector = this; |
|
60 this._inspector = null; |
|
61 |
|
62 this._onBeforeNavigate = this._onBeforeNavigate.bind(this); |
|
63 this._target.on("will-navigate", this._onBeforeNavigate); |
|
64 |
|
65 EventEmitter.decorate(this); |
|
66 } |
|
67 |
|
68 exports.InspectorPanel = InspectorPanel; |
|
69 |
|
70 InspectorPanel.prototype = { |
|
71 /** |
|
72 * open is effectively an asynchronous constructor |
|
73 */ |
|
74 open: function InspectorPanel_open() { |
|
75 return this.target.makeRemote().then(() => { |
|
76 return this._getPageStyle(); |
|
77 }).then(() => { |
|
78 return this._getDefaultNodeForSelection(); |
|
79 }).then(defaultSelection => { |
|
80 return this._deferredOpen(defaultSelection); |
|
81 }).then(null, console.error); |
|
82 }, |
|
83 |
|
84 get toolbox() { |
|
85 return this._toolbox; |
|
86 }, |
|
87 |
|
88 get inspector() { |
|
89 return this._toolbox.inspector; |
|
90 }, |
|
91 |
|
92 get walker() { |
|
93 return this._toolbox.walker; |
|
94 }, |
|
95 |
|
96 get selection() { |
|
97 return this._toolbox.selection; |
|
98 }, |
|
99 |
|
100 get isOuterHTMLEditable() { |
|
101 return this._target.client.traits.editOuterHTML; |
|
102 }, |
|
103 |
|
104 get hasUrlToImageDataResolver() { |
|
105 return this._target.client.traits.urlToImageDataResolver; |
|
106 }, |
|
107 |
|
108 _deferredOpen: function(defaultSelection) { |
|
109 let deferred = promise.defer(); |
|
110 |
|
111 this.onNewRoot = this.onNewRoot.bind(this); |
|
112 this.walker.on("new-root", this.onNewRoot); |
|
113 |
|
114 this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); |
|
115 this.lastNodemenuItem = this.nodemenu.lastChild; |
|
116 this._setupNodeMenu = this._setupNodeMenu.bind(this); |
|
117 this._resetNodeMenu = this._resetNodeMenu.bind(this); |
|
118 this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true); |
|
119 this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); |
|
120 |
|
121 this.onNewSelection = this.onNewSelection.bind(this); |
|
122 this.selection.on("new-node-front", this.onNewSelection); |
|
123 this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); |
|
124 this.selection.on("before-new-node-front", this.onBeforeNewSelection); |
|
125 this.onDetached = this.onDetached.bind(this); |
|
126 this.selection.on("detached-front", this.onDetached); |
|
127 |
|
128 this.breadcrumbs = new HTMLBreadcrumbs(this); |
|
129 |
|
130 if (this.target.isLocalTab) { |
|
131 this.browser = this.target.tab.linkedBrowser; |
|
132 this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this); |
|
133 this.browser.addEventListener("resize", this.scheduleLayoutChange, true); |
|
134 |
|
135 // Show a warning when the debugger is paused. |
|
136 // We show the warning only when the inspector |
|
137 // is selected. |
|
138 this.updateDebuggerPausedWarning = function() { |
|
139 let notificationBox = this._toolbox.getNotificationBox(); |
|
140 let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); |
|
141 if (!notification && this._toolbox.currentToolId == "inspector" && |
|
142 this.target.isThreadPaused) { |
|
143 let message = this.strings.GetStringFromName("debuggerPausedWarning.message"); |
|
144 notificationBox.appendNotification(message, |
|
145 "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH); |
|
146 } |
|
147 |
|
148 if (notification && this._toolbox.currentToolId != "inspector") { |
|
149 notificationBox.removeNotification(notification); |
|
150 } |
|
151 |
|
152 if (notification && !this.target.isThreadPaused) { |
|
153 notificationBox.removeNotification(notification); |
|
154 } |
|
155 |
|
156 }.bind(this); |
|
157 this.target.on("thread-paused", this.updateDebuggerPausedWarning); |
|
158 this.target.on("thread-resumed", this.updateDebuggerPausedWarning); |
|
159 this._toolbox.on("select", this.updateDebuggerPausedWarning); |
|
160 this.updateDebuggerPausedWarning(); |
|
161 } |
|
162 |
|
163 this._initMarkup(); |
|
164 this.isReady = false; |
|
165 |
|
166 this.once("markuploaded", function() { |
|
167 this.isReady = true; |
|
168 |
|
169 // All the components are initialized. Let's select a node. |
|
170 this.selection.setNodeFront(defaultSelection, "inspector-open"); |
|
171 |
|
172 this.markup.expandNode(this.selection.nodeFront); |
|
173 |
|
174 this.emit("ready"); |
|
175 deferred.resolve(this); |
|
176 }.bind(this)); |
|
177 |
|
178 this.setupSearchBox(); |
|
179 this.setupSidebar(); |
|
180 |
|
181 return deferred.promise; |
|
182 }, |
|
183 |
|
184 _onBeforeNavigate: function() { |
|
185 this._defaultNode = null; |
|
186 this.selection.setNodeFront(null); |
|
187 this._destroyMarkup(); |
|
188 this.isDirty = false; |
|
189 }, |
|
190 |
|
191 _getPageStyle: function() { |
|
192 return this._toolbox.inspector.getPageStyle().then(pageStyle => { |
|
193 this.pageStyle = pageStyle; |
|
194 }); |
|
195 }, |
|
196 |
|
197 /** |
|
198 * Return a promise that will resolve to the default node for selection. |
|
199 */ |
|
200 _getDefaultNodeForSelection: function() { |
|
201 if (this._defaultNode) { |
|
202 return this._defaultNode; |
|
203 } |
|
204 let walker = this.walker; |
|
205 let rootNode = null; |
|
206 |
|
207 // If available, set either the previously selected node or the body |
|
208 // as default selected, else set documentElement |
|
209 return walker.getRootNode().then(aRootNode => { |
|
210 rootNode = aRootNode; |
|
211 return walker.querySelector(rootNode, this.selectionCssSelector); |
|
212 }).then(front => { |
|
213 if (front) { |
|
214 return front; |
|
215 } |
|
216 return walker.querySelector(rootNode, "body"); |
|
217 }).then(front => { |
|
218 if (front) { |
|
219 return front; |
|
220 } |
|
221 return this.walker.documentElement(this.walker.rootNode); |
|
222 }).then(node => { |
|
223 if (walker !== this.walker) { |
|
224 promise.reject(null); |
|
225 } |
|
226 this._defaultNode = node; |
|
227 return node; |
|
228 }); |
|
229 }, |
|
230 |
|
231 /** |
|
232 * Target getter. |
|
233 */ |
|
234 get target() { |
|
235 return this._target; |
|
236 }, |
|
237 |
|
238 /** |
|
239 * Target setter. |
|
240 */ |
|
241 set target(value) { |
|
242 this._target = value; |
|
243 }, |
|
244 |
|
245 /** |
|
246 * Expose gViewSourceUtils so that other tools can make use of them. |
|
247 */ |
|
248 get viewSourceUtils() { |
|
249 return this.panelWin.gViewSourceUtils; |
|
250 }, |
|
251 |
|
252 /** |
|
253 * Indicate that a tool has modified the state of the page. Used to |
|
254 * decide whether to show the "are you sure you want to navigate" |
|
255 * notification. |
|
256 */ |
|
257 markDirty: function InspectorPanel_markDirty() { |
|
258 this.isDirty = true; |
|
259 }, |
|
260 |
|
261 /** |
|
262 * Hooks the searchbar to show result and auto completion suggestions. |
|
263 */ |
|
264 setupSearchBox: function InspectorPanel_setupSearchBox() { |
|
265 // Initiate the selectors search object. |
|
266 if (this.searchSuggestions) { |
|
267 this.searchSuggestions.destroy(); |
|
268 this.searchSuggestions = null; |
|
269 } |
|
270 this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); |
|
271 this.searchSuggestions = new SelectorSearch(this, this.searchBox); |
|
272 }, |
|
273 |
|
274 /** |
|
275 * Build the sidebar. |
|
276 */ |
|
277 setupSidebar: function InspectorPanel_setupSidebar() { |
|
278 let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); |
|
279 this.sidebar = new ToolSidebar(tabbox, this, "inspector"); |
|
280 |
|
281 let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); |
|
282 |
|
283 this._setDefaultSidebar = function(event, toolId) { |
|
284 Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId); |
|
285 }.bind(this); |
|
286 |
|
287 this.sidebar.on("select", this._setDefaultSidebar); |
|
288 |
|
289 this.sidebar.addTab("ruleview", |
|
290 "chrome://browser/content/devtools/cssruleview.xhtml", |
|
291 "ruleview" == defaultTab); |
|
292 |
|
293 this.sidebar.addTab("computedview", |
|
294 "chrome://browser/content/devtools/computedview.xhtml", |
|
295 "computedview" == defaultTab); |
|
296 |
|
297 if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && !this.target.isRemote) { |
|
298 this.sidebar.addTab("fontinspector", |
|
299 "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml", |
|
300 "fontinspector" == defaultTab); |
|
301 } |
|
302 |
|
303 this.sidebar.addTab("layoutview", |
|
304 "chrome://browser/content/devtools/layoutview/view.xhtml", |
|
305 "layoutview" == defaultTab); |
|
306 |
|
307 let ruleViewTab = this.sidebar.getTab("ruleview"); |
|
308 |
|
309 this.sidebar.show(); |
|
310 }, |
|
311 |
|
312 /** |
|
313 * Reset the inspector on new root mutation. |
|
314 */ |
|
315 onNewRoot: function InspectorPanel_onNewRoot() { |
|
316 this._defaultNode = null; |
|
317 this.selection.setNodeFront(null); |
|
318 this._destroyMarkup(); |
|
319 this.isDirty = false; |
|
320 |
|
321 let onNodeSelected = defaultNode => { |
|
322 // Cancel this promise resolution as a new one had |
|
323 // been queued up. |
|
324 if (this._pendingSelection != onNodeSelected) { |
|
325 return; |
|
326 } |
|
327 this._pendingSelection = null; |
|
328 this.selection.setNodeFront(defaultNode, "navigateaway"); |
|
329 |
|
330 this._initMarkup(); |
|
331 this.once("markuploaded", () => { |
|
332 if (!this.markup) { |
|
333 return; |
|
334 } |
|
335 this.markup.expandNode(this.selection.nodeFront); |
|
336 this.setupSearchBox(); |
|
337 this.emit("new-root"); |
|
338 }); |
|
339 }; |
|
340 this._pendingSelection = onNodeSelected; |
|
341 this._getDefaultNodeForSelection().then(onNodeSelected); |
|
342 }, |
|
343 |
|
344 _selectionCssSelector: null, |
|
345 |
|
346 /** |
|
347 * Set the currently selected node unique css selector. |
|
348 * Will store the current target url along with it to allow pre-selection at |
|
349 * reload |
|
350 */ |
|
351 set selectionCssSelector(cssSelector) { |
|
352 this._selectionCssSelector = { |
|
353 selector: cssSelector, |
|
354 url: this._target.url |
|
355 }; |
|
356 }, |
|
357 |
|
358 /** |
|
359 * Get the current selection unique css selector if any, that is, if a node |
|
360 * is actually selected and that node has been selected while on the same url |
|
361 */ |
|
362 get selectionCssSelector() { |
|
363 if (this._selectionCssSelector && |
|
364 this._selectionCssSelector.url === this._target.url) { |
|
365 return this._selectionCssSelector.selector; |
|
366 } else { |
|
367 return null; |
|
368 } |
|
369 }, |
|
370 |
|
371 /** |
|
372 * When a new node is selected. |
|
373 */ |
|
374 onNewSelection: function InspectorPanel_onNewSelection(event, value, reason) { |
|
375 if (reason === "selection-destroy") { |
|
376 return; |
|
377 } |
|
378 |
|
379 this.cancelLayoutChange(); |
|
380 |
|
381 // Wait for all the known tools to finish updating and then let the |
|
382 // client know. |
|
383 let selection = this.selection.nodeFront; |
|
384 |
|
385 // On any new selection made by the user, store the unique css selector |
|
386 // of the selected node so it can be restored after reload of the same page |
|
387 if (reason !== "navigateaway" && |
|
388 this.selection.node && |
|
389 this.selection.isElementNode()) { |
|
390 this.selectionCssSelector = CssLogic.findCssSelector(this.selection.node); |
|
391 } |
|
392 |
|
393 let selfUpdate = this.updating("inspector-panel"); |
|
394 Services.tm.mainThread.dispatch(() => { |
|
395 try { |
|
396 selfUpdate(selection); |
|
397 } catch(ex) { |
|
398 console.error(ex); |
|
399 } |
|
400 }, Ci.nsIThread.DISPATCH_NORMAL); |
|
401 }, |
|
402 |
|
403 /** |
|
404 * Delay the "inspector-updated" notification while a tool |
|
405 * is updating itself. Returns a function that must be |
|
406 * invoked when the tool is done updating with the node |
|
407 * that the tool is viewing. |
|
408 */ |
|
409 updating: function(name) { |
|
410 if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) { |
|
411 this.cancelUpdate(); |
|
412 } |
|
413 |
|
414 if (!this._updateProgress) { |
|
415 // Start an update in progress. |
|
416 var self = this; |
|
417 this._updateProgress = { |
|
418 node: this.selection.nodeFront, |
|
419 outstanding: new Set(), |
|
420 checkDone: function() { |
|
421 if (this !== self._updateProgress) { |
|
422 return; |
|
423 } |
|
424 if (this.node !== self.selection.nodeFront) { |
|
425 self.cancelUpdate(); |
|
426 return; |
|
427 } |
|
428 if (this.outstanding.size !== 0) { |
|
429 return; |
|
430 } |
|
431 |
|
432 self._updateProgress = null; |
|
433 self.emit("inspector-updated", name); |
|
434 }, |
|
435 }; |
|
436 } |
|
437 |
|
438 let progress = this._updateProgress; |
|
439 let done = function() { |
|
440 progress.outstanding.delete(done); |
|
441 progress.checkDone(); |
|
442 }; |
|
443 progress.outstanding.add(done); |
|
444 return done; |
|
445 }, |
|
446 |
|
447 /** |
|
448 * Cancel notification of inspector updates. |
|
449 */ |
|
450 cancelUpdate: function() { |
|
451 this._updateProgress = null; |
|
452 }, |
|
453 |
|
454 /** |
|
455 * When a new node is selected, before the selection has changed. |
|
456 */ |
|
457 onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event, |
|
458 node) { |
|
459 if (this.breadcrumbs.indexOf(node) == -1) { |
|
460 // only clear locks if we'd have to update breadcrumbs |
|
461 this.clearPseudoClasses(); |
|
462 } |
|
463 }, |
|
464 |
|
465 /** |
|
466 * When a node is deleted, select its parent node or the defaultNode if no |
|
467 * parent is found (may happen when deleting an iframe inside which the |
|
468 * node was selected). |
|
469 */ |
|
470 onDetached: function InspectorPanel_onDetached(event, parentNode) { |
|
471 this.cancelLayoutChange(); |
|
472 this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode)); |
|
473 this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached"); |
|
474 }, |
|
475 |
|
476 /** |
|
477 * Destroy the inspector. |
|
478 */ |
|
479 destroy: function InspectorPanel__destroy() { |
|
480 if (this._panelDestroyer) { |
|
481 return this._panelDestroyer; |
|
482 } |
|
483 |
|
484 if (this.walker) { |
|
485 this.walker.off("new-root", this.onNewRoot); |
|
486 this.pageStyle = null; |
|
487 } |
|
488 |
|
489 this.cancelUpdate(); |
|
490 this.cancelLayoutChange(); |
|
491 |
|
492 if (this.browser) { |
|
493 this.browser.removeEventListener("resize", this.scheduleLayoutChange, true); |
|
494 this.browser = null; |
|
495 } |
|
496 |
|
497 this.target.off("will-navigate", this._onBeforeNavigate); |
|
498 |
|
499 this.target.off("thread-paused", this.updateDebuggerPausedWarning); |
|
500 this.target.off("thread-resumed", this.updateDebuggerPausedWarning); |
|
501 this._toolbox.off("select", this.updateDebuggerPausedWarning); |
|
502 |
|
503 this.sidebar.off("select", this._setDefaultSidebar); |
|
504 this.sidebar.destroy(); |
|
505 this.sidebar = null; |
|
506 |
|
507 this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true); |
|
508 this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true); |
|
509 this.breadcrumbs.destroy(); |
|
510 this.searchSuggestions.destroy(); |
|
511 this.searchBox = null; |
|
512 this.selection.off("new-node-front", this.onNewSelection); |
|
513 this.selection.off("before-new-node", this.onBeforeNewSelection); |
|
514 this.selection.off("before-new-node-front", this.onBeforeNewSelection); |
|
515 this.selection.off("detached-front", this.onDetached); |
|
516 this._panelDestroyer = this._destroyMarkup(); |
|
517 this.panelWin.inspector = null; |
|
518 this.target = null; |
|
519 this.panelDoc = null; |
|
520 this.panelWin = null; |
|
521 this.breadcrumbs = null; |
|
522 this.searchSuggestions = null; |
|
523 this.lastNodemenuItem = null; |
|
524 this.nodemenu = null; |
|
525 this._toolbox = null; |
|
526 |
|
527 return this._panelDestroyer; |
|
528 }, |
|
529 |
|
530 /** |
|
531 * Show the node menu. |
|
532 */ |
|
533 showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) { |
|
534 if (aExtraItems) { |
|
535 for (let item of aExtraItems) { |
|
536 this.nodemenu.appendChild(item); |
|
537 } |
|
538 } |
|
539 this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false); |
|
540 }, |
|
541 |
|
542 hideNodeMenu: function InspectorPanel_hideNodeMenu() { |
|
543 this.nodemenu.hidePopup(); |
|
544 }, |
|
545 |
|
546 /** |
|
547 * Disable the delete item if needed. Update the pseudo classes. |
|
548 */ |
|
549 _setupNodeMenu: function InspectorPanel_setupNodeMenu() { |
|
550 let isSelectionElement = this.selection.isElementNode(); |
|
551 |
|
552 // Set the pseudo classes |
|
553 for (let name of ["hover", "active", "focus"]) { |
|
554 let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name); |
|
555 |
|
556 if (isSelectionElement) { |
|
557 let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name); |
|
558 menu.setAttribute("checked", checked); |
|
559 menu.removeAttribute("disabled"); |
|
560 } else { |
|
561 menu.setAttribute("disabled", "true"); |
|
562 } |
|
563 } |
|
564 |
|
565 // Disable delete item if needed |
|
566 let deleteNode = this.panelDoc.getElementById("node-menu-delete"); |
|
567 if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) { |
|
568 deleteNode.setAttribute("disabled", "true"); |
|
569 } else { |
|
570 deleteNode.removeAttribute("disabled"); |
|
571 } |
|
572 |
|
573 // Disable / enable "Copy Unique Selector", "Copy inner HTML" & |
|
574 // "Copy outer HTML" as appropriate |
|
575 let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector"); |
|
576 let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner"); |
|
577 let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter"); |
|
578 if (isSelectionElement) { |
|
579 unique.removeAttribute("disabled"); |
|
580 copyInnerHTML.removeAttribute("disabled"); |
|
581 copyOuterHTML.removeAttribute("disabled"); |
|
582 } else { |
|
583 unique.setAttribute("disabled", "true"); |
|
584 copyInnerHTML.setAttribute("disabled", "true"); |
|
585 copyOuterHTML.setAttribute("disabled", "true"); |
|
586 } |
|
587 |
|
588 // Enable the "edit HTML" item if the selection is an element and the root |
|
589 // actor has the appropriate trait (isOuterHTMLEditable) |
|
590 let editHTML = this.panelDoc.getElementById("node-menu-edithtml"); |
|
591 if (this.isOuterHTMLEditable && isSelectionElement) { |
|
592 editHTML.removeAttribute("disabled"); |
|
593 } else { |
|
594 editHTML.setAttribute("disabled", "true"); |
|
595 } |
|
596 |
|
597 // Enable the "copy image data-uri" item if the selection is previewable |
|
598 // which essentially checks if it's an image or canvas tag |
|
599 let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri"); |
|
600 let markupContainer = this.markup.getContainer(this.selection.nodeFront); |
|
601 if (markupContainer && markupContainer.isPreviewable()) { |
|
602 copyImageData.removeAttribute("disabled"); |
|
603 } else { |
|
604 copyImageData.setAttribute("disabled", "true"); |
|
605 } |
|
606 }, |
|
607 |
|
608 _resetNodeMenu: function InspectorPanel_resetNodeMenu() { |
|
609 // Remove any extra items |
|
610 while (this.lastNodemenuItem.nextSibling) { |
|
611 let toDelete = this.lastNodemenuItem.nextSibling; |
|
612 toDelete.parentNode.removeChild(toDelete); |
|
613 } |
|
614 }, |
|
615 |
|
616 _initMarkup: function InspectorPanel_initMarkup() { |
|
617 let doc = this.panelDoc; |
|
618 |
|
619 this._markupBox = doc.getElementById("markup-box"); |
|
620 |
|
621 // create tool iframe |
|
622 this._markupFrame = doc.createElement("iframe"); |
|
623 this._markupFrame.setAttribute("flex", "1"); |
|
624 this._markupFrame.setAttribute("tooltip", "aHTMLTooltip"); |
|
625 this._markupFrame.setAttribute("context", "inspector-node-popup"); |
|
626 |
|
627 // This is needed to enable tooltips inside the iframe document. |
|
628 this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); |
|
629 this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true); |
|
630 |
|
631 this._markupBox.setAttribute("collapsed", true); |
|
632 this._markupBox.appendChild(this._markupFrame); |
|
633 this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml"); |
|
634 }, |
|
635 |
|
636 _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() { |
|
637 this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); |
|
638 delete this._boundMarkupFrameLoad; |
|
639 |
|
640 this._markupFrame.contentWindow.focus(); |
|
641 |
|
642 this._markupBox.removeAttribute("collapsed"); |
|
643 |
|
644 let controllerWindow = this._toolbox.doc.defaultView; |
|
645 this.markup = new MarkupView(this, this._markupFrame, controllerWindow); |
|
646 |
|
647 this.emit("markuploaded"); |
|
648 }, |
|
649 |
|
650 _destroyMarkup: function InspectorPanel__destroyMarkup() { |
|
651 let destroyPromise; |
|
652 |
|
653 if (this._boundMarkupFrameLoad) { |
|
654 this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); |
|
655 this._boundMarkupFrameLoad = null; |
|
656 } |
|
657 |
|
658 if (this.markup) { |
|
659 destroyPromise = this.markup.destroy(); |
|
660 this.markup = null; |
|
661 } else { |
|
662 destroyPromise = promise.resolve(); |
|
663 } |
|
664 |
|
665 if (this._markupFrame) { |
|
666 this._markupFrame.parentNode.removeChild(this._markupFrame); |
|
667 this._markupFrame = null; |
|
668 } |
|
669 |
|
670 this._markupBox = null; |
|
671 |
|
672 return destroyPromise; |
|
673 }, |
|
674 |
|
675 /** |
|
676 * Toggle a pseudo class. |
|
677 */ |
|
678 togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) { |
|
679 if (this.selection.isElementNode()) { |
|
680 let node = this.selection.nodeFront; |
|
681 if (node.hasPseudoClassLock(aPseudo)) { |
|
682 return this.walker.removePseudoClassLock(node, aPseudo, {parents: true}); |
|
683 } |
|
684 |
|
685 let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; |
|
686 return this.walker.addPseudoClassLock(node, aPseudo, {parents: hierarchical}); |
|
687 } |
|
688 }, |
|
689 |
|
690 /** |
|
691 * Clear any pseudo-class locks applied to the current hierarchy. |
|
692 */ |
|
693 clearPseudoClasses: function InspectorPanel_clearPseudoClasses() { |
|
694 if (!this.walker) { |
|
695 return; |
|
696 } |
|
697 return this.walker.clearPseudoClassLocks().then(null, console.error); |
|
698 }, |
|
699 |
|
700 /** |
|
701 * Edit the outerHTML of the selected Node. |
|
702 */ |
|
703 editHTML: function InspectorPanel_editHTML() |
|
704 { |
|
705 if (!this.selection.isNode()) { |
|
706 return; |
|
707 } |
|
708 if (this.markup) { |
|
709 this.markup.beginEditingOuterHTML(this.selection.nodeFront); |
|
710 } |
|
711 }, |
|
712 |
|
713 /** |
|
714 * Copy the innerHTML of the selected Node to the clipboard. |
|
715 */ |
|
716 copyInnerHTML: function InspectorPanel_copyInnerHTML() |
|
717 { |
|
718 if (!this.selection.isNode()) { |
|
719 return; |
|
720 } |
|
721 this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront)); |
|
722 }, |
|
723 |
|
724 /** |
|
725 * Copy the outerHTML of the selected Node to the clipboard. |
|
726 */ |
|
727 copyOuterHTML: function InspectorPanel_copyOuterHTML() |
|
728 { |
|
729 if (!this.selection.isNode()) { |
|
730 return; |
|
731 } |
|
732 |
|
733 this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront)); |
|
734 }, |
|
735 |
|
736 /** |
|
737 * Copy the data-uri for the currently selected image in the clipboard. |
|
738 */ |
|
739 copyImageDataUri: function InspectorPanel_copyImageDataUri() |
|
740 { |
|
741 let container = this.markup.getContainer(this.selection.nodeFront); |
|
742 if (container && container.isPreviewable()) { |
|
743 container.copyImageDataUri(); |
|
744 } |
|
745 }, |
|
746 |
|
747 _copyLongStr: function InspectorPanel_copyLongStr(promise) |
|
748 { |
|
749 return promise.then(longstr => { |
|
750 return longstr.string().then(toCopy => { |
|
751 longstr.release().then(null, console.error); |
|
752 clipboardHelper.copyString(toCopy); |
|
753 }); |
|
754 }).then(null, console.error); |
|
755 }, |
|
756 |
|
757 /** |
|
758 * Copy a unique selector of the selected Node to the clipboard. |
|
759 */ |
|
760 copyUniqueSelector: function InspectorPanel_copyUniqueSelector() |
|
761 { |
|
762 if (!this.selection.isNode()) { |
|
763 return; |
|
764 } |
|
765 |
|
766 let toCopy = CssLogic.findCssSelector(this.selection.node); |
|
767 if (toCopy) { |
|
768 clipboardHelper.copyString(toCopy); |
|
769 } |
|
770 }, |
|
771 |
|
772 /** |
|
773 * Delete the selected node. |
|
774 */ |
|
775 deleteNode: function IUI_deleteNode() { |
|
776 if (!this.selection.isNode() || |
|
777 this.selection.isRoot()) { |
|
778 return; |
|
779 } |
|
780 |
|
781 // If the markup panel is active, use the markup panel to delete |
|
782 // the node, making this an undoable action. |
|
783 if (this.markup) { |
|
784 this.markup.deleteNode(this.selection.nodeFront); |
|
785 } else { |
|
786 // remove the node from content |
|
787 this.walker.removeNode(this.selection.nodeFront); |
|
788 } |
|
789 }, |
|
790 |
|
791 /** |
|
792 * Trigger a high-priority layout change for things that need to be |
|
793 * updated immediately |
|
794 */ |
|
795 immediateLayoutChange: function Inspector_immediateLayoutChange() |
|
796 { |
|
797 this.emit("layout-change"); |
|
798 }, |
|
799 |
|
800 /** |
|
801 * Schedule a low-priority change event for things like paint |
|
802 * and resize. |
|
803 */ |
|
804 scheduleLayoutChange: function Inspector_scheduleLayoutChange(event) |
|
805 { |
|
806 // Filter out non browser window resize events (i.e. triggered by iframes) |
|
807 if (this.browser.contentWindow === event.target) { |
|
808 if (this._timer) { |
|
809 return null; |
|
810 } |
|
811 this._timer = this.panelWin.setTimeout(function() { |
|
812 this.emit("layout-change"); |
|
813 this._timer = null; |
|
814 }.bind(this), LAYOUT_CHANGE_TIMER); |
|
815 } |
|
816 }, |
|
817 |
|
818 /** |
|
819 * Cancel a pending low-priority change event if any is |
|
820 * scheduled. |
|
821 */ |
|
822 cancelLayoutChange: function Inspector_cancelLayoutChange() |
|
823 { |
|
824 if (this._timer) { |
|
825 this.panelWin.clearTimeout(this._timer); |
|
826 delete this._timer; |
|
827 } |
|
828 } |
|
829 }; |
|
830 |
|
831 ///////////////////////////////////////////////////////////////////////// |
|
832 //// Initializers |
|
833 |
|
834 loader.lazyGetter(InspectorPanel.prototype, "strings", |
|
835 function () { |
|
836 return Services.strings.createBundle( |
|
837 "chrome://browser/locale/devtools/inspector.properties"); |
|
838 }); |
|
839 |
|
840 loader.lazyGetter(this, "clipboardHelper", function() { |
|
841 return Cc["@mozilla.org/widget/clipboardhelper;1"]. |
|
842 getService(Ci.nsIClipboardHelper); |
|
843 }); |
|
844 |
|
845 |
|
846 loader.lazyGetter(this, "DOMUtils", function () { |
|
847 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
|
848 }); |