tor-browser

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

Frames.js (13209B)


      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, { Component } from "devtools/client/shared/vendor/react";
      6 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
      7 
      8 import FrameComponent from "./Frame";
      9 import Group from "./Group";
     10 
     11 import { collapseFrames } from "../../../utils/pause/frames/index";
     12 
     13 const NUM_FRAMES_SHOWN = 7;
     14 
     15 const isMacOS = Services.appinfo.OS === "Darwin";
     16 
     17 class Frames extends Component {
     18  constructor(props) {
     19    super(props);
     20    // This is used to cache the groups based on their group id's
     21    // easy access to simpler data structure. This was not put on
     22    // the state to avoid unnecessary updates.
     23    this.groups = {};
     24 
     25    this.state = {
     26      showAllFrames: !!props.disableFrameTruncate,
     27      currentFrame: "",
     28      expandedFrameGroups: this.props.expandedFrameGroups || {},
     29    };
     30  }
     31 
     32  static get propTypes() {
     33    return {
     34      disableContextMenu: PropTypes.bool.isRequired,
     35      disableFrameTruncate: PropTypes.bool.isRequired,
     36      displayFullUrl: PropTypes.bool.isRequired,
     37      frames: PropTypes.array.isRequired,
     38      frameworkGroupingOn: PropTypes.bool.isRequired,
     39      getFrameTitle: PropTypes.func,
     40      panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
     41      selectFrame: PropTypes.func.isRequired,
     42      selectedFrame: PropTypes.object,
     43      isTracerFrameSelected: PropTypes.bool.isRequired,
     44      showFrameContextMenu: PropTypes.func,
     45      shouldDisplayOriginalLocation: PropTypes.bool,
     46      onExpandFrameGroup: PropTypes.func,
     47      expandedFrameGroups: PropTypes.obj,
     48    };
     49  }
     50 
     51  shouldComponentUpdate(nextProps, nextState) {
     52    const {
     53      frames,
     54      selectedFrame,
     55      isTracerFrameSelected,
     56      frameworkGroupingOn,
     57      shouldDisplayOriginalLocation,
     58    } = this.props;
     59 
     60    const { showAllFrames, currentFrame, expandedFrameGroups } = this.state;
     61    return (
     62      frames !== nextProps.frames ||
     63      selectedFrame !== nextProps.selectedFrame ||
     64      isTracerFrameSelected !== nextProps.isTracerFrameSelected ||
     65      showAllFrames !== nextState.showAllFrames ||
     66      currentFrame !== nextState.currentFrame ||
     67      expandedFrameGroups !== nextState.expandedFrameGroups ||
     68      frameworkGroupingOn !== nextProps.frameworkGroupingOn ||
     69      shouldDisplayOriginalLocation !== nextProps.shouldDisplayOriginalLocation
     70    );
     71  }
     72 
     73  toggleFramesDisplay = () => {
     74    this.setState(prevState => ({
     75      showAllFrames: !prevState.showAllFrames,
     76    }));
     77  };
     78 
     79  isGroupExpanded(groupId) {
     80    return !!this.state.expandedFrameGroups[groupId];
     81  }
     82 
     83  expandGroup(el) {
     84    const { selectedFrame } = this.props;
     85    // No need to handles group frame checks for the smart trace
     86    if (selectedFrame) {
     87      // If a frame within the group is selected,
     88      // do not collapse the frame.
     89      const isGroupFrameSelected = this.groups[el.id].some(
     90        frame => frame.id == this.props.selectedFrame.id
     91      );
     92 
     93      if (this.isGroupExpanded(el.id) && isGroupFrameSelected) {
     94        return;
     95      }
     96    }
     97 
     98    const newExpandedGroups = {
     99      ...this.state.expandedFrameGroups,
    100      [el.id]: !this.state.expandedFrameGroups[el.id],
    101    };
    102    this.setState({ expandedFrameGroups: newExpandedGroups });
    103    // Cache the expanded state, for when the callstack is collapsed
    104    // expanded again later
    105    this.props.onExpandFrameGroup?.(newExpandedGroups);
    106  }
    107 
    108  collapseFrames(frames) {
    109    const { frameworkGroupingOn } = this.props;
    110    if (!frameworkGroupingOn) {
    111      return frames;
    112    }
    113 
    114    return collapseFrames(frames);
    115  }
    116 
    117  truncateFrames(frames) {
    118    const numFramesToShow = this.state.showAllFrames
    119      ? frames.length
    120      : NUM_FRAMES_SHOWN;
    121 
    122    return frames.slice(0, numFramesToShow);
    123  }
    124 
    125  onFocus(event) {
    126    event.stopPropagation();
    127    this.setState({ currentFrame: event.target.id });
    128  }
    129 
    130  onClick(event) {
    131    event.stopPropagation();
    132 
    133    const { frames } = this.props;
    134    const el = event.target.closest(".frame");
    135    // Ignore non frame elements and frame group title elements
    136    if (el == null) {
    137      return;
    138    }
    139    if (el.classList.contains("frames-group")) {
    140      this.expandGroup(el);
    141      return;
    142    }
    143    const clickedFrame = frames.find(frame => frame.id == el.id);
    144    this.props.selectFrame(clickedFrame);
    145  }
    146 
    147  // eslint-disable-next-line complexity
    148  onKeyDown(event) {
    149    const element = event.target;
    150    const focusedFrame = this.props.frames.find(
    151      frame => frame.id == element.id
    152    );
    153    const isFrameGroup = element.classList.contains("frames-group");
    154    const nextSibling = element.nextElementSibling;
    155    const previousSibling = element.previousElementSibling;
    156    if (event.key == "Tab") {
    157      if (!element.classList.contains("top-frames-list")) {
    158        event.preventDefault();
    159        element.closest(".top-frames-list").focus();
    160      }
    161    } else if (event.key == "Home") {
    162      this.focusFirstItem(event, previousSibling);
    163    } else if (event.key == "End") {
    164      this.focusLastItem(event, nextSibling);
    165    } else if (event.key == "Enter" || event.key == " ") {
    166      event.preventDefault();
    167      if (!isFrameGroup) {
    168        this.props.selectFrame(focusedFrame);
    169      } else {
    170        this.expandGroup(element);
    171      }
    172    } else if (event.key == "ArrowDown") {
    173      event.preventDefault();
    174      if (element.classList.contains("top-frames-list")) {
    175        element.firstChild.focus();
    176        return;
    177      }
    178      if (isFrameGroup) {
    179        if (nextSibling == null) {
    180          return;
    181        }
    182        if (nextSibling.classList.contains("frames-list")) {
    183          // If on an expanded frame group, jump to the first element inside the group
    184          nextSibling.firstChild.focus();
    185        } else if (!nextSibling.classList.contains("frame")) {
    186          // Jump any none frame elements e.g async frames
    187          nextSibling.nextElementSibling?.focus();
    188        } else {
    189          nextSibling.focus();
    190        }
    191      } else if (!isFrameGroup) {
    192        if (nextSibling == null) {
    193          const parentFrameGroup = element.closest(".frames-list");
    194          if (parentFrameGroup) {
    195            // Jump to the next item in the parent list if it exists
    196            parentFrameGroup.nextElementSibling?.focus();
    197          }
    198        } else if (!nextSibling.classList.contains("frame")) {
    199          // Jump any none frame elements e.g async frames
    200          nextSibling.nextElementSibling?.focus();
    201        } else {
    202          nextSibling.focus();
    203        }
    204      }
    205    } else if (event.key == "ArrowUp") {
    206      event.preventDefault();
    207      if (element.classList.contains("top-frames-list")) {
    208        element.lastChild.focus();
    209        return;
    210      }
    211      if (previousSibling == null) {
    212        const frameGroup = element.closest(".frames-list");
    213        if (frameGroup) {
    214          // Go to the heading of the frame group
    215          const frameGroupHeading = frameGroup.previousSibling;
    216          frameGroupHeading.focus();
    217        }
    218      } else if (previousSibling.classList.contains("frames-list")) {
    219        previousSibling.lastChild.focus();
    220      } else if (!previousSibling.classList.contains("frame")) {
    221        // Jump any none frame elements e.g async frames
    222        previousSibling.previousElementSibling?.focus();
    223      } else {
    224        previousSibling.focus();
    225      }
    226    } else if (event.key == "ArrowRight") {
    227      if (isMacOS && event.metaKey) {
    228        this.focusLastItem(event, nextSibling);
    229      }
    230    } else if (event.key == "ArrowLeft") {
    231      if (isMacOS && event.metaKey) {
    232        this.focusFirstItem(event, previousSibling);
    233      }
    234    }
    235  }
    236 
    237  focusFirstItem(event, previousSibling) {
    238    event.preventDefault();
    239    const element = event.target;
    240    const parent = element.parentNode;
    241 
    242    const isFrameList = parent.classList.contains("frames-list");
    243    // Already at the first element of the top list
    244    if (previousSibling == null && !isFrameList) {
    245      return;
    246    }
    247 
    248    if (isFrameList) {
    249      // Jump to the first frame in the main list
    250      parent.parentNode.firstChild.focus();
    251      return;
    252    }
    253    parent.firstChild.focus();
    254  }
    255 
    256  focusLastItem(event, nextSibling) {
    257    event.preventDefault();
    258    const element = event.target;
    259    const parent = element.parentNode;
    260 
    261    const isFrameList = parent.classList.contains("frames-list");
    262    // Already at the last element on the list
    263    if (nextSibling == null && !isFrameList) {
    264      return;
    265    }
    266    // If the last is an expanded frame group jump to
    267    // the last frame in the group.
    268    if (isFrameList) {
    269      // Jump to the last frame in the main list
    270      const parentLastItem = parent.parentNode.lastChild;
    271      if (parentLastItem && !parentLastItem.classList.contains("frames-list")) {
    272        parentLastItem.focus();
    273      } else {
    274        parent.lastChild.focus();
    275      }
    276    } else {
    277      const lastItem = element.parentNode.lastChild;
    278      if (lastItem.classList.contains("frames-list")) {
    279        lastItem.lastChild.focus();
    280      } else {
    281        lastItem.focus();
    282      }
    283    }
    284  }
    285 
    286  onContextMenu(event, frames) {
    287    event.stopPropagation();
    288    event.preventDefault();
    289 
    290    const el = event.target.closest("div[role='option'].frame");
    291    const currentFrame = frames.find(frame => frame.id == el.id);
    292    this.props.showFrameContextMenu(event, currentFrame);
    293  }
    294 
    295  renderFrames(frames) {
    296    const {
    297      selectFrame,
    298      selectedFrame,
    299      isTracerFrameSelected,
    300      displayFullUrl,
    301      getFrameTitle,
    302      disableContextMenu,
    303      panel,
    304      shouldDisplayOriginalLocation,
    305      showFrameContextMenu,
    306    } = this.props;
    307 
    308    const framesOrGroups = this.truncateFrames(this.collapseFrames(frames));
    309 
    310    // We're not using a <ul> because it adds new lines before and after when
    311    // the user copies the trace. Needed for the console which has several
    312    // places where we don't want to have those new lines.
    313    return React.createElement(
    314      "div",
    315      {
    316        className: "top-frames-list",
    317        onClick: e => this.onClick(e, selectedFrame),
    318        onKeyDown: e => this.onKeyDown(e),
    319        onFocus: e => this.onFocus(e),
    320        onContextMenu: disableContextMenu
    321          ? null
    322          : e => this.onContextMenu(e, frames),
    323        "aria-activedescendant": this.state.currentFrame,
    324        "aria-labelledby": "call-stack-pane",
    325        role: "listbox",
    326        tabIndex: 0,
    327      },
    328      framesOrGroups.map((frameOrGroup, index) => {
    329        if (frameOrGroup.id) {
    330          return React.createElement(FrameComponent, {
    331            frame: frameOrGroup,
    332            showFrameContextMenu,
    333            selectFrame,
    334            selectedFrame,
    335            isTracerFrameSelected,
    336            shouldDisplayOriginalLocation,
    337            key: String(frameOrGroup.id),
    338            displayFullUrl,
    339            getFrameTitle,
    340            disableContextMenu,
    341            panel,
    342            index,
    343          });
    344        }
    345        const groupTitle = frameOrGroup[0].library;
    346        const groupId = `${frameOrGroup[0].library}-${index}`;
    347        // Cache the group to use for checking when a group frame
    348        // is selected.
    349        this.groups[groupId] = frameOrGroup;
    350        return React.createElement(Group, {
    351          key: groupId,
    352          group: frameOrGroup,
    353          groupTitle,
    354          groupId,
    355          expanded: this.isGroupExpanded(groupId),
    356          frameIndex: index,
    357          showFrameContextMenu,
    358          selectFrame,
    359          selectedFrame,
    360          isTracerFrameSelected,
    361          displayFullUrl,
    362          getFrameTitle,
    363          disableContextMenu,
    364          panel,
    365          index,
    366        });
    367      })
    368    );
    369  }
    370 
    371  renderToggleButton(frames) {
    372    const { l10n } = this.context;
    373    const buttonMessage = this.state.showAllFrames
    374      ? l10n.getStr("callStack.collapse")
    375      : l10n.getStr("callStack.expand");
    376 
    377    frames = this.collapseFrames(frames);
    378    if (frames.length <= NUM_FRAMES_SHOWN) {
    379      return null;
    380    }
    381    return React.createElement(
    382      "div",
    383      {
    384        className: "show-more-container",
    385      },
    386      React.createElement(
    387        "button",
    388        {
    389          className: "show-more",
    390          onClick: this.toggleFramesDisplay,
    391        },
    392        buttonMessage
    393      )
    394    );
    395  }
    396 
    397  render() {
    398    const { frames, disableFrameTruncate } = this.props;
    399 
    400    if (!frames) {
    401      return React.createElement(
    402        "div",
    403        {
    404          className: "pane frames",
    405        },
    406        React.createElement(
    407          "div",
    408          {
    409            className: "pane-info empty",
    410          },
    411          L10N.getStr("callStack.notPaused")
    412        )
    413      );
    414    }
    415    return React.createElement(
    416      "div",
    417      {
    418        className: "pane frames",
    419      },
    420      this.renderFrames(frames),
    421      disableFrameTruncate ? null : this.renderToggleButton(frames)
    422    );
    423  }
    424 }
    425 
    426 Frames.contextTypes = { l10n: PropTypes.object };
    427 
    428 // Export the non-connected component in order to use it outside of the debugger
    429 // panel (e.g. console, netmonitor, …) via SmartTrace.
    430 export { Frames };