SourcesTree.js (12927B)
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 // Dependencies 6 import React, { 7 Component, 8 Fragment, 9 } from "devtools/client/shared/vendor/react"; 10 import { 11 div, 12 button, 13 span, 14 footer, 15 } from "devtools/client/shared/vendor/react-dom-factories"; 16 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 17 import { connect } from "devtools/client/shared/vendor/react-redux"; 18 const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js"); 19 const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js"); 20 const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js"); 21 import { prefs } from "../../utils/prefs"; 22 23 // Selectors 24 import { 25 getMainThreadHost, 26 getExpandedState, 27 getProjectDirectoryRoot, 28 getProjectDirectoryRootName, 29 getProjectDirectoryRootFullName, 30 getSourcesTreeSources, 31 getFocusedSourceItem, 32 getHideIgnoredSources, 33 } from "../../selectors/index"; 34 35 // Actions 36 import actions from "../../actions/index"; 37 38 // Components 39 import SourcesTreeItem from "./SourcesTreeItem"; 40 import DebuggerImage from "../shared/DebuggerImage"; 41 42 const classnames = require("resource://devtools/client/shared/classnames.js"); 43 const Tree = require("resource://devtools/client/shared/components/Tree.js"); 44 45 function shouldAutoExpand(item, mainThreadHost) { 46 // There is only one case where we want to force auto expand, 47 // when we are on the group of the page's domain. 48 return item.type == "group" && item.groupName === mainThreadHost; 49 } 50 51 class SourcesTree extends Component { 52 constructor(props) { 53 super(props); 54 55 this.state = { 56 hasOverflow: undefined, 57 }; 58 59 // Monitor resize to check if the source tree shows a scrollbar. 60 this.onResize = this.onResize.bind(this); 61 this.resizeObserver = new ResizeObserver(this.onResize); 62 } 63 64 static get propTypes() { 65 return { 66 mainThreadHost: PropTypes.string, 67 expanded: PropTypes.object.isRequired, 68 focusItem: PropTypes.func.isRequired, 69 focused: PropTypes.object, 70 projectRoot: PropTypes.string.isRequired, 71 selectSource: PropTypes.func.isRequired, 72 setExpandedState: PropTypes.func.isRequired, 73 rootItems: PropTypes.array.isRequired, 74 clearProjectDirectoryRoot: PropTypes.func.isRequired, 75 projectRootName: PropTypes.string.isRequired, 76 setHideOrShowIgnoredSources: PropTypes.func.isRequired, 77 hideIgnoredSources: PropTypes.bool.isRequired, 78 }; 79 } 80 81 onResize() { 82 const tree = this.refs.tree; 83 if (!tree) { 84 return; 85 } 86 87 // "treeRef" is created via createRef() in the Tree component. 88 const treeEl = tree.treeRef.current; 89 const hasOverflow = treeEl.scrollHeight > treeEl.clientHeight; 90 if (hasOverflow !== this.state.hasOverflow) { 91 this.setState({ hasOverflow }); 92 } 93 } 94 95 componentDidUpdate() { 96 this.onResize(); 97 } 98 99 componentDidMount() { 100 this.resizeObserver.observe(this.refs.pane); 101 this.onResize(); 102 } 103 104 componentWillUnmount() { 105 this.resizeObserver.disconnect(); 106 } 107 108 selectSourceItem = item => { 109 // Note that when the source is pretty printed, `item.source` still refers to the minified source. 110 // `mayBeSelectMappedSource` function within selectSource/selectLocation action will handle this edgecase 111 // and ensure selecting the pretty printed source, if relevant. 112 this.props.selectSource(item.source, item.sourceActor); 113 }; 114 115 onFocus = item => { 116 this.props.focusItem(item); 117 }; 118 119 onActivate = item => { 120 if (item.type == "source") { 121 this.selectSourceItem(item); 122 } 123 }; 124 125 onExpand = (item, shouldIncludeChildren) => { 126 this.setExpanded(item, true, shouldIncludeChildren); 127 }; 128 129 onCollapse = (item, shouldIncludeChildren) => { 130 this.setExpanded(item, false, shouldIncludeChildren); 131 }; 132 133 setExpanded = (item, isExpanded, shouldIncludeChildren) => { 134 // Note that setExpandedState relies on us to clone this Set 135 // which is going to be store as-is in the reducer. 136 const expanded = new Set(this.props.expanded); 137 138 let changed = false; 139 const expandItem = i => { 140 const key = this.getKey(i); 141 if (isExpanded) { 142 changed |= !expanded.has(key); 143 expanded.add(key); 144 } else { 145 changed |= expanded.has(key); 146 expanded.delete(key); 147 } 148 }; 149 expandItem(item); 150 151 if (shouldIncludeChildren) { 152 let parents = [item]; 153 while (parents.length) { 154 const children = []; 155 for (const parent of parents) { 156 for (const child of this.getChildren(parent)) { 157 expandItem(child); 158 children.push(child); 159 } 160 } 161 parents = children; 162 } 163 } 164 if (changed) { 165 this.props.setExpandedState(expanded); 166 } 167 }; 168 169 isEmpty() { 170 return !this.getRoots().length; 171 } 172 173 renderEmptyElement(message) { 174 return div( 175 { 176 key: "empty", 177 className: "no-sources-message", 178 }, 179 message 180 ); 181 } 182 183 getRoots = () => { 184 return this.props.rootItems; 185 }; 186 187 getKey = item => { 188 // As this is used as React key in Tree component, 189 // we need to update the key when switching to a new project root 190 // otherwise these items won't be updated and will have a buggy padding start. 191 const { projectRoot } = this.props; 192 if (projectRoot) { 193 return projectRoot + item.uniquePath; 194 } 195 return item.uniquePath; 196 }; 197 198 getChildren = item => { 199 // This is the precial magic that coalesce "empty" folders, 200 // i.e folders which have only one sub-folder as children. 201 function skipEmptyDirectories(directory) { 202 if (directory.type != "directory") { 203 return directory; 204 } 205 if ( 206 directory.children.length == 1 && 207 directory.children[0].type == "directory" 208 ) { 209 return skipEmptyDirectories(directory.children[0]); 210 } 211 return directory; 212 } 213 if (item.type == "thread") { 214 return item.children; 215 } else if (item.type == "group" || item.type == "directory") { 216 return item.children.map(skipEmptyDirectories); 217 } 218 return []; 219 }; 220 221 getParent = item => { 222 if (item.type == "thread") { 223 return null; 224 } 225 const { rootItems } = this.props; 226 // This is the second magic which skip empty folders 227 // (See getChildren comment) 228 function skipEmptyDirectories(directory) { 229 if ( 230 directory.type == "group" || 231 directory.type == "thread" || 232 rootItems.includes(directory) 233 ) { 234 return directory; 235 } 236 if ( 237 directory.children.length == 1 && 238 directory.children[0].type == "directory" 239 ) { 240 return skipEmptyDirectories(directory.parent); 241 } 242 return directory; 243 } 244 return skipEmptyDirectories(item.parent); 245 }; 246 247 renderProjectRootHeader() { 248 const { projectRootName, projectRootFullName } = this.props; 249 250 if (!projectRootName) { 251 return null; 252 } 253 return div( 254 { 255 key: "root", 256 className: "sources-clear-root-container", 257 }, 258 button( 259 { 260 className: "sources-clear-root", 261 onClick: () => this.props.clearProjectDirectoryRoot(), 262 title: L10N.getFormatStr("removeDirectoryRoot.label"), 263 }, 264 React.createElement(DebuggerImage, { 265 name: "back", 266 }) 267 ), 268 div({ className: "devtools-separator" }), 269 span( 270 { 271 className: "sources-clear-root-label", 272 title: L10N.getFormatStr( 273 "directoryRoot.tooltip.label", 274 projectRootFullName || projectRootName 275 ), 276 }, 277 projectRootName 278 ) 279 ); 280 } 281 282 renderItem = (item, depth, focused, arrow, expanded) => { 283 const { mainThreadHost } = this.props; 284 return React.createElement(SourcesTreeItem, { 285 arrow, 286 item, 287 depth, 288 focused, 289 autoExpand: shouldAutoExpand(item, mainThreadHost), 290 expanded, 291 focusItem: this.onFocus, 292 selectSourceItem: this.selectSourceItem, 293 setExpanded: this.setExpanded, 294 getParent: this.getParent, 295 }); 296 }; 297 298 renderTree() { 299 const { expanded, focused } = this.props; 300 301 const treeProps = { 302 autoExpandAll: false, 303 autoExpandDepth: 1, 304 expanded, 305 focused, 306 getChildren: this.getChildren, 307 getParent: this.getParent, 308 getKey: this.getKey, 309 getRoots: this.getRoots, 310 onCollapse: this.onCollapse, 311 onExpand: this.onExpand, 312 onFocus: this.onFocus, 313 isExpanded: item => { 314 return this.props.expanded.has(this.getKey(item)); 315 }, 316 onActivate: this.onActivate, 317 ref: "tree", 318 renderItem: this.renderItem, 319 preventBlur: true, 320 }; 321 return React.createElement(Tree, treeProps); 322 } 323 324 renderPane(child) { 325 const { projectRoot } = this.props; 326 return div( 327 { 328 key: "pane", 329 className: classnames("sources-pane", { 330 "sources-list-custom-root": !!projectRoot, 331 }), 332 }, 333 child 334 ); 335 } 336 337 renderFooter() { 338 if (this.props.hideIgnoredSources) { 339 return footer( 340 { 341 className: "source-list-footer", 342 }, 343 L10N.getStr("ignoredSourcesHidden"), 344 button( 345 { 346 className: "devtools-togglebutton", 347 onClick: () => this.props.setHideOrShowIgnoredSources(false), 348 title: L10N.getStr("showIgnoredSources.tooltip.label"), 349 }, 350 L10N.getStr("showIgnoredSources") 351 ) 352 ); 353 } 354 return null; 355 } 356 357 renderSettingsButton() { 358 const { toolboxDoc } = this.context; 359 return React.createElement( 360 MenuButton, 361 { 362 menuId: "sources-tree-settings-menu-button", 363 toolboxDoc, 364 className: 365 "devtools-button command-bar-button debugger-settings-menu-button", 366 title: L10N.getStr("sources-settings.button.label"), 367 "aria-label": L10N.getStr("sources-settings.button.label"), 368 }, 369 () => this.renderSettingsMenuItems() 370 ); 371 } 372 373 renderSettingsMenuItems() { 374 return React.createElement( 375 MenuList, 376 { 377 id: "sources-tree-settings-menu-list", 378 }, 379 React.createElement(MenuItem, { 380 key: "debugger-settings-menu-item-hide-ignored-sources", 381 className: "menu-item debugger-settings-menu-item-hide-ignored-sources", 382 checked: prefs.hideIgnoredSources, 383 label: L10N.getStr("settings.hideIgnoredSources.label"), 384 tooltip: L10N.getStr("settings.hideIgnoredSources.tooltip"), 385 onClick: () => 386 this.props.setHideOrShowIgnoredSources(!prefs.hideIgnoredSources), 387 }), 388 React.createElement(MenuItem, { 389 key: "debugger-settings-menu-item-show-content-scripts", 390 className: "menu-item debugger-settings-menu-item-show-content-scripts", 391 checked: prefs.showContentScripts, 392 label: L10N.getStr("sources-settings.showContentScripts.label"), 393 tooltip: L10N.getStr("sources-settings.showContentScripts.tooltip"), 394 onClick: () => 395 this.props.setShowContentScripts(!prefs.showContentScripts), 396 }) 397 ); 398 } 399 400 render() { 401 const { projectRoot } = this.props; 402 return div( 403 { 404 key: "pane", 405 ref: "pane", 406 className: classnames("sources-list", { 407 "sources-list-custom-root": !!projectRoot, 408 "sources-list-has-overflow": this.state.hasOverflow, 409 }), 410 }, 411 this.renderSettingsButton(), 412 this.renderProjectRootHeader(), 413 this.isEmpty() 414 ? this.renderEmptyElement( 415 L10N.getStr( 416 projectRoot ? "noSourcesInDirectoryRootText" : "noSourcesText" 417 ) 418 ) 419 : React.createElement( 420 Fragment, 421 null, 422 this.renderTree(), 423 this.renderFooter() 424 ) 425 ); 426 } 427 } 428 429 SourcesTree.contextTypes = { 430 toolboxDoc: PropTypes.object, 431 }; 432 433 const mapStateToProps = state => { 434 return { 435 mainThreadHost: getMainThreadHost(state), 436 expanded: getExpandedState(state), 437 focused: getFocusedSourceItem(state), 438 projectRoot: getProjectDirectoryRoot(state), 439 rootItems: getSourcesTreeSources(state), 440 projectRootName: getProjectDirectoryRootName(state), 441 projectRootFullName: getProjectDirectoryRootFullName(state), 442 hideIgnoredSources: getHideIgnoredSources(state), 443 }; 444 }; 445 446 export default connect(mapStateToProps, { 447 selectSource: actions.selectSource, 448 setExpandedState: actions.setExpandedState, 449 focusItem: actions.focusItem, 450 clearProjectDirectoryRoot: actions.clearProjectDirectoryRoot, 451 setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources, 452 setShowContentScripts: actions.setShowContentScripts, 453 })(SourcesTree);