SourcesTreeItem.js (8053B)
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 import SourceIcon from "../shared/SourceIcon"; 11 import DebuggerImage from "../shared/DebuggerImage"; 12 13 import { 14 getHideIgnoredSources, 15 isSourceOverridden, 16 getGeneratedSourceByURL, 17 } from "../../selectors/index"; 18 import actions from "../../actions/index"; 19 20 import { sourceTypes } from "../../utils/source"; 21 import { createLocation } from "../../utils/location"; 22 23 const classnames = require("resource://devtools/client/shared/classnames.js"); 24 25 class SourceTreeItemContents extends Component { 26 static get propTypes() { 27 return { 28 autoExpand: PropTypes.bool.isRequired, 29 depth: PropTypes.number.isRequired, 30 expanded: PropTypes.bool.isRequired, 31 focusItem: PropTypes.func.isRequired, 32 focused: PropTypes.bool.isRequired, 33 hasMatchingGeneratedSource: PropTypes.bool, 34 item: PropTypes.object.isRequired, 35 selectSourceItem: PropTypes.func.isRequired, 36 setExpanded: PropTypes.func.isRequired, 37 getParent: PropTypes.func.isRequired, 38 hideIgnoredSources: PropTypes.bool, 39 arrow: PropTypes.object, 40 }; 41 } 42 43 componentDidMount() { 44 const { autoExpand, item } = this.props; 45 if (autoExpand) { 46 this.props.setExpanded(item, true, false); 47 } 48 } 49 50 onClick = () => { 51 const { item, focusItem, selectSourceItem } = this.props; 52 53 focusItem(item); 54 if (item.type == "source") { 55 selectSourceItem(item); 56 } 57 }; 58 59 onContextMenu = event => { 60 event.stopPropagation(); 61 event.preventDefault(); 62 this.props.showSourceTreeItemContextMenu( 63 event, 64 this.props.item, 65 this.props.depth, 66 this.props.setExpanded, 67 this.renderItemName(), 68 this.props.isSourceOverridden 69 ); 70 }; 71 72 renderIcon(item) { 73 if (item.type == "thread") { 74 const icon = item.thread.targetType.includes("worker") 75 ? "worker" 76 : "window"; 77 return React.createElement(DebuggerImage, { 78 name: icon, 79 }); 80 } 81 if (item.type == "group") { 82 if (item.groupName === "Webpack") { 83 return React.createElement(DebuggerImage, { 84 name: "webpack", 85 }); 86 } else if (item.groupName === "Angular") { 87 return React.createElement(DebuggerImage, { 88 name: "angular", 89 }); 90 } 91 // Check if the group relates to an extension. 92 // This happens when a webextension injects a content script. 93 if (item.isForExtensionSource) { 94 return React.createElement(DebuggerImage, { 95 name: "extension", 96 }); 97 } 98 return React.createElement(DebuggerImage, { 99 name: "globe-small", 100 }); 101 } 102 if (item.type == "directory") { 103 return React.createElement(DebuggerImage, { 104 name: "folder", 105 }); 106 } 107 if (item.type == "source") { 108 const { source, sourceActor } = item; 109 return React.createElement(SourceIcon, { 110 location: createLocation({ 111 source, 112 sourceActor, 113 }), 114 modifier: icon => { 115 // In the SourceTree, extension files should use the file-extension based icon, 116 // whereas we use the extension icon in other Components (eg. source tabs and breakpoints pane). 117 if (icon === "extension") { 118 return sourceTypes[source.displayURL.fileExtension] || "javascript"; 119 } 120 return ( 121 icon + 122 (this.props.isSourceOverridden ? " has-network-override" : "") 123 ); 124 }, 125 }); 126 } 127 return null; 128 } 129 renderItemName() { 130 const { item } = this.props; 131 132 if (item.type == "thread") { 133 const { thread } = item; 134 return ( 135 thread.name + 136 (thread.serviceWorkerStatus ? ` (${thread.serviceWorkerStatus})` : "") 137 ); 138 } 139 if (item.type == "group") { 140 return item.groupName; 141 } 142 if (item.type == "directory") { 143 const parentItem = this.props.getParent(item); 144 return item.path.replace(parentItem.path, "").replace(/^\//, ""); 145 } 146 if (item.type == "source") { 147 return item.source.longName; 148 } 149 150 return null; 151 } 152 153 renderItemTooltip() { 154 const { item } = this.props; 155 156 if (item.type == "thread") { 157 return item.thread.name; 158 } 159 if (item.type == "group") { 160 return item.groupName; 161 } 162 if (item.type == "directory") { 163 return item.path; 164 } 165 if (item.type == "source") { 166 return item.source.url; 167 } 168 169 return null; 170 } 171 172 render() { 173 const { item, focused, hasMatchingGeneratedSource, hideIgnoredSources } = 174 this.props; 175 176 if (hideIgnoredSources && item.isBlackBoxed) { 177 return null; 178 } 179 const suffix = hasMatchingGeneratedSource 180 ? span( 181 { 182 className: "suffix", 183 }, 184 L10N.getStr("sourceFooter.mappedSuffix") 185 ) 186 : null; 187 return div( 188 { 189 className: classnames("node", { 190 focused, 191 blackboxed: item.type == "source" && item.isBlackBoxed, 192 }), 193 key: item.path, 194 onClick: this.onClick, 195 onContextMenu: this.onContextMenu, 196 title: this.renderItemTooltip(), 197 }, 198 this.props.arrow, 199 this.renderIcon(item), 200 span( 201 { 202 className: "label", 203 }, 204 this.renderItemName(), 205 suffix 206 ) 207 ); 208 } 209 } 210 211 function getHasMatchingGeneratedSource(state, source) { 212 if (!source || !source.isOriginal) { 213 return false; 214 } 215 216 return !!getGeneratedSourceByURL(state, source.url); 217 } 218 219 const toolboxMapStateToProps = (state, props) => { 220 const { item } = props; 221 return { 222 isSourceOverridden: isSourceOverridden(state, item.source), 223 }; 224 }; 225 226 const SourceTreeItemInner = connect(toolboxMapStateToProps, {}, undefined, { 227 storeKey: "toolbox-store", 228 })(SourceTreeItemContents); 229 230 class SourcesTreeItem extends Component { 231 static get propTypes() { 232 return { 233 autoExpand: PropTypes.bool.isRequired, 234 depth: PropTypes.bool.isRequired, 235 expanded: PropTypes.bool.isRequired, 236 focusItem: PropTypes.func.isRequired, 237 focused: PropTypes.bool.isRequired, 238 hasMatchingGeneratedSource: PropTypes.bool.isRequired, 239 item: PropTypes.object.isRequired, 240 selectSourceItem: PropTypes.func.isRequired, 241 setExpanded: PropTypes.func.isRequired, 242 showSourceTreeItemContextMenu: PropTypes.func.isRequired, 243 getParent: PropTypes.func.isRequired, 244 hideIgnoredSources: PropTypes.bool, 245 arrow: PropTypes.object, 246 }; 247 } 248 249 render() { 250 return React.createElement(SourceTreeItemInner, { 251 autoExpand: this.props.autoExpand, 252 depth: this.props.depth, 253 expanded: this.props.expanded, 254 focusItem: this.props.focusItem, 255 focused: this.props.focused, 256 hasMatchingGeneratedSource: this.props.hasMatchingGeneratedSource, 257 item: this.props.item, 258 selectSourceItem: this.props.selectSourceItem, 259 setExpanded: this.props.setExpanded, 260 showSourceTreeItemContextMenu: this.props.showSourceTreeItemContextMenu, 261 getParent: this.props.getParent, 262 hideIgnoredSources: this.props.hideIgnoredSources, 263 arrow: this.props.arrow, 264 }); 265 } 266 } 267 268 const mapStateToProps = (state, props) => { 269 const { item } = props; 270 if (item.type == "source") { 271 const { source } = item; 272 return { 273 hasMatchingGeneratedSource: getHasMatchingGeneratedSource(state, source), 274 hideIgnoredSources: getHideIgnoredSources(state), 275 }; 276 } 277 return {}; 278 }; 279 280 export default connect(mapStateToProps, { 281 showSourceTreeItemContextMenu: actions.showSourceTreeItemContextMenu, 282 })(SourcesTreeItem);