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 };