mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-14 13:46:55 +03:00
727d753081
* stash * and those * stash * stash * stash * stash * tests * fix errors * revert * stash * fix lint * prettier
249 lines
6.9 KiB
TypeScript
249 lines
6.9 KiB
TypeScript
import React, { Component } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import observeResize from 'simple-element-resize-detector';
|
|
import { FaAngleDoubleRight } from 'react-icons/fa';
|
|
import ContextMenu from '../ContextMenu';
|
|
import createStyledComponent from '../utils/createStyledComponent';
|
|
import * as styles from './styles';
|
|
|
|
const TabsWrapper = createStyledComponent(styles);
|
|
|
|
export type ReactButtonElement = React.ReactElement<
|
|
JSX.IntrinsicElements['button'],
|
|
'button'
|
|
>;
|
|
|
|
export interface Tab<P> {
|
|
name: string;
|
|
value?: string;
|
|
component?: React.ComponentType<P>;
|
|
selector?: (tab: this) => P;
|
|
}
|
|
|
|
interface Props<P> {
|
|
tabs: ReactButtonElement[];
|
|
items: Tab<P>[];
|
|
main: boolean | undefined;
|
|
onClick: (value: string) => void;
|
|
position: 'left' | 'right' | 'center';
|
|
collapsible: boolean | undefined;
|
|
selected: string | undefined;
|
|
}
|
|
|
|
interface State {
|
|
visibleTabs: ReactButtonElement[];
|
|
hiddenTabs: ReactButtonElement[];
|
|
subMenuOpened: boolean;
|
|
contextMenu: { top: number; left: number } | undefined;
|
|
}
|
|
|
|
export default class TabsHeader<P> extends Component<Props<P>, State> {
|
|
state: State = {
|
|
visibleTabs: this.props.tabs.slice(),
|
|
hiddenTabs: [],
|
|
subMenuOpened: false,
|
|
contextMenu: undefined,
|
|
};
|
|
|
|
iconWidth = 0;
|
|
hiddenTabsWidth: number[] = [];
|
|
tabsWrapperRef?: HTMLDivElement | null;
|
|
tabsRef?: HTMLDivElement | null;
|
|
resizeDetector?: HTMLIFrameElement;
|
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props<P>) {
|
|
if (
|
|
nextProps.tabs !== this.props.tabs ||
|
|
nextProps.selected !== this.props.selected ||
|
|
nextProps.collapsible !== this.props.collapsible
|
|
) {
|
|
this.setState({ hiddenTabs: [], visibleTabs: nextProps.tabs.slice() });
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.collapsible) {
|
|
this.collapse();
|
|
this.enableResizeEvents();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props<P>) {
|
|
const { collapsible } = this.props;
|
|
if (!collapsible) {
|
|
if (prevProps.collapsible !== collapsible) this.disableResizeEvents();
|
|
return;
|
|
}
|
|
|
|
let shouldCollapse = false;
|
|
if (this.iconWidth === 0) {
|
|
const tabButtons = this.tabsRef!.children;
|
|
if (
|
|
(this.tabsRef!.children[tabButtons.length - 1] as HTMLButtonElement)
|
|
.value === 'expandIcon'
|
|
) {
|
|
this.iconWidth = tabButtons[
|
|
tabButtons.length - 1
|
|
].getBoundingClientRect().width;
|
|
shouldCollapse = true;
|
|
}
|
|
} else if (this.state.hiddenTabs.length === 0) {
|
|
this.iconWidth = 0;
|
|
}
|
|
|
|
if (prevProps.collapsible !== collapsible) {
|
|
this.enableResizeEvents();
|
|
shouldCollapse = true;
|
|
}
|
|
|
|
if (shouldCollapse || this.props.selected !== prevProps.selected) {
|
|
this.collapse();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.props.collapsible) {
|
|
this.disableResizeEvents();
|
|
}
|
|
}
|
|
|
|
enableResizeEvents() {
|
|
this.resizeDetector = observeResize(this.tabsWrapperRef!, this.collapse);
|
|
window.addEventListener('mousedown', this.hideSubmenu);
|
|
}
|
|
|
|
disableResizeEvents() {
|
|
this.resizeDetector!.remove();
|
|
window.removeEventListener('mousedown', this.hideSubmenu);
|
|
}
|
|
|
|
collapse = () => {
|
|
if (this.state.subMenuOpened) this.hideSubmenu();
|
|
|
|
const { selected, tabs } = this.props;
|
|
const tabsWrapperRef = this.tabsWrapperRef;
|
|
const tabsRef = this.tabsRef;
|
|
const tabButtons = this.tabsRef!.children;
|
|
const visibleTabs = this.state.visibleTabs;
|
|
const hiddenTabs = this.state.hiddenTabs;
|
|
let tabsWrapperRight = tabsWrapperRef!.getBoundingClientRect().right;
|
|
if (!tabsWrapperRight) return; // tabs are hidden
|
|
|
|
const tabsRefRight = tabsRef!.getBoundingClientRect().right;
|
|
let i = visibleTabs.length - 1;
|
|
let hiddenTab;
|
|
|
|
if (tabsRefRight >= tabsWrapperRight - this.iconWidth) {
|
|
if (
|
|
this.props.position === 'right' &&
|
|
hiddenTabs.length > 0 &&
|
|
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
|
|
tabsWrapperRef!.getBoundingClientRect().width
|
|
) {
|
|
while (
|
|
i < tabs.length - 1 &&
|
|
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
|
|
tabsWrapperRef!.getBoundingClientRect().width
|
|
) {
|
|
hiddenTab = hiddenTabs.shift();
|
|
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
|
|
i++;
|
|
}
|
|
} else {
|
|
while (
|
|
i > 0 &&
|
|
tabButtons[i] &&
|
|
tabButtons[i].getBoundingClientRect().right >=
|
|
tabsWrapperRight - this.iconWidth
|
|
) {
|
|
if ((tabButtons[i] as HTMLButtonElement).value !== selected) {
|
|
hiddenTabs.unshift(...visibleTabs.splice(i, 1));
|
|
this.hiddenTabsWidth.unshift(
|
|
tabButtons[i].getBoundingClientRect().width
|
|
);
|
|
} else {
|
|
tabsWrapperRight -= tabButtons[i].getBoundingClientRect().width;
|
|
}
|
|
i--;
|
|
}
|
|
}
|
|
} else {
|
|
while (
|
|
i < tabs.length - 1 &&
|
|
tabButtons[i] &&
|
|
tabButtons[i].getBoundingClientRect().right + this.hiddenTabsWidth[0] <
|
|
tabsWrapperRight - this.iconWidth
|
|
) {
|
|
hiddenTab = hiddenTabs.shift();
|
|
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
|
|
this.hiddenTabsWidth.shift();
|
|
i++;
|
|
}
|
|
}
|
|
this.setState({ visibleTabs, hiddenTabs });
|
|
};
|
|
|
|
hideSubmenu = () => {
|
|
this.setState({ subMenuOpened: false, contextMenu: undefined });
|
|
};
|
|
|
|
getTabsWrapperRef: React.RefCallback<HTMLDivElement> = (node) => {
|
|
this.tabsWrapperRef = node;
|
|
};
|
|
|
|
getTabsRef: React.RefCallback<HTMLDivElement> = (node) => {
|
|
this.tabsRef = node;
|
|
};
|
|
|
|
expandMenu: React.MouseEventHandler = (e) => {
|
|
const rect = e.currentTarget.children[0].getBoundingClientRect();
|
|
this.setState({
|
|
contextMenu: {
|
|
top: rect.top + 10,
|
|
left: rect.left,
|
|
},
|
|
subMenuOpened: true,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
const { visibleTabs, hiddenTabs, contextMenu } = this.state;
|
|
return (
|
|
<TabsWrapper
|
|
ref={this.getTabsWrapperRef}
|
|
main={this.props.main}
|
|
position={this.props.position}
|
|
>
|
|
<div ref={this.getTabsRef}>
|
|
{visibleTabs}
|
|
{this.props.collapsible &&
|
|
visibleTabs.length < this.props.items.length && (
|
|
<button onClick={this.expandMenu} value="expandIcon">
|
|
<FaAngleDoubleRight />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{this.props.collapsible && contextMenu && (
|
|
<ContextMenu
|
|
items={hiddenTabs}
|
|
onClick={this.props.onClick}
|
|
x={contextMenu.left}
|
|
y={contextMenu.top}
|
|
visible={this.state.subMenuOpened}
|
|
/>
|
|
)}
|
|
</TabsWrapper>
|
|
);
|
|
}
|
|
|
|
static propTypes = {
|
|
tabs: PropTypes.array.isRequired,
|
|
items: PropTypes.array.isRequired,
|
|
main: PropTypes.bool,
|
|
onClick: PropTypes.func,
|
|
position: PropTypes.string,
|
|
collapsible: PropTypes.bool,
|
|
selected: PropTypes.string,
|
|
};
|
|
}
|