/* 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 * file, You can obtain one at . */ // @flow // Dependencies import React, { Component } from "react"; import classnames from "classnames"; import { connect } from "../../utils/connect"; // Selectors import { getShownSource, getSelectedSource, getDebuggeeUrl, getExpandedState, getProjectDirectoryRoot, getDisplayedSources, getFocusedSourceItem, getContext } from "../../selectors"; import { getGeneratedSourceByURL } from "../../reducers/sources"; // Actions import actions from "../../actions"; // Components import SourcesTreeItem from "./SourcesTreeItem"; import ManagedTree from "../shared/ManagedTree"; // Utils import { createTree, getDirectories, isDirectory, getSourceFromNode, nodeHasChildren, updateTree } from "../../utils/sources-tree"; import { parse } from "../../utils/url"; import { getRawSourceURL } from "../../utils/source"; import type { TreeNode, TreeDirectory, ParentMap } from "../../utils/sources-tree/types"; import type { Source, Context, Thread } from "../../types"; import type { SourcesMapByThread, State as AppState } from "../../reducers/types"; import type { Item } from "../shared/ManagedTree"; type Props = { cx: Context, threads: Thread[], sources: SourcesMapByThread, sourceCount: number, shownSource?: Source, selectedSource?: Source, debuggeeUrl: string, projectRoot: string, expanded: Set, selectSource: typeof actions.selectSource, setExpandedState: typeof actions.setExpandedState, focusItem: typeof actions.focusItem, focused: TreeNode }; type State = { parentMap: ParentMap, sourceTree: TreeDirectory, uncollapsedTree: TreeDirectory, listItems?: any, highlightItems?: any }; type SetExpanded = (item: TreeNode, expanded: boolean, altKey: boolean) => void; function shouldAutoExpand(depth, item, debuggeeUrl) { if (depth !== 1) { return false; } const { host } = parse(debuggeeUrl); return item.name === host; } class SourcesTree extends Component { constructor(props: Props) { super(props); const { debuggeeUrl, sources, threads } = this.props; this.state = createTree({ debuggeeUrl, sources, threads }); } componentWillReceiveProps(nextProps: Props) { const { projectRoot, debuggeeUrl, sources, shownSource, selectedSource, threads } = this.props; const { uncollapsedTree, sourceTree } = this.state; if ( projectRoot != nextProps.projectRoot || debuggeeUrl != nextProps.debuggeeUrl || threads != nextProps.threads || nextProps.sourceCount === 0 ) { // early recreate tree because of changes // to project root, debuggee url or lack of sources return this.setState( createTree({ sources: nextProps.sources, debuggeeUrl: nextProps.debuggeeUrl, threads: nextProps.threads }) ); } if (nextProps.shownSource && nextProps.shownSource != shownSource) { const listItems = getDirectories(nextProps.shownSource, sourceTree); return this.setState({ listItems }); } if ( nextProps.selectedSource && nextProps.selectedSource != selectedSource ) { const highlightItems = getDirectories( nextProps.selectedSource, sourceTree ); this.setState({ highlightItems }); } // NOTE: do not run this every time a source is clicked, // only when a new source is added if (nextProps.sources != this.props.sources) { this.setState( updateTree({ newSources: nextProps.sources, threads: nextProps.threads, prevSources: sources, debuggeeUrl, uncollapsedTree, sourceTree }) ); } } selectItem = (item: TreeNode) => { if (item.type == "source" && !Array.isArray(item.contents)) { this.props.selectSource(this.props.cx, item.contents.id); } }; onFocus = (item: TreeNode) => { this.props.focusItem(item); }; onActivate = (item: TreeNode) => { this.selectItem(item); }; // NOTE: we get the source from sources because item.contents is cached getSource(item: TreeNode): ?Source { return getSourceFromNode(item); } getPath = (item: TreeNode): string => { const { path } = item; const source = this.getSource(item); if (!source || isDirectory(item)) { return path; } const blackBoxedPart = source.isBlackBoxed ? ":blackboxed" : ""; return `${path}/${source.id}/${blackBoxedPart}`; }; onExpand = (item: Item, expandedState: Set) => { this.props.setExpandedState(expandedState); }; onCollapse = (item: Item, expandedState: Set) => { this.props.setExpandedState(expandedState); }; isEmpty() { const { sourceTree } = this.state; return sourceTree.contents.length === 0; } renderEmptyElement(message) { return (
{message}
); } getRoots = () => { const { projectRoot } = this.props; const { sourceTree } = this.state; const sourceContents = sourceTree.contents[0]; const rootLabel = projectRoot.split("/").pop(); // The "sourceTree.contents[0]" check ensures that there are contents // A custom root with no existing sources will be ignored if (projectRoot && sourceContents) { if (sourceContents && sourceContents.name !== rootLabel) { return sourceContents.contents[0].contents; } return sourceContents.contents; } return sourceTree.contents; }; getChildren = (item: $Shape) => { return nodeHasChildren(item) ? item.contents : []; }; renderItem = ( item: TreeNode, depth: number, focused: boolean, _, expanded: boolean, { setExpanded }: { setExpanded: SetExpanded } ) => { const { debuggeeUrl, projectRoot, threads } = this.props; return ( ); }; renderTree() { const { expanded, focused } = this.props; const { highlightItems, listItems, parentMap } = this.state; const treeProps = { autoExpandAll: false, autoExpandDepth: 1, expanded, focused, getChildren: this.getChildren, getParent: (item: $Shape) => parentMap.get(item), getPath: this.getPath, getRoots: this.getRoots, highlightItems, itemHeight: 21, key: this.isEmpty() ? "empty" : "full", listItems, onCollapse: this.onCollapse, onExpand: this.onExpand, onFocus: this.onFocus, onActivate: this.onActivate, renderItem: this.renderItem, preventBlur: true }; return ; } renderPane(...children) { const { projectRoot } = this.props; return (
{children}
); } render() { return this.renderPane( this.isEmpty() ? ( this.renderEmptyElement(L10N.getStr("noSourcesText")) ) : (
{this.renderTree()}
) ); } } function getSourceForTree( state: AppState, displayedSources: SourcesMapByThread, source: ?Source ): ?Source { if (!source) { return null; } if (!source || !source.isPrettyPrinted) { return source; } return getGeneratedSourceByURL(state, getRawSourceURL(source.url)); } const mapStateToProps = (state, props) => { const selectedSource = getSelectedSource(state); const shownSource = getShownSource(state); const displayedSources = getDisplayedSources(state); return { threads: props.threads, cx: getContext(state), shownSource: getSourceForTree(state, displayedSources, shownSource), selectedSource: getSourceForTree(state, displayedSources, selectedSource), debuggeeUrl: getDebuggeeUrl(state), expanded: getExpandedState(state), focused: getFocusedSourceItem(state), projectRoot: getProjectDirectoryRoot(state), sources: displayedSources, sourceCount: Object.values(displayedSources).length }; }; export default connect( mapStateToProps, { selectSource: actions.selectSource, setExpandedState: actions.setExpandedState, focusItem: actions.focusItem } )(SourcesTree);