Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
// Dependencies
import React, {
Component,
Fragment,
} from "devtools/client/shared/vendor/react";
import {
div,
button,
span,
footer,
} from "devtools/client/shared/vendor/react-dom-factories";
import PropTypes from "devtools/client/shared/vendor/react-prop-types";
import { connect } from "devtools/client/shared/vendor/react-redux";
const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js");
const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js");
import { prefs } from "../../utils/prefs";
// Selectors
import {
getMainThreadHost,
getExpandedState,
getProjectDirectoryRoot,
getProjectDirectoryRootName,
getSourcesTreeSources,
getFocusedSourceItem,
getHideIgnoredSources,
} from "../../selectors/index";
// Actions
import actions from "../../actions/index";
// Components
import SourcesTreeItem from "./SourcesTreeItem";
import AccessibleImage from "../shared/AccessibleImage";
const classnames = require("resource://devtools/client/shared/classnames.js");
const Tree = require("resource://devtools/client/shared/components/Tree.js");
function shouldAutoExpand(item, mainThreadHost) {
// There is only one case where we want to force auto expand,
// when we are on the group of the page's domain.
return item.type == "group" && item.groupName === mainThreadHost;
}
class SourcesTree extends Component {
constructor(props) {
super(props);
this.state = {};
}
static get propTypes() {
return {
mainThreadHost: PropTypes.string,
expanded: PropTypes.object.isRequired,
focusItem: PropTypes.func.isRequired,
focused: PropTypes.object,
projectRoot: PropTypes.string.isRequired,
selectSource: PropTypes.func.isRequired,
setExpandedState: PropTypes.func.isRequired,
rootItems: PropTypes.array.isRequired,
clearProjectDirectoryRoot: PropTypes.func.isRequired,
projectRootName: PropTypes.string.isRequired,
setHideOrShowIgnoredSources: PropTypes.func.isRequired,
hideIgnoredSources: PropTypes.bool.isRequired,
};
}
selectSourceItem = item => {
this.props.selectSource(item.source, item.sourceActor);
};
onFocus = item => {
this.props.focusItem(item);
};
onActivate = item => {
if (item.type == "source") {
this.selectSourceItem(item);
}
};
onExpand = (item, shouldIncludeChildren) => {
this.setExpanded(item, true, shouldIncludeChildren);
};
onCollapse = (item, shouldIncludeChildren) => {
this.setExpanded(item, false, shouldIncludeChildren);
};
setExpanded = (item, isExpanded, shouldIncludeChildren) => {
// Note that setExpandedState relies on us to clone this Set
// which is going to be store as-is in the reducer.
const expanded = new Set(this.props.expanded);
let changed = false;
const expandItem = i => {
const key = this.getKey(i);
if (isExpanded) {
changed |= !expanded.has(key);
expanded.add(key);
} else {
changed |= expanded.has(key);
expanded.delete(key);
}
};
expandItem(item);
if (shouldIncludeChildren) {
let parents = [item];
while (parents.length) {
const children = [];
for (const parent of parents) {
for (const child of this.getChildren(parent)) {
expandItem(child);
children.push(child);
}
}
parents = children;
}
}
if (changed) {
this.props.setExpandedState(expanded);
}
};
isEmpty() {
return !this.getRoots().length;
}
renderEmptyElement(message) {
return div(
{
key: "empty",
className: "no-sources-message",
},
message
);
}
getRoots = () => {
return this.props.rootItems;
};
getKey = item => {
// As this is used as React key in Tree component,
// we need to update the key when switching to a new project root
// otherwise these items won't be updated and will have a buggy padding start.
const { projectRoot } = this.props;
if (projectRoot) {
return projectRoot + item.uniquePath;
}
return item.uniquePath;
};
getChildren = item => {
// This is the precial magic that coalesce "empty" folders,
// i.e folders which have only one sub-folder as children.
function skipEmptyDirectories(directory) {
if (directory.type != "directory") {
return directory;
}
if (
directory.children.length == 1 &&
directory.children[0].type == "directory"
) {
return skipEmptyDirectories(directory.children[0]);
}
return directory;
}
if (item.type == "thread") {
return item.children;
} else if (item.type == "group" || item.type == "directory") {
return item.children.map(skipEmptyDirectories);
}
return [];
};
getParent = item => {
if (item.type == "thread") {
return null;
}
const { rootItems } = this.props;
// This is the second magic which skip empty folders
// (See getChildren comment)
function skipEmptyDirectories(directory) {
if (
directory.type == "group" ||
directory.type == "thread" ||
rootItems.includes(directory)
) {
return directory;
}
if (
directory.children.length == 1 &&
directory.children[0].type == "directory"
) {
return skipEmptyDirectories(directory.parent);
}
return directory;
}
return skipEmptyDirectories(item.parent);
};
renderProjectRootHeader() {
const { projectRootName } = this.props;
if (!projectRootName) {
return null;
}
return div(
{
key: "root",
className: "sources-clear-root-container",
},
button(
{
className: "sources-clear-root",
onClick: () => this.props.clearProjectDirectoryRoot(),
title: L10N.getStr("removeDirectoryRoot.label"),
},
React.createElement(AccessibleImage, {
className: "home",
}),
React.createElement(AccessibleImage, {
className: "breadcrumb",
}),
span(
{
className: "sources-clear-root-label",
},
projectRootName
)
)
);
}
renderItem = (item, depth, focused, arrow, expanded) => {
const { mainThreadHost } = this.props;
return React.createElement(SourcesTreeItem, {
arrow,
item,
depth,
focused,
autoExpand: shouldAutoExpand(item, mainThreadHost),
expanded,
focusItem: this.onFocus,
selectSourceItem: this.selectSourceItem,
setExpanded: this.setExpanded,
getParent: this.getParent,
});
};
renderTree() {
const { expanded, focused } = this.props;
const treeProps = {
autoExpandAll: false,
autoExpandDepth: 1,
expanded,
focused,
getChildren: this.getChildren,
getParent: this.getParent,
getKey: this.getKey,
getRoots: this.getRoots,
onCollapse: this.onCollapse,
onExpand: this.onExpand,
onFocus: this.onFocus,
isExpanded: item => {
return this.props.expanded.has(this.getKey(item));
},
onActivate: this.onActivate,
renderItem: this.renderItem,
preventBlur: true,
};
return React.createElement(Tree, treeProps);
}
renderPane(child) {
const { projectRoot } = this.props;
return div(
{
key: "pane",
className: classnames("sources-pane", {
"sources-list-custom-root": !!projectRoot,
}),
},
child
);
}
renderFooter() {
if (this.props.hideIgnoredSources) {
return footer(
{
className: "source-list-footer",
},
L10N.getStr("ignoredSourcesHidden"),
button(
{
className: "devtools-togglebutton",
onClick: () => this.props.setHideOrShowIgnoredSources(false),
title: L10N.getStr("showIgnoredSources.tooltip.label"),
},
L10N.getStr("showIgnoredSources")
)
);
}
return null;
}
renderSettingsButton() {
const { toolboxDoc } = this.context;
return React.createElement(
MenuButton,
{
menuId: "sources-tree-settings-menu-button",
toolboxDoc,
className:
"devtools-button command-bar-button debugger-settings-menu-button",
title: L10N.getStr("sources-settings.button.label"),
"aria-label": L10N.getStr("sources-settings.button.label"),
},
() => this.renderSettingsMenuItems()
);
}
renderSettingsMenuItems() {
return React.createElement(
MenuList,
{
id: "sources-tree-settings-menu-list",
},
React.createElement(MenuItem, {
key: "debugger-settings-menu-item-hide-ignored-sources",
className: "menu-item debugger-settings-menu-item-hide-ignored-sources",
checked: prefs.hideIgnoredSources,
label: L10N.getStr("settings.hideIgnoredSources.label"),
tooltip: L10N.getStr("settings.hideIgnoredSources.tooltip"),
onClick: () =>
this.props.setHideOrShowIgnoredSources(!prefs.hideIgnoredSources),
}),
React.createElement(MenuItem, {
key: "debugger-settings-menu-item-show-content-scripts",
className: "menu-item debugger-settings-menu-item-show-content-scripts",
checked: prefs.showContentScripts,
label: L10N.getStr("sources-settings.showContentScripts.label"),
tooltip: L10N.getStr("sources-settings.showContentScripts.tooltip"),
onClick: () =>
this.props.setShowContentScripts(!prefs.showContentScripts),
})
);
}
render() {
const { projectRoot } = this.props;
return div(
{
key: "pane",
className: classnames("sources-list", {
"sources-list-custom-root": !!projectRoot,
}),
},
this.renderSettingsButton(),
this.isEmpty()
? this.renderEmptyElement(L10N.getStr("noSourcesText"))
: React.createElement(
Fragment,
null,
this.renderProjectRootHeader(),
this.renderTree(),
this.renderFooter()
)
);
}
}
SourcesTree.contextTypes = {
toolboxDoc: PropTypes.object,
};
const mapStateToProps = state => {
return {
mainThreadHost: getMainThreadHost(state),
expanded: getExpandedState(state),
focused: getFocusedSourceItem(state),
projectRoot: getProjectDirectoryRoot(state),
rootItems: getSourcesTreeSources(state),
projectRootName: getProjectDirectoryRootName(state),
hideIgnoredSources: getHideIgnoredSources(state),
};
};
export default connect(mapStateToProps, {
selectSource: actions.selectSource,
setExpandedState: actions.setExpandedState,
focusItem: actions.focusItem,
clearProjectDirectoryRoot: actions.clearProjectDirectoryRoot,
setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources,
setShowContentScripts: actions.setShowContentScripts,
})(SourcesTree);