Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
15 kB · 536 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537import * 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<HTMLDivElement | null>(null); const miniScrollRef = React.useRef<HTMLDivElement | null>(null); const tabStripInnerRef = React.useRef<HTMLDivElement | null>(null); const contentRef = React.useRef<HTMLDivElement | null>(null); const buttonBarRef = React.useRef<HTMLDivElement | null>(null); const overflowbuttonRef = React.useRef<HTMLButtonElement | null>(null); const stickyButtonsRef = React.useRef<HTMLDivElement | null>(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<HTMLElement, 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<HTMLElement>) => { 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<HTMLElement>) => { if (!isAuxMouseEvent(event)) { layout.doAction(Actions.setActiveTabset(node.getId(), layout.getWindowId())); } };
const onAuxMouseClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { if (isAuxMouseEvent(event)) { layout.auxMouseClick(node, event); } };
const onContextMenu = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { layout.showContextMenu(node, event); };
const onInterceptPointerDown = (event: React.PointerEvent) => { event.stopPropagation(); };
const onMaximizeToggle = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { if (node.canMaximize()) { layout.maximize(node); } event.stopPropagation(); };
const onClose = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { layout.doAction(Actions.deleteTabset(node.getId())); event.stopPropagation(); };
const onCloseTab = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { layout.doAction(Actions.deleteTab(node.getChildren()[0].getId())); event.stopPropagation(); };
const onPopoutTab = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { if (selectedTabNode !== undefined) { layout.doAction(Actions.popoutTab(selectedTabNode.getId())); // layout.doAction(Actions.popoutTabset(node.getId())); } event.stopPropagation(); };
const onDoubleClick = (event: React.MouseEvent<HTMLElement, 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( <TabButton layout={layout} node={child} path={`${path}/tb${i}`} key={child.getId()} selected={isSelected} />, ); if (i < node.getChildren().length - 1) { tabs.push( <div key={`divider${i}`} className={cm(CLASSES.FLEXLAYOUT__TABSET_TAB_DIVIDER)} />, ); } } }
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( <div ref={stickyButtonsRef} key="sticky_buttons_container" onPointerDown={onInterceptPointerDown} onDragStart={(e) => { e.preventDefault(); }} className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_STICKY_BUTTONS_CONTAINER)} > {stickyButtons} </div>, ); } }
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} <div className={cm(CLASSES.FLEXLAYOUT__TAB_BUTTON_OVERFLOW_COUNT)}> {hiddenTabs.length > 0 ? hiddenTabs.length : ""} </div> </> ); } buttons.splice( Math.min(renderState.overflowPosition, buttons.length), 0, <button key="overflowbutton" data-layout-path={`${path}/button/overflow`} ref={overflowbuttonRef} className={`${cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON)} ${cm( CLASSES.FLEXLAYOUT__TAB_BUTTON_OVERFLOW, )}`} title={overflowTitle} onClick={onOverflowClick} onPointerDown={onInterceptPointerDown} > {overflowContent} </button>, ); } }
if ( selectedTabNode !== undefined && layout.isSupportsPopout() && selectedTabNode.isEnablePopout() && selectedTabNode.isEnablePopoutIcon() ) { const popoutTitle = layout.i18nName(I18nLabel.Popout_Tab); buttons.push( <button key="popout" data-layout-path={`${path}/button/popout`} title={popoutTitle} className={`${cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON)} ${cm( CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON_FLOAT, )}`} onClick={onPopoutTab} onPointerDown={onInterceptPointerDown} > {typeof icons.popout === "function" ? icons.popout(selectedTabNode) : icons.popout} </button>, ); }
if (node.canMaximize()) { const minTitle = layout.i18nName(I18nLabel.Restore); const maxTitle = layout.i18nName(I18nLabel.Maximize); buttons.push( <button key="max" data-layout-path={`${path}/button/max`} title={node.isMaximized() ? minTitle : maxTitle} className={`${cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON)} ${cm( CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON_ + (node.isMaximized() ? "max" : "min"), )}`} onClick={onMaximizeToggle} onPointerDown={onInterceptPointerDown} > {node.isMaximized() ? typeof icons.restore === "function" ? icons.restore(node) : icons.restore : typeof icons.maximize === "function" ? icons.maximize(node) : icons.maximize} </button>, ); }
if (!node.isMaximized() && showClose) { const title = isTabStretch ? layout.i18nName(I18nLabel.Close_Tab) : layout.i18nName(I18nLabel.Close_Tabset); buttons.push( <button key="close" data-layout-path={`${path}/button/close`} title={title} className={`${cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON)} ${cm( CLASSES.FLEXLAYOUT__TAB_TOOLBAR_BUTTON_CLOSE, )}`} onClick={isTabStretch ? onCloseTab : onClose} onPointerDown={onInterceptPointerDown} > {typeof icons.closeTabset === "function" ? icons.closeTabset(node) : icons.closeTabset} </button>, ); }
if (node.isActive() && node.isEnableActiveIcon()) { const title = layout.i18nName(I18nLabel.Active_Tabset); buttons.push( <div key="active" data-layout-path={`${path}/button/active`} title={title} className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_ICON)} > {typeof icons.activeTabset === "function" ? icons.activeTabset(node) : icons.activeTabset} </div>, ); }
const buttonbar = ( <div key="buttonbar" ref={buttonBarRef} className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR)} onPointerDown={onInterceptPointerDown} onDragStart={(e) => { e.preventDefault(); }} > {buttons} </div> );
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 = ( <div className={tabStripClasses} style={{ flexWrap: "wrap", gap: "1px", marginTop: "2px" }} ref={tabStripRef} data-layout-path={`${path}/tabstrip`} onPointerDown={onPointerDown} onDoubleClick={onDoubleClick} onContextMenu={onContextMenu} onClick={onAuxMouseClick} onAuxClick={onAuxMouseClick} draggable={true} onDragStart={onDragStart} > {tabs} <div style={{ flexGrow: 1 }} /> {buttonbar} </div> ); } } else { if (node.isEnableTabStrip()) { let miniScrollbar: any; if (node.isEnableTabScrollbar()) { miniScrollbar = ( <div ref={miniScrollRef} className={cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR)} onPointerDown={onScrollPointerDown} /> ); } tabStrip = ( <div className={tabStripClasses} ref={tabStripRef} data-layout-path={`${path}/tabstrip`} onPointerDown={onPointerDown} onDoubleClick={onDoubleClick} onContextMenu={onContextMenu} onClick={onAuxMouseClick} onAuxClick={onAuxMouseClick} draggable={true} onWheel={onMouseWheel} onDragStart={onDragStart} > <div className={cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR_CONTAINER)}> <div ref={tabStripInnerRef} className={`${cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER)} ${cm( CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_ + node.getTabLocation(), )}`} style={{ overflowX: "auto", overflowY: "hidden" }} onScroll={onScroll} > <div style={{ width: isTabStretch ? "100%" : "none" }} className={`${cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER)} ${cm( CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER_ + node.getTabLocation(), )}`} > {tabs} </div> </div> {miniScrollbar} </div> {buttonbar} </div> ); } }
let emptyTabset: React.ReactNode; if (node.getChildren().length === 0) { const placeHolderCallback = layout.getTabSetPlaceHolderCallback(); if (placeHolderCallback) { emptyTabset = placeHolderCallback(node); } }
let content = ( <div ref={contentRef} className={cm(CLASSES.FLEXLAYOUT__TABSET_CONTENT)}> {emptyTabset} </div> );
if (node.getTabLocation() === "top") { content = ( <> {tabStrip} {content} </> ); } else { content = ( <> {content} {tabStrip} </> ); }
const style: Record<string, any> = { 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 = ( <div ref={selfRef} className={cm(CLASSES.FLEXLAYOUT__TABSET_CONTAINER)} style={style}> <div className={cm(CLASSES.FLEXLAYOUT__TABSET)} data-layout-path={path}> {content} </div> </div> );
if (node.isMaximized()) { if (layout.getMainElement()) { return createPortal( <div style={{ position: "absolute", display: "flex", top: 0, left: 0, bottom: 0, right: 0, }} > {tabset} </div>, layout.getMainElement()!, ); } return tabset; } return tabset;};