Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
CanvasFrameAnonymousContentHelper,
loader.lazyGetter(this, "PausedReasonsBundle", () => {
return new Localization(
["devtools/shared/debugger-paused-reasons.ftl"],
true
);
});
loader.lazyRequireGetter(
this,
"DEBUGGER_PAUSED_REASONS_L10N_MAPPING",
true
);
/**
* The PausedDebuggerOverlay is a class that displays a semi-transparent mask on top of
* the whole page and a toolbar at the top of the page.
* This is used to signal to users that script execution is current paused.
* The toolbar is used to display the reason for the pause in script execution as well as
* buttons to resume or step through the program.
*/
class PausedDebuggerOverlay {
constructor(highlighterEnv, options = {}) {
this.env = highlighterEnv;
this.resume = options.resume;
this.stepOver = options.stepOver;
this.lastTarget = null;
this.markup = new CanvasFrameAnonymousContentHelper(
highlighterEnv,
this._buildMarkup.bind(this),
{ waitForDocumentToLoad: false }
);
this.isReady = this.markup.initialize();
}
ID_CLASS_PREFIX = "paused-dbg-";
_buildMarkup() {
const prefix = this.ID_CLASS_PREFIX;
const container = this.markup.createNode({
attributes: { class: "highlighter-container" },
});
// Wrapper element.
const wrapper = this.markup.createNode({
parent: container,
attributes: {
id: "root",
class: "root",
hidden: "true",
overlay: "true",
},
prefix,
});
const toolbar = this.markup.createNode({
parent: wrapper,
attributes: {
id: "toolbar",
class: "toolbar",
},
prefix,
});
this.markup.createNode({
nodeType: "span",
parent: toolbar,
attributes: {
id: "reason",
class: "reason",
},
prefix,
});
this.markup.createNode({
parent: toolbar,
attributes: {
id: "divider",
class: "divider",
},
prefix,
});
const stepWrapper = this.markup.createNode({
parent: toolbar,
attributes: {
id: "step-button-wrapper",
class: "step-button-wrapper",
},
prefix,
});
this.markup.createNode({
nodeType: "button",
parent: stepWrapper,
attributes: {
id: "step-button",
class: "step-button",
},
prefix,
});
const resumeWrapper = this.markup.createNode({
parent: toolbar,
attributes: {
id: "resume-button-wrapper",
class: "resume-button-wrapper",
},
prefix,
});
this.markup.createNode({
nodeType: "button",
parent: resumeWrapper,
attributes: {
id: "resume-button",
class: "resume-button",
},
prefix,
});
return container;
}
destroy() {
this.hide();
this.markup.destroy();
this.env = null;
this.lastTarget = null;
}
onClick(target) {
const { id } = target;
if (!id) {
return;
}
if (id.includes("paused-dbg-step-button")) {
this.stepOver();
} else if (id.includes("paused-dbg-resume-button")) {
this.resume();
}
}
onMouseMove(target) {
// Not an element we care about
if (!target || !target.id) {
return;
}
// If the user didn't change targets, do nothing
if (this.lastTarget && this.lastTarget.id === target.id) {
return;
}
if (
target.id.includes("step-button") ||
target.id.includes("resume-button")
) {
// The hover should be applied to the wrapper (icon's parent node)
const newTarget = target.parentNode.id.includes("wrapper")
? target.parentNode
: target;
// Remove the hover class if the user has changed buttons
if (this.lastTarget && this.lastTarget != newTarget) {
this.lastTarget.classList.remove("hover");
}
newTarget.classList.add("hover");
this.lastTarget = newTarget;
} else if (this.lastTarget) {
// Remove the hover class if the user isn't on a button
this.lastTarget.classList.remove("hover");
}
}
handleEvent(e) {
switch (e.type) {
case "mousedown":
this.onClick(e.target);
break;
case "DOMMouseScroll":
// Prevent scrolling. That's because we only took a screenshot of the viewport, so
// scrolling out of the viewport wouldn't draw the expected things. In the future
// we can take the screenshot again on scroll, but for now it doesn't seem
// important.
e.preventDefault();
break;
case "mousemove":
this.onMouseMove(e.target);
break;
}
}
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
}
show(reason) {
if (this.env.isXUL || !reason) {
return false;
}
// Only track mouse movement when the the overlay is shown
// Prevents mouse tracking when the user isn't paused
const { pageListenerTarget } = this.env;
pageListenerTarget.addEventListener("mousemove", this);
// Show the highlighter's root element.
const root = this.getElement("root");
root.removeAttribute("hidden");
root.setAttribute("overlay", "true");
// Set the text to appear in the toolbar.
const toolbar = this.getElement("toolbar");
this.getElement("reason").setTextContent(
PausedReasonsBundle.formatValueSync(
DEBUGGER_PAUSED_REASONS_L10N_MAPPING[reason]
)
);
toolbar.removeAttribute("hidden");
// When the debugger pauses execution in a page, events will not be delivered
// to any handlers added to elements on that page. So here we use the
// document's setSuppressedEventListener interface to still be able to act on mouse
// events (they'll be handled by the `handleEvent` method)
this.env.window.document.setSuppressedEventListener(this);
return true;
}
hide() {
if (this.env.isXUL) {
return;
}
const { pageListenerTarget } = this.env;
pageListenerTarget.removeEventListener("mousemove", this);
// Hide the overlay.
this.getElement("root").setAttribute("hidden", "true");
// Remove the hover state
this.getElement("step-button-wrapper").classList.remove("hover");
this.getElement("resume-button-wrapper").classList.remove("hover");
}
}
exports.PausedDebuggerOverlay = PausedDebuggerOverlay;