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
import React, { PureComponent } from "devtools/client/shared/vendor/react";
import {
div,
ul,
li,
span,
} 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";
import {
getSourceTabs,
getSelectedSource,
getSourcesForTabs,
getIsPaused,
getCurrentThread,
getBlackBoxRanges,
} from "../../selectors/index";
import { isVisible } from "../../utils/ui";
import { getHiddenTabs } from "../../utils/tabs";
import { isPretty, getFileURL } from "../../utils/source";
import actions from "../../actions/index";
import Tab from "./Tab";
import { PaneToggleButton } from "../shared/Button/index";
import Dropdown from "../shared/Dropdown";
import AccessibleImage from "../shared/AccessibleImage";
import CommandBar from "../SecondaryPanes/CommandBar";
const { debounce } = require("resource://devtools/shared/debounce.js");
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 {
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,
};
}
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 { selectSource } = this.props;
const onClick = () => selectSource(source);
return li(
{
key: source.id,
onClick,
title: getFileURL(source, false),
},
React.createElement(AccessibleImage, {
className: `dropdown-icon ${this.getIconClass(source)}`,
}),
span(
{
className: "dropdown-label",
},
source.shortName
)
);
};
// Note that these three listener will be called from Tab component
// so that e.target will be Tab's DOM (and not Tabs one).
onTabDragStart = e => {
this.draggedSourceId = e.target.dataset.sourceId;
this.draggedSourceIndex = e.target.dataset.index;
};
onTabDragEnd = () => {
this.draggedSourceId = null;
this.draggedSourceIndex = -1;
};
onTabDragOver = e => {
e.preventDefault();
const hoveredTabIndex = e.target.dataset.index;
const { moveTabBySourceId } = this.props;
if (hoveredTabIndex === this.draggedSourceIndex) {
return;
}
const tabDOMRect = e.target.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.draggedSourceId, 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.draggedSourceId, targetTab);
this.draggedSourceIndex = targetTab;
}
};
renderTabs() {
const { tabs } = this.props;
if (!tabs) {
return null;
}
return div(
{
className: "source-tabs",
ref: "sourceTabs",
},
tabs.map(({ source, sourceActor }, index) => {
return React.createElement(Tab, {
onDragStart: this.onTabDragStart,
onDragOver: this.onTabDragOver,
onDragEnd: this.onTabDragEnd,
key: source.id + sourceActor?.id,
index,
source,
sourceActor,
});
})
);
}
renderDropdown() {
const { hiddenTabs } = this.state;
if (!hiddenTabs || !hiddenTabs.length) {
return null;
}
const panel = ul(null, hiddenTabs.map(this.renderDropdownSource));
const icon = React.createElement(AccessibleImage, {
className: "more-tabs",
});
return React.createElement(Dropdown, {
panel,
icon,
});
}
renderCommandBar() {
const { horizontal, endPanelCollapsed, isPaused } = this.props;
if (!endPanelCollapsed || !isPaused) {
return null;
}
return React.createElement(CommandBar, {
horizontal,
});
}
renderStartPanelToggleButton() {
return React.createElement(PaneToggleButton, {
position: "start",
collapsed: this.props.startPanelCollapsed,
handleClick: this.props.togglePaneCollapse,
});
}
renderEndPanelToggleButton() {
const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props;
if (!horizontal) {
return null;
}
return React.createElement(PaneToggleButton, {
position: "end",
collapsed: endPanelCollapsed,
handleClick: togglePaneCollapse,
horizontal,
});
}
render() {
return div(
{
className: "source-header",
},
this.renderStartPanelToggleButton(),
this.renderTabs(),
this.renderDropdown(),
this.renderEndPanelToggleButton(),
this.renderCommandBar()
);
}
}
const mapStateToProps = state => {
return {
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);