tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ConditionalPanel.js (13548B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import React, { PureComponent } from "devtools/client/shared/vendor/react";
      6 import {
      7  div,
      8  input,
      9  button,
     10  form,
     11  label,
     12 } from "devtools/client/shared/vendor/react-dom-factories";
     13 import ReactDOM from "devtools/client/shared/vendor/react-dom";
     14 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
     15 import { connect } from "devtools/client/shared/vendor/react-redux";
     16 import { toEditorLine } from "../../utils/editor/index";
     17 import { createEditor } from "../../utils/editor/create-editor";
     18 import actions from "../../actions/index";
     19 
     20 import {
     21  getClosestBreakpoint,
     22  getConditionalPanelLocation,
     23  getLogPointStatus,
     24 } from "../../selectors/index";
     25 
     26 const classnames = require("resource://devtools/client/shared/classnames.js");
     27 
     28 export class ConditionalPanel extends PureComponent {
     29  cbPanel;
     30  input;
     31  codeMirror;
     32  panelNode;
     33  scrollParent;
     34 
     35  constructor() {
     36    super();
     37    this.cbPanel = null;
     38    this.breakpointPanelEditor = null;
     39    this.formRef = React.createRef();
     40  }
     41 
     42  static get propTypes() {
     43    return {
     44      breakpoint: PropTypes.object,
     45      closeConditionalPanel: PropTypes.func.isRequired,
     46      editor: PropTypes.object.isRequired,
     47      location: PropTypes.any.isRequired,
     48      log: PropTypes.bool.isRequired,
     49      openConditionalPanel: PropTypes.func.isRequired,
     50      setBreakpointOptions: PropTypes.func.isRequired,
     51      selectedSource: PropTypes.object.isRequired,
     52    };
     53  }
     54 
     55  removeBreakpointPanelEditor() {
     56    if (this.breakpointPanelEditor) {
     57      this.breakpointPanelEditor.destroy();
     58    }
     59    this.breakpointPanelEditor = null;
     60  }
     61 
     62  keepFocusOnInput() {
     63    if (this.input) {
     64      this.input.focus();
     65    } else if (this.breakpointPanelEditor) {
     66      if (!this.breakpointPanelEditor.isDestroyed()) {
     67        this.breakpointPanelEditor.focus();
     68      }
     69    }
     70  }
     71 
     72  onFormSubmit = e => {
     73    if (e && e.preventDefault) {
     74      e.preventDefault();
     75    }
     76    const formData = new FormData(this.formRef.current);
     77    const showStacktrace = formData.get("showStacktrace") === "on";
     78 
     79    if (
     80      !this.breakpointPanelEditor ||
     81      this.breakpointPanelEditor.isDestroyed()
     82    ) {
     83      return;
     84    }
     85    const expression = this.breakpointPanelEditor.getText(null);
     86    this.saveAndClose(expression, showStacktrace);
     87  };
     88 
     89  onPanelBlur = e => {
     90    // if the focus is outside of the conditional panel,
     91    // close/hide the conditional panel
     92    if (
     93      e.relatedTarget &&
     94      e.relatedTarget.closest(".conditional-breakpoint-panel-container")
     95    ) {
     96      return;
     97    }
     98    this.props.closeConditionalPanel();
     99  };
    100 
    101  /**
    102   * Set the breakpoint/logpoint if expression isn't empty, and close the panel.
    103   *
    104   * @param {string} expression: The expression that will be used for setting the
    105   *        conditional breakpoint/logpoint
    106   * @param {boolean} showStacktrace: Whether to show the stacktrace for the logpoint
    107   */
    108  saveAndClose = (expression = null, showStacktrace = false) => {
    109    if (typeof expression === "string") {
    110      const trimmedExpression = expression.trim();
    111      if (trimmedExpression) {
    112        this.setBreakpoint(trimmedExpression, showStacktrace);
    113      } else if (this.props.breakpoint) {
    114        // if the user was editing the condition/log of an existing breakpoint,
    115        // we remove the condition/log.
    116        this.setBreakpoint(null);
    117      }
    118    }
    119 
    120    this.props.closeConditionalPanel();
    121  };
    122 
    123  /**
    124   * Handle inline editor keydown event
    125   *
    126   * @param {Event} e: The keydown event
    127   */
    128  onKey = e => {
    129    if (e.key === "Enter" && !e.shiftKey) {
    130      e.preventDefault();
    131      this.formRef.current.requestSubmit();
    132    } else if (e.key === "Escape") {
    133      this.props.closeConditionalPanel();
    134    }
    135  };
    136 
    137  /**
    138   * Handle inline editor blur event
    139   *
    140   * @param {Event} e: The blur event
    141   */
    142  onBlur = e => {
    143    let explicitOriginalTarget = e?.explicitOriginalTarget;
    144    // The explicit original target can be a text node, in such case retrieve its parent
    145    // element so we can use `closest` on it.
    146    if (explicitOriginalTarget && !Element.isInstance(explicitOriginalTarget)) {
    147      explicitOriginalTarget = explicitOriginalTarget.parentElement;
    148    }
    149 
    150    if (
    151      // if there is no event
    152      // or if the focus is the conditional panel
    153      // do not close the conditional panel
    154      !e ||
    155      (explicitOriginalTarget &&
    156        explicitOriginalTarget.closest(
    157          ".conditional-breakpoint-panel-container"
    158        ))
    159    ) {
    160      return;
    161    }
    162 
    163    this.props.closeConditionalPanel();
    164  };
    165 
    166  setBreakpoint(value, showStacktrace) {
    167    const { log, breakpoint } = this.props;
    168    // If breakpoint is `pending`, props will not contain a breakpoint.
    169    // If source is a URL without location, breakpoint will contain no generatedLocation.
    170    const location =
    171      breakpoint && breakpoint.generatedLocation
    172        ? breakpoint.generatedLocation
    173        : this.props.location;
    174    const options = breakpoint ? breakpoint.options : {};
    175    const type = log ? "logValue" : "condition";
    176    return this.props.setBreakpointOptions(location, {
    177      ...options,
    178      [type]: value,
    179      showStacktrace,
    180    });
    181  }
    182 
    183  clearConditionalPanel() {
    184    if (this.cbPanel) {
    185      this.cbPanel.clear();
    186      this.cbPanel = null;
    187    }
    188    if (this.scrollParent) {
    189      this.scrollParent.removeEventListener("scroll", this.repositionOnScroll);
    190    }
    191  }
    192 
    193  repositionOnScroll = () => {
    194    if (this.panelNode && this.scrollParent) {
    195      const { scrollLeft } = this.scrollParent;
    196      this.panelNode.style.transform = `translateX(${scrollLeft}px)`;
    197    }
    198  };
    199 
    200  showConditionalPanel(prevProps) {
    201    const { location, log, editor, breakpoint, selectedSource } = this.props;
    202 
    203    if (!selectedSource || !location) {
    204      this.removeBreakpointPanelEditor();
    205      return;
    206    }
    207    // When breakpoint is removed
    208    if (prevProps?.breakpoint && !breakpoint) {
    209      editor.removeLineContentMarker(editor.markerTypes.CONDITIONAL_BP_MARKER);
    210      this.removeBreakpointPanelEditor();
    211      return;
    212    }
    213    if (selectedSource.id !== location.source.id) {
    214      editor.removeLineContentMarker(editor.markerTypes.CONDITIONAL_BP_MARKER);
    215      this.removeBreakpointPanelEditor();
    216      return;
    217    }
    218    const line = toEditorLine(location.source, location.line || 0);
    219    editor.setLineContentMarker({
    220      id: editor.markerTypes.CONDITIONAL_BP_MARKER,
    221      lines: [{ line }],
    222      renderAsBlock: true,
    223      createLineElementNode: () => {
    224        // Create a Codemirror editor for the breakpoint panel
    225 
    226        const onEnterKeyMapConfig = {
    227          preventDefault: true,
    228          stopPropagation: true,
    229          run: () => this.formRef.current.requestSubmit(),
    230        };
    231 
    232        const breakpointPanelEditor = createEditor({
    233          cm6: true,
    234          readOnly: false,
    235          lineNumbers: false,
    236          lineWrapping: true,
    237          placeholder: L10N.getStr(
    238            log
    239              ? "editor.conditionalPanel.logPoint.placeholder2"
    240              : "editor.conditionalPanel.placeholder2"
    241          ),
    242          keyMap: [
    243            {
    244              key: "Enter",
    245              ...onEnterKeyMapConfig,
    246            },
    247            {
    248              key: "Mod-Enter",
    249              ...onEnterKeyMapConfig,
    250            },
    251            {
    252              key: "Escape",
    253              preventDefault: true,
    254              stopPropagation: true,
    255              run: () => this.props.closeConditionalPanel(),
    256            },
    257          ],
    258        });
    259 
    260        this.breakpointPanelEditor = breakpointPanelEditor;
    261        return this.renderConditionalPanel(this.props, breakpointPanelEditor);
    262      },
    263    });
    264  }
    265 
    266  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    267  UNSAFE_componentWillMount() {
    268    this.showConditionalPanel();
    269  }
    270 
    271  componentDidUpdate(prevProps) {
    272    this.showConditionalPanel(prevProps);
    273    this.keepFocusOnInput();
    274  }
    275 
    276  componentWillUnmount() {
    277    // This is called if CodeMirror is re-initializing itself before the
    278    // user closes the conditional panel. Clear the widget, and re-render it
    279    // as soon as this component gets remounted
    280    const { editor } = this.props;
    281    editor.removeLineContentMarker(editor.markerTypes.CONDITIONAL_BP_MARKER);
    282    this.removeBreakpointPanelEditor();
    283  }
    284 
    285  componentDidMount() {
    286    if (this.formRef && this.formRef.current) {
    287      const checkbox = this.formRef.current.querySelector("#showStacktrace");
    288      if (checkbox) {
    289        checkbox.checked = this.props.breakpoint?.options?.showStacktrace;
    290      }
    291    }
    292  }
    293 
    294  renderToWidget(props) {
    295    if (this.cbPanel) {
    296      this.clearConditionalPanel();
    297    }
    298    const { location, editor } = props;
    299    if (!location) {
    300      return;
    301    }
    302 
    303    const editorLine = toEditorLine(location.source, location.line || 0);
    304    this.cbPanel = editor.codeMirror.addLineWidget(
    305      editorLine,
    306      this.renderConditionalPanel(props, editor),
    307      {
    308        coverGutter: true,
    309        noHScroll: true,
    310      }
    311    );
    312 
    313    if (this.input) {
    314      let parent = this.input.parentNode;
    315      while (parent) {
    316        if (
    317          HTMLElement.isInstance(parent) &&
    318          parent.classList.contains("CodeMirror-scroll")
    319        ) {
    320          this.scrollParent = parent;
    321          break;
    322        }
    323        parent = parent.parentNode;
    324      }
    325 
    326      if (this.scrollParent) {
    327        this.scrollParent.addEventListener("scroll", this.repositionOnScroll);
    328        this.repositionOnScroll();
    329      }
    330    }
    331  }
    332 
    333  setupAndAppendInlineEditor = (el, editor) => {
    334    editor.appendToLocalElement(el);
    335    editor.on("blur", e => this.onBlur(e));
    336 
    337    editor.setText(this.getDefaultValue());
    338    editor.focus();
    339    editor.selectAll();
    340  };
    341 
    342  getDefaultValue() {
    343    const { breakpoint, log } = this.props;
    344    const options = breakpoint?.options || {};
    345    const value = log ? options.logValue : options.condition;
    346    return value || "";
    347  }
    348 
    349  renderConditionalPanel(props, editor) {
    350    const { log } = props;
    351    const panel = document.createElement("div");
    352    const isWindows = Services.appinfo.OS.startsWith("WINNT");
    353 
    354    const isCreating = !this.props.breakpoint;
    355 
    356    const saveButton = button(
    357      {
    358        type: "submit",
    359        id: "save-logpoint",
    360        className: "devtools-button conditional-breakpoint-panel-save-button",
    361      },
    362      L10N.getStr(
    363        isCreating
    364          ? "editor.conditionalPanel.create"
    365          : "editor.conditionalPanel.update"
    366      )
    367    );
    368 
    369    const cancelButton = button(
    370      {
    371        type: "button",
    372        className: "devtools-button conditional-breakpoint-panel-cancel-button",
    373        onClick: () => this.props.closeConditionalPanel(),
    374      },
    375      L10N.getStr("editor.conditionalPanel.cancel")
    376    );
    377 
    378    // CodeMirror6 can't have margin on a block widget, so we need to wrap the actual
    379    // panel inside a container which won't have any margin
    380    const reactElPanel = div(
    381      {
    382        className: "conditional-breakpoint-panel-container",
    383        onBlur: this.onPanelBlur,
    384        tabIndex: -1,
    385      },
    386      form(
    387        {
    388          className: classnames("conditional-breakpoint-panel", {
    389            "log-point": log,
    390          }),
    391          onSubmit: this.onFormSubmit,
    392          ref: this.formRef,
    393        },
    394        div(
    395          {
    396            className: "input-container",
    397          },
    398          div(
    399            {
    400              className: "prompt",
    401            },
    402            "ยป"
    403          ),
    404          div({
    405            className: "inline-codemirror-container",
    406            ref: el => this.setupAndAppendInlineEditor(el, editor),
    407          })
    408        ),
    409        div(
    410          {
    411            className: "conditional-breakpoint-panel-controls",
    412          },
    413          log
    414            ? label(
    415                {
    416                  className: "conditional-breakpoint-panel-checkbox-label",
    417                  htmlFor: "showStacktrace",
    418                },
    419                input({
    420                  type: "checkbox",
    421                  id: "showStacktrace",
    422                  name: "showStacktrace",
    423                  defaultChecked:
    424                    this.props.breakpoint?.options?.showStacktrace,
    425                  "aria-label": L10N.getStr(
    426                    "editor.conditionalPanel.logPoint.showStacktrace"
    427                  ),
    428                }),
    429                L10N.getStr("editor.conditionalPanel.logPoint.showStacktrace")
    430              )
    431            : null,
    432          div(
    433            {
    434              className: "conditional-breakpoint-panel-buttons",
    435            },
    436            isWindows ? saveButton : cancelButton,
    437            isWindows ? cancelButton : saveButton
    438          )
    439        )
    440      )
    441    );
    442    ReactDOM.render(reactElPanel, panel);
    443    return panel;
    444  }
    445 
    446  render() {
    447    return null;
    448  }
    449 }
    450 
    451 const mapStateToProps = state => {
    452  const location = getConditionalPanelLocation(state);
    453 
    454  if (!location) {
    455    return {};
    456  }
    457 
    458  const breakpoint = getClosestBreakpoint(state, location);
    459 
    460  return {
    461    breakpoint,
    462    location,
    463    log: getLogPointStatus(state),
    464  };
    465 };
    466 
    467 const { setBreakpointOptions, openConditionalPanel, closeConditionalPanel } =
    468  actions;
    469 
    470 const mapDispatchToProps = {
    471  setBreakpointOptions,
    472  openConditionalPanel,
    473  closeConditionalPanel,
    474 };
    475 
    476 export default connect(mapStateToProps, mapDispatchToProps)(ConditionalPanel);