/* 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 . */ import React, { Component } from "devtools/client/shared/vendor/react"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import FrameComponent from "./Frame"; import Group from "./Group"; import { collapseFrames } from "../../../utils/pause/frames/index"; const NUM_FRAMES_SHOWN = 7; const isMacOS = Services.appinfo.OS === "Darwin"; class Frames extends Component { constructor(props) { super(props); // This is used to cache the groups based on their group id's // easy access to simpler data structure. This was not put on // the state to avoid unnecessary updates. this.groups = {}; this.state = { showAllFrames: !!props.disableFrameTruncate, currentFrame: "", expandedFrameGroups: this.props.expandedFrameGroups || {}, }; } static get propTypes() { return { disableContextMenu: PropTypes.bool.isRequired, disableFrameTruncate: PropTypes.bool.isRequired, displayFullUrl: PropTypes.bool.isRequired, frames: PropTypes.array.isRequired, frameworkGroupingOn: PropTypes.bool.isRequired, getFrameTitle: PropTypes.func, panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired, selectFrame: PropTypes.func.isRequired, selectedFrame: PropTypes.object, isTracerFrameSelected: PropTypes.bool.isRequired, showFrameContextMenu: PropTypes.func, shouldDisplayOriginalLocation: PropTypes.bool, onExpandFrameGroup: PropTypes.func, expandedFrameGroups: PropTypes.obj, }; } shouldComponentUpdate(nextProps, nextState) { const { frames, selectedFrame, isTracerFrameSelected, frameworkGroupingOn, shouldDisplayOriginalLocation, } = this.props; const { showAllFrames, currentFrame, expandedFrameGroups } = this.state; return ( frames !== nextProps.frames || selectedFrame !== nextProps.selectedFrame || isTracerFrameSelected !== nextProps.isTracerFrameSelected || showAllFrames !== nextState.showAllFrames || currentFrame !== nextState.currentFrame || expandedFrameGroups !== nextState.expandedFrameGroups || frameworkGroupingOn !== nextProps.frameworkGroupingOn || shouldDisplayOriginalLocation !== nextProps.shouldDisplayOriginalLocation ); } toggleFramesDisplay = () => { this.setState(prevState => ({ showAllFrames: !prevState.showAllFrames, })); }; isGroupExpanded(groupId) { return !!this.state.expandedFrameGroups[groupId]; } expandGroup(el) { const { selectedFrame } = this.props; // No need to handles group frame checks for the smart trace if (selectedFrame) { // If a frame within the group is selected, // do not collapse the frame. const isGroupFrameSelected = this.groups[el.id].some( frame => frame.id == this.props.selectedFrame.id ); if (this.isGroupExpanded(el.id) && isGroupFrameSelected) { return; } } const newExpandedGroups = { ...this.state.expandedFrameGroups, [el.id]: !this.state.expandedFrameGroups[el.id], }; this.setState({ expandedFrameGroups: newExpandedGroups }); // Cache the expanded state, for when the callstack is collapsed // expanded again later this.props.onExpandFrameGroup?.(newExpandedGroups); } collapseFrames(frames) { const { frameworkGroupingOn } = this.props; if (!frameworkGroupingOn) { return frames; } return collapseFrames(frames); } truncateFrames(frames) { const numFramesToShow = this.state.showAllFrames ? frames.length : NUM_FRAMES_SHOWN; return frames.slice(0, numFramesToShow); } onFocus(event) { event.stopPropagation(); this.setState({ currentFrame: event.target.id }); } onClick(event) { event.stopPropagation(); const { frames } = this.props; const el = event.target.closest(".frame"); // Ignore non frame elements and frame group title elements if (el == null) { return; } if (el.classList.contains("frames-group")) { this.expandGroup(el); return; } const clickedFrame = frames.find(frame => frame.id == el.id); this.props.selectFrame(clickedFrame); } // eslint-disable-next-line complexity onKeyDown(event) { const element = event.target; const focusedFrame = this.props.frames.find( frame => frame.id == element.id ); const isFrameGroup = element.classList.contains("frames-group"); const nextSibling = element.nextElementSibling; const previousSibling = element.previousElementSibling; if (event.key == "Tab") { if (!element.classList.contains("top-frames-list")) { event.preventDefault(); element.closest(".top-frames-list").focus(); } } else if (event.key == "Home") { this.focusFirstItem(event, previousSibling); } else if (event.key == "End") { this.focusLastItem(event, nextSibling); } else if (event.key == "Enter" || event.key == " ") { event.preventDefault(); if (!isFrameGroup) { this.props.selectFrame(focusedFrame); } else { this.expandGroup(element); } } else if (event.key == "ArrowDown") { event.preventDefault(); if (element.classList.contains("top-frames-list")) { element.firstChild.focus(); return; } if (isFrameGroup) { if (nextSibling == null) { return; } if (nextSibling.classList.contains("frames-list")) { // If on an expanded frame group, jump to the first element inside the group nextSibling.firstChild.focus(); } else if (!nextSibling.classList.contains("frame")) { // Jump any none frame elements e.g async frames nextSibling.nextElementSibling?.focus(); } else { nextSibling.focus(); } } else if (!isFrameGroup) { if (nextSibling == null) { const parentFrameGroup = element.closest(".frames-list"); if (parentFrameGroup) { // Jump to the next item in the parent list if it exists parentFrameGroup.nextElementSibling?.focus(); } } else if (!nextSibling.classList.contains("frame")) { // Jump any none frame elements e.g async frames nextSibling.nextElementSibling?.focus(); } else { nextSibling.focus(); } } } else if (event.key == "ArrowUp") { event.preventDefault(); if (element.classList.contains("top-frames-list")) { element.lastChild.focus(); return; } if (previousSibling == null) { const frameGroup = element.closest(".frames-list"); if (frameGroup) { // Go to the heading of the frame group const frameGroupHeading = frameGroup.previousSibling; frameGroupHeading.focus(); } } else if (previousSibling.classList.contains("frames-list")) { previousSibling.lastChild.focus(); } else if (!previousSibling.classList.contains("frame")) { // Jump any none frame elements e.g async frames previousSibling.previousElementSibling?.focus(); } else { previousSibling.focus(); } } else if (event.key == "ArrowRight") { if (isMacOS && event.metaKey) { this.focusLastItem(event, nextSibling); } } else if (event.key == "ArrowLeft") { if (isMacOS && event.metaKey) { this.focusFirstItem(event, previousSibling); } } } focusFirstItem(event, previousSibling) { event.preventDefault(); const element = event.target; const parent = element.parentNode; const isFrameList = parent.classList.contains("frames-list"); // Already at the first element of the top list if (previousSibling == null && !isFrameList) { return; } if (isFrameList) { // Jump to the first frame in the main list parent.parentNode.firstChild.focus(); return; } parent.firstChild.focus(); } focusLastItem(event, nextSibling) { event.preventDefault(); const element = event.target; const parent = element.parentNode; const isFrameList = parent.classList.contains("frames-list"); // Already at the last element on the list if (nextSibling == null && !isFrameList) { return; } // If the last is an expanded frame group jump to // the last frame in the group. if (isFrameList) { // Jump to the last frame in the main list const parentLastItem = parent.parentNode.lastChild; if (parentLastItem && !parentLastItem.classList.contains("frames-list")) { parentLastItem.focus(); } else { parent.lastChild.focus(); } } else { const lastItem = element.parentNode.lastChild; if (lastItem.classList.contains("frames-list")) { lastItem.lastChild.focus(); } else { lastItem.focus(); } } } onContextMenu(event, frames) { event.stopPropagation(); event.preventDefault(); const el = event.target.closest("div[role='option'].frame"); const currentFrame = frames.find(frame => frame.id == el.id); this.props.showFrameContextMenu(event, currentFrame); } renderFrames(frames) { const { selectFrame, selectedFrame, isTracerFrameSelected, displayFullUrl, getFrameTitle, disableContextMenu, panel, shouldDisplayOriginalLocation, showFrameContextMenu, } = this.props; const framesOrGroups = this.truncateFrames(this.collapseFrames(frames)); // We're not using a