Footer.js (14431B)
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 button, 9 span, 10 hr, 11 } from "devtools/client/shared/vendor/react-dom-factories"; 12 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 13 import { connect } from "devtools/client/shared/vendor/react-redux"; 14 import actions from "../../actions/index"; 15 import { 16 getSelectedSource, 17 getSelectedLocation, 18 getSelectedSourceTextContent, 19 getPrettySource, 20 getPaneCollapse, 21 isSourceBlackBoxed, 22 canPrettyPrintSource, 23 getPrettyPrintMessage, 24 isSourceOnSourceMapIgnoreList, 25 isSourceMapIgnoreListEnabled, 26 getSelectedMappedSource, 27 getSourceMapErrorForSourceActor, 28 areSourceMapsEnabled, 29 getShouldSelectOriginalLocation, 30 isSourceActorWithSourceMap, 31 getSourceMapResolvedURL, 32 isSelectedMappedSourceLoading, 33 } from "../../selectors/index"; 34 35 import { shouldBlackbox } from "../../utils/source"; 36 37 import { PaneToggleButton } from "../shared/Button/index"; 38 import DebuggerImage from "../shared/DebuggerImage"; 39 40 const classnames = require("resource://devtools/client/shared/classnames.js"); 41 const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js"); 42 const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js"); 43 const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js"); 44 45 class SourceFooter extends PureComponent { 46 static get propTypes() { 47 return { 48 canPrettyPrint: PropTypes.bool.isRequired, 49 prettyPrintMessage: PropTypes.string, 50 endPanelCollapsed: PropTypes.bool.isRequired, 51 horizontal: PropTypes.bool.isRequired, 52 jumpToMappedLocation: PropTypes.func.isRequired, 53 mappedSource: PropTypes.object, 54 selectedSource: PropTypes.object, 55 selectedLocation: PropTypes.object, 56 isSelectedSourceBlackBoxed: PropTypes.bool, 57 sourceLoaded: PropTypes.bool.isRequired, 58 toggleBlackBox: PropTypes.func.isRequired, 59 togglePaneCollapse: PropTypes.func.isRequired, 60 prettyPrintAndSelectSource: PropTypes.func.isRequired, 61 isSourceOnIgnoreList: PropTypes.bool.isRequired, 62 }; 63 } 64 65 prettyPrintButton() { 66 const { 67 selectedSource, 68 canPrettyPrint, 69 prettyPrintMessage, 70 prettyPrintAndSelectSource, 71 removePrettyPrintedSource, 72 sourceLoaded, 73 } = this.props; 74 75 if (!selectedSource) { 76 return null; 77 } 78 79 if (!sourceLoaded && selectedSource.isPrettyPrinted) { 80 return div( 81 { 82 className: "action", 83 key: "pretty-loader", 84 }, 85 React.createElement(DebuggerImage, { 86 name: "loader", 87 className: "spin", 88 }) 89 ); 90 } 91 92 const type = "prettyPrint"; 93 return button( 94 { 95 onClick: () => { 96 if (selectedSource.isPrettyPrinted) { 97 removePrettyPrintedSource(selectedSource); 98 return; 99 } 100 if (!canPrettyPrint) { 101 return; 102 } 103 prettyPrintAndSelectSource(selectedSource); 104 }, 105 className: classnames("action", type, { 106 pretty: selectedSource.isPrettyPrinted, 107 }), 108 key: type, 109 title: prettyPrintMessage, 110 "aria-label": prettyPrintMessage, 111 disabled: !canPrettyPrint && !selectedSource.isPrettyPrinted, 112 }, 113 React.createElement(DebuggerImage, { 114 name: type, 115 }) 116 ); 117 } 118 119 blackBoxButton() { 120 const { 121 selectedSource, 122 isSelectedSourceBlackBoxed, 123 toggleBlackBox, 124 sourceLoaded, 125 isSourceOnIgnoreList, 126 } = this.props; 127 128 if (!selectedSource || !shouldBlackbox(selectedSource)) { 129 return null; 130 } 131 132 let tooltip = isSelectedSourceBlackBoxed 133 ? L10N.getStr("sourceFooter.unignore") 134 : L10N.getStr("sourceFooter.ignore"); 135 136 if (isSourceOnIgnoreList) { 137 tooltip = L10N.getStr("sourceFooter.ignoreList"); 138 } 139 140 const type = "black-box"; 141 return button( 142 { 143 onClick: () => toggleBlackBox(selectedSource), 144 className: classnames("action", type, { 145 active: sourceLoaded, 146 blackboxed: isSelectedSourceBlackBoxed || isSourceOnIgnoreList, 147 }), 148 key: type, 149 title: tooltip, 150 "aria-label": tooltip, 151 disabled: isSourceOnIgnoreList, 152 }, 153 React.createElement(DebuggerImage, { 154 name: "blackBox", 155 }) 156 ); 157 } 158 159 renderToggleButton() { 160 if (this.props.horizontal) { 161 return null; 162 } 163 return React.createElement(PaneToggleButton, { 164 key: "toggle", 165 collapsed: this.props.endPanelCollapsed, 166 horizontal: this.props.horizontal, 167 handleClick: this.props.togglePaneCollapse, 168 position: "end", 169 }); 170 } 171 172 renderCommands() { 173 const commands = [ 174 this.blackBoxButton(), 175 this.prettyPrintButton(), 176 this.renderSourceMapButton(), 177 ].filter(Boolean); 178 179 return commands.length 180 ? div( 181 { 182 className: "commands", 183 }, 184 commands 185 ) 186 : null; 187 } 188 189 renderMappedSource() { 190 const { mappedSource, jumpToMappedLocation, selectedLocation } = this.props; 191 192 if (!mappedSource) { 193 return null; 194 } 195 196 const tooltip = L10N.getFormatStr( 197 mappedSource.isOriginal 198 ? "sourceFooter.mappedGeneratedSource.tooltip" 199 : "sourceFooter.mappedOriginalSource.tooltip", 200 mappedSource.url 201 ); 202 const label = L10N.getFormatStr( 203 mappedSource.isOriginal 204 ? "sourceFooter.mappedOriginalSource.title" 205 : "sourceFooter.mappedGeneratedSource.title", 206 mappedSource.shortName 207 ); 208 return button( 209 { 210 className: "mapped-source", 211 onClick: () => jumpToMappedLocation(selectedLocation), 212 title: tooltip, 213 }, 214 span(null, label) 215 ); 216 } 217 218 renderCursorPosition() { 219 // When we open a new source, there is no particular location selected and the line will be set to zero or falsy 220 if (!this.props.selectedLocation || !this.props.selectedLocation.line) { 221 return null; 222 } 223 224 // Note that line is 1-based while column is 0-based. 225 const { line, column } = this.props.selectedLocation; 226 227 const text = L10N.getFormatStr( 228 "sourceFooter.currentCursorPosition", 229 line, 230 column + 1 231 ); 232 const title = L10N.getFormatStr( 233 "sourceFooter.currentCursorPosition.tooltip", 234 line, 235 column + 1 236 ); 237 return div( 238 { 239 className: "cursor-position", 240 title, 241 }, 242 text 243 ); 244 } 245 246 getSourceMapLabel() { 247 if (!this.props.selectedLocation) { 248 return undefined; 249 } 250 if (!this.props.areSourceMapsEnabled) { 251 return L10N.getStr("sourceFooter.sourceMapButton.disabled"); 252 } 253 if (this.props.sourceMapError) { 254 return undefined; 255 } 256 if (!this.props.isSourceActorWithSourceMap) { 257 return L10N.getStr("sourceFooter.sourceMapButton.sourceNotMapped"); 258 } 259 if ( 260 this.props.selectedLocation.source.isOriginal && 261 !this.props.selectedLocation.source.isPrettyPrinted 262 ) { 263 return L10N.getStr("sourceFooter.sourceMapButton.isOriginalSource"); 264 } 265 return L10N.getStr("sourceFooter.sourceMapButton.isBundleSource"); 266 } 267 268 getSourceMapTitle() { 269 if (this.props.sourceMapError) { 270 return L10N.getFormatStr( 271 "sourceFooter.sourceMapButton.errorTitle", 272 this.props.sourceMapError 273 ); 274 } 275 if (this.props.isSourceMapLoading) { 276 return L10N.getStr("sourceFooter.sourceMapButton.loadingTitle"); 277 } 278 return L10N.getStr("sourceFooter.sourceMapButton.title"); 279 } 280 281 renderSourceMapButton() { 282 const { toolboxDoc } = this.context; 283 284 const selectedSource = this.props.selectedLocation?.source; 285 return React.createElement( 286 MenuButton, 287 { 288 menuId: "debugger-source-map-button", 289 key: "debugger-source-map-button", 290 toolboxDoc, 291 className: classnames("devtools-button", "debugger-source-map-button", { 292 error: !!this.props.sourceMapError, 293 loading: this.props.isSourceMapLoading, 294 disabled: !this.props.areSourceMapsEnabled, 295 "not-mapped": 296 (!selectedSource?.isOriginal || selectedSource?.isPrettyPrinted) && 297 !this.props.isSourceActorWithSourceMap, 298 original: 299 selectedSource?.isOriginal && !selectedSource.isPrettyPrinted, 300 }), 301 title: this.getSourceMapTitle(), 302 label: this.getSourceMapLabel(), 303 icon: true, 304 }, 305 () => this.renderSourceMapMenuItems() 306 ); 307 } 308 309 renderSourceMapMenuItems() { 310 const items = [ 311 React.createElement(MenuItem, { 312 className: "menu-item debugger-source-map-enabled", 313 checked: this.props.areSourceMapsEnabled, 314 label: L10N.getStr("sourceFooter.sourceMapButton.enable"), 315 onClick: this.toggleSourceMaps, 316 }), 317 hr(), 318 React.createElement(MenuItem, { 319 className: "menu-item debugger-source-map-open-original", 320 checked: this.props.shouldSelectOriginalLocation, 321 label: L10N.getStr( 322 "sourceFooter.sourceMapButton.showOriginalSourceByDefault" 323 ), 324 onClick: this.toggleSelectOriginalByDefault, 325 }), 326 ]; 327 328 if (this.props.mappedSource) { 329 items.push( 330 React.createElement(MenuItem, { 331 className: "menu-item debugger-jump-mapped-source", 332 label: this.props.mappedSource.isOriginal 333 ? L10N.getStr("sourceFooter.sourceMapButton.jumpToOriginalSource") 334 : L10N.getStr("sourceFooter.sourceMapButton.jumpToGeneratedSource"), 335 tooltip: this.props.mappedSource.url, 336 onClick: () => 337 this.props.jumpToMappedLocation(this.props.selectedLocation), 338 }) 339 ); 340 } 341 342 if (this.props.resolvedSourceMapURL) { 343 items.push( 344 React.createElement(MenuItem, { 345 className: "menu-item debugger-source-map-link", 346 label: L10N.getStr( 347 "sourceFooter.sourceMapButton.openSourceMapInNewTab" 348 ), 349 onClick: this.openSourceMap, 350 }) 351 ); 352 } 353 return React.createElement( 354 MenuList, 355 { 356 id: "debugger-source-map-list", 357 }, 358 items 359 ); 360 } 361 362 openSourceMap = () => { 363 let line, column; 364 if ( 365 this.props.sourceMapError && 366 this.props.sourceMapError.includes("JSON.parse") 367 ) { 368 const match = this.props.sourceMapError.match( 369 /at line (\d+) column (\d+)/ 370 ); 371 if (match) { 372 line = match[1]; 373 column = match[2]; 374 } 375 } 376 this.props.openSourceMap( 377 this.props.resolvedSourceMapURL || this.props.selectedLocation.source.url, 378 line, 379 column 380 ); 381 }; 382 383 toggleSourceMaps = () => { 384 this.props.toggleSourceMapsEnabled(!this.props.areSourceMapsEnabled); 385 }; 386 387 toggleSelectOriginalByDefault = () => { 388 this.props.setDefaultSelectedLocation( 389 !this.props.shouldSelectOriginalLocation 390 ); 391 this.props.jumpToMappedSelectedLocation(); 392 }; 393 394 render() { 395 return div( 396 { 397 className: "source-footer", 398 }, 399 div( 400 { 401 className: "source-footer-start", 402 }, 403 this.renderCommands() 404 ), 405 div( 406 { 407 className: "source-footer-end", 408 }, 409 this.renderMappedSource(), 410 this.renderCursorPosition(), 411 this.renderToggleButton() 412 ) 413 ); 414 } 415 } 416 SourceFooter.contextTypes = { 417 toolboxDoc: PropTypes.object, 418 }; 419 420 const mapStateToProps = state => { 421 const selectedSource = getSelectedSource(state); 422 const selectedLocation = getSelectedLocation(state); 423 const sourceTextContent = getSelectedSourceTextContent(state); 424 425 const areSourceMapsEnabledProp = areSourceMapsEnabled(state); 426 const isSourceActorWithSourceMapProp = selectedLocation?.sourceActor 427 ? isSourceActorWithSourceMap(state, selectedLocation?.sourceActor.id) 428 : false; 429 const sourceMapError = selectedLocation?.sourceActor 430 ? getSourceMapErrorForSourceActor(state, selectedLocation.sourceActor.id) 431 : null; 432 const mappedSource = getSelectedMappedSource(state); 433 434 const isSourceMapLoading = 435 areSourceMapsEnabledProp && 436 isSourceActorWithSourceMapProp && 437 // `mappedSource` will be null while loading, we need another way to know when it is done computing 438 !mappedSource && 439 isSelectedMappedSourceLoading(state) && 440 !sourceMapError && 441 !selectedSource?.isPrettyPrinted; 442 443 return { 444 selectedSource, 445 selectedLocation, 446 isSelectedSourceBlackBoxed: selectedSource 447 ? isSourceBlackBoxed(state, selectedSource) 448 : null, 449 isSourceOnIgnoreList: 450 isSourceMapIgnoreListEnabled(state) && 451 isSourceOnSourceMapIgnoreList(state, selectedSource), 452 sourceLoaded: !!sourceTextContent, 453 mappedSource, 454 isSourceMapLoading, 455 prettySource: getPrettySource( 456 state, 457 selectedSource ? selectedSource.id : null 458 ), 459 endPanelCollapsed: getPaneCollapse(state, "end"), 460 canPrettyPrint: selectedLocation 461 ? canPrettyPrintSource( 462 state, 463 selectedSource, 464 selectedLocation.sourceActor 465 ) 466 : false, 467 prettyPrintMessage: selectedLocation 468 ? getPrettyPrintMessage(state, selectedLocation) 469 : null, 470 471 sourceMapError, 472 resolvedSourceMapURL: selectedLocation?.sourceActor 473 ? getSourceMapResolvedURL(state, selectedLocation.sourceActor.id) 474 : null, 475 isSourceActorWithSourceMap: isSourceActorWithSourceMapProp, 476 477 areSourceMapsEnabled: areSourceMapsEnabledProp, 478 shouldSelectOriginalLocation: getShouldSelectOriginalLocation(state), 479 }; 480 }; 481 482 export default connect(mapStateToProps, { 483 removePrettyPrintedSource: actions.removePrettyPrintedSource, 484 prettyPrintAndSelectSource: actions.prettyPrintAndSelectSource, 485 toggleBlackBox: actions.toggleBlackBox, 486 jumpToMappedLocation: actions.jumpToMappedLocation, 487 togglePaneCollapse: actions.togglePaneCollapse, 488 toggleSourceMapsEnabled: actions.toggleSourceMapsEnabled, 489 setDefaultSelectedLocation: actions.setDefaultSelectedLocation, 490 jumpToMappedSelectedLocation: actions.jumpToMappedSelectedLocation, 491 openSourceMap: actions.openSourceMap, 492 })(SourceFooter);