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
import React, { PureComponent } from "devtools/client/shared/vendor/react";
import {
div,
input,
button,
form,
label,
} from "devtools/client/shared/vendor/react-dom-factories";
import ReactDOM from "devtools/client/shared/vendor/react-dom";
import PropTypes from "devtools/client/shared/vendor/react-prop-types";
import { connect } from "devtools/client/shared/vendor/react-redux";
import { toEditorLine } from "../../utils/editor/index";
import { createEditor } from "../../utils/editor/create-editor";
import actions from "../../actions/index";
import { markerTypes } from "../../constants";
import {
getClosestBreakpoint,
getConditionalPanelLocation,
getLogPointStatus,
} from "../../selectors/index";
const classnames = require("resource://devtools/client/shared/classnames.js");
export class ConditionalPanel extends PureComponent {
cbPanel;
input;
codeMirror;
panelNode;
scrollParent;
constructor() {
super();
this.cbPanel = null;
this.breakpointPanelEditor = null;
this.formRef = React.createRef();
}
static get propTypes() {
return {
breakpoint: PropTypes.object,
closeConditionalPanel: PropTypes.func.isRequired,
editor: PropTypes.object.isRequired,
location: PropTypes.any.isRequired,
log: PropTypes.bool.isRequired,
openConditionalPanel: PropTypes.func.isRequired,
setBreakpointOptions: PropTypes.func.isRequired,
selectedSource: PropTypes.object.isRequired,
};
}
removeBreakpointPanelEditor() {
if (this.breakpointPanelEditor) {
this.breakpointPanelEditor.destroy();
}
this.breakpointPanelEditor = null;
}
keepFocusOnInput() {
if (this.input) {
this.input.focus();
} else if (this.breakpointPanelEditor) {
if (!this.breakpointPanelEditor.isDestroyed()) {
this.breakpointPanelEditor.focus();
}
}
}
onFormSubmit = e => {
if (e && e.preventDefault) {
e.preventDefault();
}
const formData = new FormData(this.formRef.current);
const showStacktrace = formData.get("showStacktrace") === "on";
if (
!this.breakpointPanelEditor ||
this.breakpointPanelEditor.isDestroyed()
) {
return;
}
const expression = this.breakpointPanelEditor.getText(null);
this.saveAndClose(expression, showStacktrace);
};
onPanelBlur = e => {
// if the focus is outside of the conditional panel,
// close/hide the conditional panel
if (
e.relatedTarget &&
e.relatedTarget.closest(".conditional-breakpoint-panel-container")
) {
return;
}
this.props.closeConditionalPanel();
};
/**
* Set the breakpoint/logpoint if expression isn't empty, and close the panel.
*
* @param {String} expression: The expression that will be used for setting the
* conditional breakpoint/logpoint
* @param {Boolean} showStacktrace: Whether to show the stacktrace for the logpoint
*/
saveAndClose = (expression = null, showStacktrace = false) => {
if (typeof expression === "string") {
const trimmedExpression = expression.trim();
if (trimmedExpression) {
this.setBreakpoint(trimmedExpression, showStacktrace);
} else if (this.props.breakpoint) {
// if the user was editing the condition/log of an existing breakpoint,
// we remove the condition/log.
this.setBreakpoint(null);
}
}
this.props.closeConditionalPanel();
};
/**
* Handle inline editor keydown event
*
* @param {Event} e: The keydown event
*/
onKey = e => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.formRef.current.requestSubmit();
} else if (e.key === "Escape") {
this.props.closeConditionalPanel();
}
};
/**
* Handle inline editor blur event
*
* @param {Event} e: The blur event
*/
onBlur = e => {
let explicitOriginalTarget = e?.explicitOriginalTarget;
// The explicit original target can be a text node, in such case retrieve its parent
// element so we can use `closest` on it.
if (explicitOriginalTarget && !Element.isInstance(explicitOriginalTarget)) {
explicitOriginalTarget = explicitOriginalTarget.parentElement;
}
if (
// if there is no event
// or if the focus is the conditional panel
// do not close the conditional panel
!e ||
(explicitOriginalTarget &&
explicitOriginalTarget.closest(
".conditional-breakpoint-panel-container"
))
) {
return;
}
this.props.closeConditionalPanel();
};
setBreakpoint(value, showStacktrace) {
const { log, breakpoint } = this.props;
// If breakpoint is `pending`, props will not contain a breakpoint.
// If source is a URL without location, breakpoint will contain no generatedLocation.
const location =
breakpoint && breakpoint.generatedLocation
? breakpoint.generatedLocation
: this.props.location;
const options = breakpoint ? breakpoint.options : {};
const type = log ? "logValue" : "condition";
return this.props.setBreakpointOptions(location, {
...options,
[type]: value,
showStacktrace,
});
}
clearConditionalPanel() {
if (this.cbPanel) {
this.cbPanel.clear();
this.cbPanel = null;
}
if (this.scrollParent) {
this.scrollParent.removeEventListener("scroll", this.repositionOnScroll);
}
}
repositionOnScroll = () => {
if (this.panelNode && this.scrollParent) {
const { scrollLeft } = this.scrollParent;
this.panelNode.style.transform = `translateX(${scrollLeft}px)`;
}
};
showConditionalPanel(prevProps) {
const { location, log, editor, breakpoint, selectedSource } = this.props;
if (!selectedSource || !location) {
this.removeBreakpointPanelEditor();
return;
}
// When breakpoint is removed
if (prevProps?.breakpoint && !breakpoint) {
editor.removeLineContentMarker(markerTypes.CONDITIONAL_BP_MARKER);
this.removeBreakpointPanelEditor();
return;
}
if (selectedSource.id !== location.source.id) {
editor.removeLineContentMarker(markerTypes.CONDITIONAL_BP_MARKER);
this.removeBreakpointPanelEditor();
return;
}
const line = toEditorLine(location.source, location.line || 0);
editor.setLineContentMarker({
id: markerTypes.CONDITIONAL_BP_MARKER,
lines: [{ line }],
renderAsBlock: true,
createLineElementNode: () => {
// Create a Codemirror editor for the breakpoint panel
const onEnterKeyMapConfig = {
preventDefault: true,
stopPropagation: true,
run: () => this.formRef.current.requestSubmit(),
};
const breakpointPanelEditor = createEditor({
cm6: true,
readOnly: false,
lineNumbers: false,
lineWrapping: true,
placeholder: L10N.getStr(
log
? "editor.conditionalPanel.logPoint.placeholder2"
: "editor.conditionalPanel.placeholder2"
),
keyMap: [
{
key: "Enter",
...onEnterKeyMapConfig,
},
{
key: "Mod-Enter",
...onEnterKeyMapConfig,
},
{
key: "Escape",
preventDefault: true,
stopPropagation: true,
run: () => this.props.closeConditionalPanel(),
},
],
});
this.breakpointPanelEditor = breakpointPanelEditor;
return this.renderConditionalPanel(this.props, breakpointPanelEditor);
},
});
}
UNSAFE_componentWillMount() {
this.showConditionalPanel();
}
componentDidUpdate(prevProps) {
this.showConditionalPanel(prevProps);
this.keepFocusOnInput();
}
componentWillUnmount() {
// This is called if CodeMirror is re-initializing itself before the
// user closes the conditional panel. Clear the widget, and re-render it
// as soon as this component gets remounted
const { editor } = this.props;
editor.removeLineContentMarker(markerTypes.CONDITIONAL_BP_MARKER);
this.removeBreakpointPanelEditor();
}
componentDidMount() {
if (this.formRef && this.formRef.current) {
const checkbox = this.formRef.current.querySelector("#showStacktrace");
if (checkbox) {
checkbox.checked = this.props.breakpoint?.options?.showStacktrace;
}
}
}
renderToWidget(props) {
if (this.cbPanel) {
this.clearConditionalPanel();
}
const { location, editor } = props;
if (!location) {
return;
}
const editorLine = toEditorLine(location.source, location.line || 0);
this.cbPanel = editor.codeMirror.addLineWidget(
editorLine,
this.renderConditionalPanel(props, editor),
{
coverGutter: true,
noHScroll: true,
}
);
if (this.input) {
let parent = this.input.parentNode;
while (parent) {
if (
HTMLElement.isInstance(parent) &&
parent.classList.contains("CodeMirror-scroll")
) {
this.scrollParent = parent;
break;
}
parent = parent.parentNode;
}
if (this.scrollParent) {
this.scrollParent.addEventListener("scroll", this.repositionOnScroll);
this.repositionOnScroll();
}
}
}
setupAndAppendInlineEditor = (el, editor) => {
editor.appendToLocalElement(el);
editor.on("blur", e => this.onBlur(e));
editor.setText(this.getDefaultValue());
editor.focus();
editor.selectAll();
};
getDefaultValue() {
const { breakpoint, log } = this.props;
const options = breakpoint?.options || {};
const value = log ? options.logValue : options.condition;
return value || "";
}
renderConditionalPanel(props, editor) {
const { log } = props;
const panel = document.createElement("div");
const isWindows = Services.appinfo.OS.startsWith("WINNT");
const isCreating = !this.props.breakpoint;
const saveButton = button(
{
type: "submit",
id: "save-logpoint",
className: "devtools-button conditional-breakpoint-panel-save-button",
},
L10N.getStr(
isCreating
? "editor.conditionalPanel.create"
: "editor.conditionalPanel.update"
)
);
const cancelButton = button(
{
type: "button",
className: "devtools-button conditional-breakpoint-panel-cancel-button",
onClick: () => this.props.closeConditionalPanel(),
},
L10N.getStr("editor.conditionalPanel.cancel")
);
// CodeMirror6 can't have margin on a block widget, so we need to wrap the actual
// panel inside a container which won't have any margin
const reactElPanel = div(
{
className: "conditional-breakpoint-panel-container",
onBlur: this.onPanelBlur,
tabIndex: -1,
},
form(
{
className: classnames("conditional-breakpoint-panel", {
"log-point": log,
}),
onSubmit: this.onFormSubmit,
ref: this.formRef,
},
div(
{
className: "input-container",
},
div(
{
className: "prompt",
},
"»"
),
div({
className: "inline-codemirror-container",
ref: el => this.setupAndAppendInlineEditor(el, editor),
})
),
div(
{
className: "conditional-breakpoint-panel-controls",
},
log
? label(
{
className: "conditional-breakpoint-panel-checkbox-label",
htmlFor: "showStacktrace",
},
input({
type: "checkbox",
id: "showStacktrace",
name: "showStacktrace",
defaultChecked:
this.props.breakpoint?.options?.showStacktrace,
"aria-label": L10N.getStr(
"editor.conditionalPanel.logPoint.showStacktrace"
),
}),
L10N.getStr("editor.conditionalPanel.logPoint.showStacktrace")
)
: null,
div(
{
className: "conditional-breakpoint-panel-buttons",
},
isWindows ? saveButton : cancelButton,
isWindows ? cancelButton : saveButton
)
)
)
);
ReactDOM.render(reactElPanel, panel);
return panel;
}
render() {
return null;
}
}
const mapStateToProps = state => {
const location = getConditionalPanelLocation(state);
if (!location) {
return {};
}
const breakpoint = getClosestBreakpoint(state, location);
return {
breakpoint,
location,
log: getLogPointStatus(state),
};
};
const { setBreakpointOptions, openConditionalPanel, closeConditionalPanel } =
actions;
const mapDispatchToProps = {
setBreakpointOptions,
openConditionalPanel,
closeConditionalPanel,
};
export default connect(mapStateToProps, mapDispatchToProps)(ConditionalPanel);