ExceptionPopup.js (3754B)
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 { div, span } from "devtools/client/shared/vendor/react-dom-factories"; 7 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 8 import { connect } from "devtools/client/shared/vendor/react-redux"; 9 10 const Reps = ChromeUtils.importESModule( 11 "resource://devtools/client/shared/components/reps/index.mjs" 12 ); 13 const { 14 REPS: { StringRep }, 15 } = Reps; 16 17 import actions from "../../../actions/index"; 18 19 import DebuggerImage from "../../shared/DebuggerImage"; 20 const classnames = require("resource://devtools/client/shared/classnames.js"); 21 const ANONYMOUS_FN_NAME = "<anonymous>"; 22 23 // The exception popup works in two modes: 24 // a. when the stacktrace is closed the exception popup 25 // gets closed when the mouse leaves the popup. 26 // b. when the stacktrace is opened the exception popup 27 // gets closed only by clicking outside the popup. 28 class ExceptionPopup extends Component { 29 constructor(props) { 30 super(props); 31 this.state = { 32 isStacktraceExpanded: true, 33 }; 34 } 35 36 static get propTypes() { 37 return { 38 mouseout: PropTypes.func.isRequired, 39 selectSourceURL: PropTypes.func.isRequired, 40 exception: PropTypes.object.isRequired, 41 }; 42 } 43 44 onExceptionMessageClick() { 45 const isStacktraceExpanded = this.state.isStacktraceExpanded; 46 this.setState({ isStacktraceExpanded: !isStacktraceExpanded }); 47 } 48 49 buildStackFrame(frame) { 50 const { filename, lineNumber } = frame; 51 const functionName = frame.functionName || ANONYMOUS_FN_NAME; 52 return div( 53 { 54 className: "frame", 55 onClick: () => 56 this.props.selectSourceURL(filename, { 57 line: lineNumber, 58 }), 59 }, 60 span( 61 { 62 className: "title", 63 }, 64 functionName 65 ), 66 span( 67 { 68 className: "location", 69 }, 70 span( 71 { 72 className: "filename", 73 }, 74 filename 75 ), 76 ":", 77 span( 78 { 79 className: "line", 80 }, 81 lineNumber 82 ) 83 ) 84 ); 85 } 86 87 renderStacktrace(stacktrace) { 88 const isStacktraceExpanded = this.state.isStacktraceExpanded; 89 90 if (stacktrace.length && isStacktraceExpanded) { 91 return div( 92 { 93 className: "exception-stacktrace", 94 }, 95 stacktrace.map(frame => this.buildStackFrame(frame)) 96 ); 97 } 98 return null; 99 } 100 101 renderArrowIcon(stacktrace) { 102 if (stacktrace.length) { 103 return React.createElement(DebuggerImage, { 104 name: "arrow", 105 className: classnames({ 106 expanded: this.state.isStacktraceExpanded, 107 }), 108 }); 109 } 110 return null; 111 } 112 113 render() { 114 const { 115 exception: { stacktrace, errorMessage }, 116 mouseout, 117 } = this.props; 118 return div( 119 { 120 className: "preview-popup exception-popup", 121 dir: "ltr", 122 onMouseLeave: () => mouseout(true, this.state.isStacktraceExpanded), 123 }, 124 div( 125 { 126 className: "exception-message", 127 onClick: () => this.onExceptionMessageClick(), 128 }, 129 this.renderArrowIcon(stacktrace), 130 StringRep.rep({ 131 object: errorMessage, 132 useQuotes: false, 133 className: "exception-text", 134 }) 135 ), 136 this.renderStacktrace(stacktrace) 137 ); 138 } 139 } 140 141 const mapDispatchToProps = { 142 selectSourceURL: actions.selectSourceURL, 143 }; 144 145 export default connect(null, mapDispatchToProps)(ExceptionPopup);