import * as React from "react"; import { createPortal } from "react-dom"; import { I18nLabel } from "../I18nLabel"; import { Actions } from "../model/Actions"; import type { TabNode } from "../model/TabNode"; import type { TabSetNode } from "../model/TabSetNode"; import { Orientation } from "../Orientation"; import { Rect } from "../Rect"; import { CLASSES } from "../Types"; import type { LayoutInternal, ITabSetRenderValues } from "./Layout"; import { showPopup } from "./PopupMenu"; import { TabButton } from "./TabButton"; import { useTabOverflow } from "./TabOverflowHook"; import { isAuxMouseEvent } from "./Utils"; /** @internal */ export interface ITabSetProps { layout: LayoutInternal; node: TabSetNode; } /** @internal */ export const TabSet = (props: ITabSetProps) => { const { node, layout } = props; const tabStripRef = React.useRef(null); const miniScrollRef = React.useRef(null); const tabStripInnerRef = React.useRef(null); const contentRef = React.useRef(null); const buttonBarRef = React.useRef(null); const overflowbuttonRef = React.useRef(null); const stickyButtonsRef = React.useRef(null); const icons = layout.getIcons(); // must use useEffect (rather than useLayoutEffect) otherwise contentrect not set correctly (has height 0 when changing theme in demo) React.useEffect(() => { node.setRect(layout.getBoundingClientRect(selfRef.current!)); if (tabStripRef.current) { node.setTabStripRect(layout.getBoundingClientRect(tabStripRef.current!)); } const newContentRect = Rect.getContentRect(contentRef.current!).relativeTo( layout.getDomRect()!, ); if (!node.getContentRect().equals(newContentRect)) { node.setContentRect(newContentRect); setTimeout(() => { //prevent Maximum update depth exceeded error layout.redrawInternal(`tabset content rect ${newContentRect}`); }, 0); } }); // this must be after the useEffect, so the node rect is already set (else window popin will not position tabs correctly) const { selfRef, userControlledPositionRef, onScroll, onScrollPointerDown, hiddenTabs, onMouseWheel, isDockStickyButtons, isShowHiddenTabs, } = useTabOverflow( layout, node, Orientation.HORZ, tabStripInnerRef, miniScrollRef, layout.getClassName(CLASSES.FLEXLAYOUT__TAB_BUTTON), ); const onOverflowClick = (event: React.MouseEvent) => { const callback = layout.getShowOverflowMenu(); const items = hiddenTabs.map((h) => { return { index: h, node: node.getChildren()[h] as TabNode }; }); if (callback !== undefined) { callback(node, event, items, onOverflowItemSelect); } else { const element = overflowbuttonRef.current!; showPopup(element, node, items, onOverflowItemSelect, layout); } event.stopPropagation(); }; const onOverflowItemSelect = (item: { node: TabNode; index: number }) => { layout.doAction(Actions.selectTab(item.node.getId())); userControlledPositionRef.current = false; }; const onDragStart = (event: React.DragEvent) => { if (!layout.getEditingTab()) { if (node.isEnableDrag()) { event.stopPropagation(); layout.setDragNode(event.nativeEvent, node as TabSetNode); } else { event.preventDefault(); } } else { event.preventDefault(); } }; const onPointerDown = (event: React.PointerEvent) => { if (!isAuxMouseEvent(event)) { layout.doAction(Actions.setActiveTabset(node.getId(), layout.getWindowId())); } }; const onAuxMouseClick = (event: React.MouseEvent) => { if (isAuxMouseEvent(event)) { layout.auxMouseClick(node, event); } }; const onContextMenu = (event: React.MouseEvent) => { layout.showContextMenu(node, event); }; const onInterceptPointerDown = (event: React.PointerEvent) => { event.stopPropagation(); }; const onMaximizeToggle = (event: React.MouseEvent) => { if (node.canMaximize()) { layout.maximize(node); } event.stopPropagation(); }; const onClose = (event: React.MouseEvent) => { layout.doAction(Actions.deleteTabset(node.getId())); event.stopPropagation(); }; const onCloseTab = (event: React.MouseEvent) => { layout.doAction(Actions.deleteTab(node.getChildren()[0].getId())); event.stopPropagation(); }; const onPopoutTab = (event: React.MouseEvent) => { if (selectedTabNode !== undefined) { layout.doAction(Actions.popoutTab(selectedTabNode.getId())); // layout.doAction(Actions.popoutTabset(node.getId())); } event.stopPropagation(); }; const onDoubleClick = (event: React.MouseEvent) => { if (node.canMaximize()) { layout.maximize(node); } }; // Start Render const cm = layout.getClassName; const selectedTabNode: TabNode = node.getSelectedNode() as TabNode; const path = node.getPath(); const tabs = []; if (node.isEnableTabStrip()) { for (let i = 0; i < node.getChildren().length; i++) { const child = node.getChildren()[i] as TabNode; const isSelected = node.getSelected() === i; tabs.push( , ); if (i < node.getChildren().length - 1) { tabs.push(
, ); } } } let stickyButtons: React.ReactNode[] = []; let buttons: React.ReactNode[] = []; // allow customization of header contents and buttons const renderState: ITabSetRenderValues = { stickyButtons, buttons, overflowPosition: undefined, }; layout.customizeTabSet(node, renderState); stickyButtons = renderState.stickyButtons; buttons = renderState.buttons; const isTabStretch = node.isEnableSingleTabStretch() && node.getChildren().length === 1; const showClose = (isTabStretch && (node.getChildren()[0] as TabNode).isEnableClose()) || node.isEnableClose(); if (renderState.overflowPosition === undefined) { renderState.overflowPosition = stickyButtons.length; } if (stickyButtons.length > 0) { if (!node.isEnableTabWrap() && (isDockStickyButtons || isTabStretch)) { buttons = [...stickyButtons, ...buttons]; } else { tabs.push(
{ e.preventDefault(); }} className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_STICKY_BUTTONS_CONTAINER)} > {stickyButtons}
, ); } } if (!node.isEnableTabWrap()) { if (isShowHiddenTabs) { const overflowTitle = layout.i18nName(I18nLabel.Overflow_Menu_Tooltip); let overflowContent: React.ReactNode; if (typeof icons.more === "function") { const items = hiddenTabs.map((h) => { return { index: h, node: node.getChildren()[h] as TabNode }; }); overflowContent = icons.more(node, items); } else { overflowContent = ( <> {icons.more}
{hiddenTabs.length > 0 ? hiddenTabs.length : ""}
); } buttons.splice( Math.min(renderState.overflowPosition, buttons.length), 0, , ); } } if ( selectedTabNode !== undefined && layout.isSupportsPopout() && selectedTabNode.isEnablePopout() && selectedTabNode.isEnablePopoutIcon() ) { const popoutTitle = layout.i18nName(I18nLabel.Popout_Tab); buttons.push( , ); } if (node.canMaximize()) { const minTitle = layout.i18nName(I18nLabel.Restore); const maxTitle = layout.i18nName(I18nLabel.Maximize); buttons.push( , ); } if (!node.isMaximized() && showClose) { const title = isTabStretch ? layout.i18nName(I18nLabel.Close_Tab) : layout.i18nName(I18nLabel.Close_Tabset); buttons.push( , ); } if (node.isActive() && node.isEnableActiveIcon()) { const title = layout.i18nName(I18nLabel.Active_Tabset); buttons.push(
{typeof icons.activeTabset === "function" ? icons.activeTabset(node) : icons.activeTabset}
, ); } const buttonbar = (
{ e.preventDefault(); }} > {buttons}
); let tabStrip: React.ReactNode; let tabStripClasses = cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_OUTER); if (node.getClassNameTabStrip() !== undefined) { tabStripClasses += ` ${node.getClassNameTabStrip()}`; } tabStripClasses += ` ${CLASSES.FLEXLAYOUT__TABSET_TABBAR_OUTER_}${node.getTabLocation()}`; if (node.isActive()) { tabStripClasses += ` ${cm(CLASSES.FLEXLAYOUT__TABSET_SELECTED)}`; } if (node.isMaximized()) { tabStripClasses += ` ${cm(CLASSES.FLEXLAYOUT__TABSET_MAXIMIZED)}`; } if (isTabStretch) { const tabNode = node.getChildren()[0] as TabNode; if (tabNode.getTabSetClassName() !== undefined) { tabStripClasses += ` ${tabNode.getTabSetClassName()}`; } } if (node.isEnableTabWrap()) { if (node.isEnableTabStrip()) { tabStrip = (
{tabs}
{buttonbar}
); } } else { if (node.isEnableTabStrip()) { let miniScrollbar: any; if (node.isEnableTabScrollbar()) { miniScrollbar = (
); } tabStrip = (
{tabs}
{miniScrollbar}
{buttonbar}
); } } let emptyTabset: React.ReactNode; if (node.getChildren().length === 0) { const placeHolderCallback = layout.getTabSetPlaceHolderCallback(); if (placeHolderCallback) { emptyTabset = placeHolderCallback(node); } } let content = (
{emptyTabset}
); if (node.getTabLocation() === "top") { content = ( <> {tabStrip} {content} ); } else { content = ( <> {content} {tabStrip} ); } const style: Record = { flexGrow: Math.max(1, node.getWeight() * 1000), minWidth: node.getMinWidth(), minHeight: node.getMinHeight(), maxWidth: node.getMaxWidth(), maxHeight: node.getMaxHeight(), }; if ( node.getModel().getMaximizedTabset(layout.getWindowId()) !== undefined && !node.isMaximized() ) { style.display = "none"; } // note: tabset container is needed to allow flexbox to size without border/padding/margin // then inner tabset can have border/padding/margin for styling const tabset = (
{content}
); if (node.isMaximized()) { if (layout.getMainElement()) { return createPortal(
{tabset}
, layout.getMainElement()!, ); } return tabset; } return tabset; };