/* 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 . */ import React, { PureComponent } from "react"; import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import { connect } from "../../utils/connect"; import { getSourceTabs, getSelectedSource, getSourcesForTabs, getIsPaused, getCurrentThread, getContext, getBlackBoxRanges, } from "../../selectors"; import { isVisible } from "../../utils/ui"; import { getHiddenTabs } from "../../utils/tabs"; import { getFilename, isPretty, getFileURL } from "../../utils/source"; import actions from "../../actions"; import "./Tabs.css"; import Tab from "./Tab"; import { PaneToggleButton } from "../shared/Button"; import Dropdown from "../shared/Dropdown"; import AccessibleImage from "../shared/AccessibleImage"; import CommandBar from "../SecondaryPanes/CommandBar"; const { debounce } = require("devtools/shared/debounce"); function haveTabSourcesChanged(tabSources, prevTabSources) { if (tabSources.length !== prevTabSources.length) { return true; } for (let i = 0; i < tabSources.length; ++i) { if (tabSources[i].id !== prevTabSources[i].id) { return true; } } return false; } class Tabs extends PureComponent { constructor(props) { super(props); this.state = { dropdownShown: false, hiddenTabs: [], }; this.onResize = debounce(() => { this.updateHiddenTabs(); }); } static get propTypes() { return { cx: PropTypes.object.isRequired, endPanelCollapsed: PropTypes.bool.isRequired, horizontal: PropTypes.bool.isRequired, isPaused: PropTypes.bool.isRequired, moveTab: PropTypes.func.isRequired, moveTabBySourceId: PropTypes.func.isRequired, selectSource: PropTypes.func.isRequired, selectedSource: PropTypes.object, blackBoxRanges: PropTypes.object.isRequired, startPanelCollapsed: PropTypes.bool.isRequired, tabSources: PropTypes.array.isRequired, tabs: PropTypes.array.isRequired, togglePaneCollapse: PropTypes.func.isRequired, }; } get draggedSource() { return this._draggedSource == null ? { url: null, id: null } : this._draggedSource; } set draggedSource(source) { this._draggedSource = source; } get draggedSourceIndex() { return this._draggedSourceIndex == null ? -1 : this._draggedSourceIndex; } set draggedSourceIndex(index) { this._draggedSourceIndex = index; } componentDidUpdate(prevProps) { if ( this.props.selectedSource !== prevProps.selectedSource || haveTabSourcesChanged(this.props.tabSources, prevProps.tabSources) ) { this.updateHiddenTabs(); } } componentDidMount() { window.requestIdleCallback(this.updateHiddenTabs); window.addEventListener("resize", this.onResize); window.document .querySelector(".editor-pane") .addEventListener("resizeend", this.onResize); } componentWillUnmount() { window.removeEventListener("resize", this.onResize); window.document .querySelector(".editor-pane") .removeEventListener("resizeend", this.onResize); } /* * Updates the hiddenSourceTabs state, by * finding the source tabs which are wrapped and are not on the top row. */ updateHiddenTabs = () => { if (!this.refs.sourceTabs) { return; } const { selectedSource, tabSources, moveTab } = this.props; const sourceTabEls = this.refs.sourceTabs.children; const hiddenTabs = getHiddenTabs(tabSources, sourceTabEls); if ( selectedSource && isVisible() && hiddenTabs.find(tab => tab.id == selectedSource.id) ) { moveTab(selectedSource.url, 0); return; } this.setState({ hiddenTabs }); }; toggleSourcesDropdown() { this.setState(prevState => ({ dropdownShown: !prevState.dropdownShown, })); } getIconClass(source) { if (isPretty(source)) { return "prettyPrint"; } if (this.props.blackBoxRanges[source.url]) { return "blackBox"; } return "file"; } renderDropdownSource = source => { const { cx, selectSource } = this.props; const filename = getFilename(source); const onClick = () => selectSource(cx, source); return (
  • {filename}
  • ); }; onTabDragStart = (source, index) => { this.draggedSource = source; this.draggedSourceIndex = index; }; onTabDragEnd = () => { this.draggedSource = null; this.draggedSourceIndex = null; }; onTabDragOver = (e, source, hoveredTabIndex) => { const { moveTabBySourceId } = this.props; if (hoveredTabIndex === this.draggedSourceIndex) { return; } const tabDOM = ReactDOM.findDOMNode( this.refs[`tab_${source.id}`].getWrappedInstance() ); const tabDOMRect = tabDOM.getBoundingClientRect(); const { pageX: mouseCursorX } = e; if ( /* Case: the mouse cursor moves into the left half of any target tab */ mouseCursorX - tabDOMRect.left < tabDOMRect.width / 2 ) { // The current tab goes to the left of the target tab const targetTab = hoveredTabIndex > this.draggedSourceIndex ? hoveredTabIndex - 1 : hoveredTabIndex; moveTabBySourceId(this.draggedSource.id, targetTab); this.draggedSourceIndex = targetTab; } else if ( /* Case: the mouse cursor moves into the right half of any target tab */ mouseCursorX - tabDOMRect.left >= tabDOMRect.width / 2 ) { // The current tab goes to the right of the target tab const targetTab = hoveredTabIndex < this.draggedSourceIndex ? hoveredTabIndex + 1 : hoveredTabIndex; moveTabBySourceId(this.draggedSource.id, targetTab); this.draggedSourceIndex = targetTab; } }; renderTabs() { const { tabs } = this.props; if (!tabs) { return null; } return (
    {tabs.map(({ source, sourceActor }, index) => { return ( this.onTabDragStart(source, index)} onDragOver={e => { this.onTabDragOver(e, source, index); e.preventDefault(); }} onDragEnd={this.onTabDragEnd} key={index} source={source} sourceActor={sourceActor} ref={`tab_${source.id}`} /> ); })}
    ); } renderDropdown() { const { hiddenTabs } = this.state; if (!hiddenTabs || !hiddenTabs.length) { return null; } const Panel = ; const icon = ; return ; } renderCommandBar() { const { horizontal, endPanelCollapsed, isPaused } = this.props; if (!endPanelCollapsed || !isPaused) { return null; } return ; } renderStartPanelToggleButton() { return ( ); } renderEndPanelToggleButton() { const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props; if (!horizontal) { return null; } return ( ); } render() { return (
    {this.renderStartPanelToggleButton()} {this.renderTabs()} {this.renderDropdown()} {this.renderEndPanelToggleButton()} {this.renderCommandBar()}
    ); } } const mapStateToProps = state => { return { cx: getContext(state), selectedSource: getSelectedSource(state), tabSources: getSourcesForTabs(state), tabs: getSourceTabs(state), blackBoxRanges: getBlackBoxRanges(state), isPaused: getIsPaused(state, getCurrentThread(state)), }; }; export default connect(mapStateToProps, { selectSource: actions.selectSource, moveTab: actions.moveTab, moveTabBySourceId: actions.moveTabBySourceId, closeTab: actions.closeTab, togglePaneCollapse: actions.togglePaneCollapse, showSource: actions.showSource, })(Tabs);