Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
45 kB · 1488 lines
TSX
at develop
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489import * as React from "react";import { createPortal } from "react-dom";import { createRoot } from "react-dom/client";
import { DockLocation } from "../DockLocation";import type { DropInfo } from "../DropInfo";import { I18nLabel } from "../I18nLabel";import type { Action } from "../model/Action";import { Actions } from "../model/Actions";import { BorderNode } from "../model/BorderNode";import type { IDraggable } from "../model/IDraggable";import type { IJsonTabNode } from "../model/IJsonModel";import type { LayoutWindow } from "../model/LayoutWindow";import { Model } from "../model/Model";import type { Node } from "../model/Node";import { TabNode } from "../model/TabNode";import { TabSetNode } from "../model/TabSetNode";import { Orientation } from "../Orientation";import { Rect } from "../Rect";import { CLASSES } from "../Types";import { BorderTab } from "./BorderTab";import { BorderTabSet } from "./BorderTabSet";import { DragContainer } from "./DragContainer";import { AsterickIcon, CloseIcon, EdgeIcon, MaximizeIcon, OverflowIcon, PopoutIcon, RestoreIcon,} from "./Icons";import { Overlay } from "./Overlay";import { PopoutWindow } from "./PopoutWindow";import { Row } from "./Row";import { SizeTracker } from "./SizeTracker";import { Tab } from "./Tab";import { TabButtonStamp } from "./TabButtonStamp";import { copyInlineStyles, enablePointerOnIFrames, isDesktop, isSafari } from "./Utils";
export interface ILayoutProps { /** the model for this layout */ model: Model; /** factory function for creating the tab components */ factory: (node: TabNode) => React.ReactNode; /** sets a top level class name on popout windows */ popoutClassName?: string; /** object mapping keys among close, maximize, restore, more, popout to React nodes to use in place of the default icons, can alternatively return functions for creating the React nodes */ icons?: IIcons; /** function called whenever the layout generates an action to update the model (allows for intercepting actions before they are dispatched to the model, for example, asking the user to confirm a tab close.) Returning undefined from the function will halt the action, otherwise return the action to continue */ onAction?: (action: Action) => Action | undefined; /** function called when rendering a tab, allows leading (icon), content section, buttons and name used in overflow menu to be customized */ onRenderTab?: ( node: TabNode, renderValues: ITabRenderValues, // change the values in this object as required ) => void; /** function called when rendering a tabset, allows header and buttons to be customized */ onRenderTabSet?: ( tabSetNode: TabSetNode | BorderNode, renderValues: ITabSetRenderValues, // change the values in this object as required ) => void; /** function called when model has changed */ onModelChange?: (model: Model, action: Action) => void; /** function called when an external object (not a tab) gets dragged onto the layout, with a single dragenter argument. Should return either undefined to reject the drag/drop or an object with keys dragText, jsonDrop, to create a tab via drag (similar to a call to addTabToTabSet). Function onDropis passed the added tabNodeand thedrop DragEvent`, unless the drag was canceled. */ onExternalDrag?: (event: React.DragEvent<HTMLElement>) => | undefined | { json: any; onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void; }; /** function called with default css class name, return value is class name that will be used. Mainly for use with css modules. */ classNameMapper?: (defaultClassName: string) => string; /** function called for each I18nLabel to allow user translation, currently used for tab and tabset move messages, return undefined to use default values */ i18nMapper?: (id: I18nLabel, param?: string) => string | undefined; /** if left undefined will do simple check based on userAgent */ supportsPopout?: boolean | undefined; /** URL of popout window relative to origin, defaults to popout.html */ popoutURL?: string | undefined; /** boolean value, defaults to false, resize tabs as splitters are dragged. Warning: this can cause resizing to become choppy when tabs are slow to draw */ realtimeResize?: boolean | undefined; /** callback for rendering the drag rectangles */ onRenderDragRect?: DragRectRenderCallback; /** callback for handling context actions on tabs and tabsets */ onContextMenu?: NodeMouseEvent; /** callback for handling mouse clicks on tabs and tabsets with alt, meta, shift keys, also handles center mouse clicks */ onAuxMouseClick?: NodeMouseEvent; /** callback for handling the display of the tab overflow menu */ onShowOverflowMenu?: ShowOverflowMenuCallback; /** callback for rendering a placeholder when a tabset is empty */ onTabSetPlaceHolder?: TabSetPlaceHolderCallback; /** Name given to popout windows, defaults to 'Popout Window' */ popoutWindowName?: string;}
/** * A React component that hosts a multi-tabbed layout */export class Layout extends React.Component<ILayoutProps> { /** @internal */ private selfRef: React.RefObject<LayoutInternal | null>; /** @internal */ private revision: number; // so LayoutInternal knows this is a parent render (used for optimization)
/** @internal */ constructor(props: ILayoutProps) { super(props); this.selfRef = React.createRef<LayoutInternal>(); this.revision = 0; }
/** re-render the layout */ redraw() { this.selfRef.current!.redraw(`parent ${this.revision}`); }
/** * Adds a new tab to the given tabset * @param tabsetId the id of the tabset where the new tab will be added * @param json the json for the new tab node * @returns the added tab node or undefined */ addTabToTabSet(tabsetId: string, json: IJsonTabNode): TabNode | undefined { return this.selfRef.current!.addTabToTabSet(tabsetId, json); }
/** * Adds a new tab by dragging an item to the drop location, must be called from within an HTML * drag start handler. You can use the setDragComponent() method to set the drag image before calling this * method. * @param event the drag start event * @param json the json for the new tab node * @param onDrop a callback to call when the drag is complete */ addTabWithDragAndDrop( event: DragEvent, json: IJsonTabNode, onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void, ) { this.selfRef.current!.addTabWithDragAndDrop(event, json, onDrop); }
/** * Move a tab/tabset using drag and drop, must be called from within an HTML * drag start handler * @param event the drag start event * @param node the tab or tabset to drag */ moveTabWithDragAndDrop(event: DragEvent, node: TabNode | TabSetNode) { this.selfRef.current!.moveTabWithDragAndDrop(event, node); }
/** * Adds a new tab to the active tabset (if there is one) * @param json the json for the new tab node * @returns the added tab node or undefined */ addTabToActiveTabSet(json: IJsonTabNode): TabNode | undefined { return this.selfRef.current!.addTabToActiveTabSet(json); }
/** * Sets the drag image from a react component for a drag event * @param event the drag event * @param component the react component to be used for the drag image * @param x the x position of the drag cursor on the image * @param y the x position of the drag cursor on the image */ setDragComponent(event: DragEvent, component: React.ReactNode, x: number, y: number) { this.selfRef.current!.setDragComponent(event, component, x, y); }
/** Get the root div element of the layout */ getRootDiv() { return this.selfRef.current!.getRootDiv(); }
/** @internal */ render() { return <LayoutInternal ref={this.selfRef} {...this.props} renderRevision={this.revision++} />; }}
/** @internal */interface ILayoutInternalProps extends ILayoutProps { renderRevision: number;
// used only for popout windows: windowId?: string; mainLayout?: LayoutInternal;}
/** @internal */interface ILayoutInternalState { rect: Rect; editingTab?: TabNode; portal?: React.ReactPortal; showEdges: boolean; showOverlay: boolean; calculatedBorderBarSize: number; layoutRevision: number; forceRevision: number; showHiddenBorder: DockLocation;}
/** @internal */export class LayoutInternal extends React.Component<ILayoutInternalProps, ILayoutInternalState> { public static dragState: DragState | undefined = undefined;
private selfRef: React.RefObject<HTMLDivElement | null>; private moveablesRef: React.RefObject<HTMLDivElement | null>; private findBorderBarSizeRef: React.RefObject<HTMLDivElement | null>; private mainRef: React.RefObject<HTMLDivElement | null>; private previousModel?: Model; private orderedIds: string[]; private moveableElementMap = new Map<string, HTMLElement>(); private dropInfo: DropInfo | undefined; private outlineDiv?: HTMLElement; private currentDocument?: Document; private currentWindow?: Window; private supportsPopout: boolean; private popoutURL: string; private icons: IIcons; private resizeObserver?: ResizeObserver;
private dragEnterCount = 0; private dragging = false; private windowId: string; private layoutWindow: LayoutWindow; private mainLayout: LayoutInternal; private isMainWindow: boolean; private isDraggingOverWindow: boolean; private styleObserver: MutationObserver | undefined; private popoutWindowName: string; // private renderCount: any;
constructor(props: ILayoutInternalProps) { super(props);
this.orderedIds = []; this.selfRef = React.createRef<HTMLDivElement>(); this.moveablesRef = React.createRef<HTMLDivElement>(); this.mainRef = React.createRef<HTMLDivElement>(); this.findBorderBarSizeRef = React.createRef<HTMLDivElement>();
this.supportsPopout = props.supportsPopout !== undefined ? props.supportsPopout : defaultSupportsPopout; this.popoutURL = props.popoutURL ? props.popoutURL : "popout.html"; this.icons = { ...defaultIcons, ...props.icons }; this.windowId = props.windowId ? props.windowId : Model.MAIN_WINDOW_ID; this.mainLayout = this.props.mainLayout ? this.props.mainLayout : this; this.isDraggingOverWindow = false; this.layoutWindow = this.props.model.getwindowsMap().get(this.windowId)!; this.layoutWindow.layout = this; this.popoutWindowName = this.props.popoutWindowName || "Popout Window"; // this.renderCount = 0;
this.state = { rect: Rect.empty(), editingTab: undefined, showEdges: false, showOverlay: false, calculatedBorderBarSize: 29, layoutRevision: 0, forceRevision: 0, showHiddenBorder: DockLocation.CENTER, };
this.isMainWindow = this.windowId === Model.MAIN_WINDOW_ID; }
componentDidMount() { this.updateRect();
this.currentDocument = (this.selfRef.current as HTMLElement).ownerDocument; this.currentWindow = this.currentDocument.defaultView!;
this.layoutWindow.window = this.currentWindow; this.layoutWindow.toScreenRectFunction = (r) => this.getScreenRect(r);
this.resizeObserver = new ResizeObserver((entries) => { requestAnimationFrame(() => { this.updateRect(); }); }); if (this.selfRef.current) { this.resizeObserver.observe(this.selfRef.current); }
if (this.isMainWindow) { this.props.model.addChangeListener(this.onModelChange); this.updateLayoutMetrics(); } else { // since resizeObserver doesn't always work as expected when observing element in another document this.currentWindow.addEventListener("resize", () => { this.updateRect(); });
const sourceElement = this.props.mainLayout!.getRootDiv()!; const targetElement = this.selfRef.current!;
copyInlineStyles(sourceElement, targetElement);
this.styleObserver = new MutationObserver(() => { const changed = copyInlineStyles(sourceElement, targetElement); if (changed) { this.redraw("mutation observer"); } });
// Observe changes to the source element's style attribute this.styleObserver.observe(sourceElement, { attributeFilter: ["style"] }); }
// allow tabs to overlay when hidden document.addEventListener("visibilitychange", () => { for (const [_, layoutWindow] of this.props.model.getwindowsMap()) { const layout = layoutWindow.layout; if (layout) { this.redraw("visibility change"); } } }); }
componentDidUpdate() { this.currentDocument = (this.selfRef.current as HTMLElement).ownerDocument; this.currentWindow = this.currentDocument.defaultView!; if (this.isMainWindow) { if (this.props.model !== this.previousModel) { if (this.previousModel !== undefined) { this.previousModel.removeChangeListener(this.onModelChange); // stop listening to old model } this.props.model.getwindowsMap().get(this.windowId)!.layout = this; this.props.model.addChangeListener(this.onModelChange); this.layoutWindow = this.props.model.getwindowsMap().get(this.windowId)!; this.layoutWindow.layout = this; this.layoutWindow.toScreenRectFunction = (r) => this.getScreenRect(r); this.previousModel = this.props.model; this.tidyMoveablesMap(); }
this.updateLayoutMetrics(); } }
componentWillUnmount() { if (this.selfRef.current) { this.resizeObserver?.unobserve(this.selfRef.current); } this.styleObserver?.disconnect(); }
render() { // first render will be used to find the size (via selfRef) if (!this.selfRef.current) { return ( <div ref={this.selfRef} className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)}> <div ref={this.moveablesRef} key="__moveables__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MOVEABLES)} /> {this.renderMetricsElements()} </div> ); }
const model = this.props.model; model.getRoot(this.windowId).calcMinMaxSize(); model.getRoot(this.windowId).setPaths(""); model.getBorderSet().setPaths();
const inner = this.renderLayout(); const outer = this.renderBorders(inner);
const tabs = this.renderTabs(); const reorderedTabs = this.reorderComponents(tabs, this.orderedIds);
let floatingWindows = null; let tabMoveables = null; let tabStamps = null; let metricElements = null;
if (this.isMainWindow) { floatingWindows = this.renderWindows(); metricElements = this.renderMetricsElements(); tabMoveables = this.renderTabMoveables(); tabStamps = ( <div key="__tabStamps__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_TAB_STAMPS)} > {this.renderTabStamps()} </div> ); }
return ( <div ref={this.selfRef} className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)} onDragEnter={this.onDragEnterRaw} onDragLeave={this.onDragLeaveRaw} onDragOver={this.onDragOver} onDrop={this.onDrop} > <div ref={this.moveablesRef} key="__moveables__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MOVEABLES)} /> {metricElements} <Overlay key="__overlay__" layout={this} show={this.state.showOverlay} /> {outer} {reorderedTabs} {tabMoveables} {tabStamps} {this.state.portal} {floatingWindows} </div> ); }
renderBorders(inner: React.ReactNode) { const classMain = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MAIN); const borders = this.props.model.getBorderSet().getBorderMap(); if (this.isMainWindow && borders.size > 0) { inner = ( <div className={classMain} ref={this.mainRef}> {inner} </div> ); const borderSetComponents = new Map<DockLocation, React.ReactNode>(); const borderSetContentComponents = new Map<DockLocation, React.ReactNode>(); for (const [_, location] of DockLocation.values) { const border = borders.get(location)!; const showBorder = border?.isShowing() && (!border.isAutoHide() || (border.isAutoHide() && (border.getChildren().length > 0 || this.state.showHiddenBorder === location))); if (showBorder) { borderSetComponents.set( location, <BorderTabSet layout={this} border={border} size={this.state.calculatedBorderBarSize} />, ); borderSetContentComponents.set( location, <BorderTab layout={this} border={border} show={border.getSelected() !== -1} />, ); } }
const classBorderOuter = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_BORDER_CONTAINER); const classBorderInner = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_BORDER_CONTAINER_INNER);
if (this.props.model.getBorderSet().getLayoutHorizontal()) { const innerWithBorderTabs = ( <div className={classBorderInner} style={{ flexDirection: "column" }}> {borderSetContentComponents.get(DockLocation.TOP)} <div className={classBorderInner} style={{ flexDirection: "row" }}> {borderSetContentComponents.get(DockLocation.LEFT)} {inner} {borderSetContentComponents.get(DockLocation.RIGHT)} </div> {borderSetContentComponents.get(DockLocation.BOTTOM)} </div> ); return ( <div className={classBorderOuter} style={{ flexDirection: "column" }}> {borderSetComponents.get(DockLocation.TOP)} <div className={classBorderInner} style={{ flexDirection: "row" }}> {borderSetComponents.get(DockLocation.LEFT)} {innerWithBorderTabs} {borderSetComponents.get(DockLocation.RIGHT)} </div> {borderSetComponents.get(DockLocation.BOTTOM)} </div> ); } const innerWithBorderTabs = ( <div className={classBorderInner} style={{ flexDirection: "row" }}> {borderSetContentComponents.get(DockLocation.LEFT)} <div className={classBorderInner} style={{ flexDirection: "column" }}> {borderSetContentComponents.get(DockLocation.TOP)} {inner} {borderSetContentComponents.get(DockLocation.BOTTOM)} </div> {borderSetContentComponents.get(DockLocation.RIGHT)} </div> );
return ( <div className={classBorderOuter} style={{ flexDirection: "row" }}> {borderSetComponents.get(DockLocation.LEFT)} <div className={classBorderInner} style={{ flexDirection: "column" }}> {borderSetComponents.get(DockLocation.TOP)} {innerWithBorderTabs} {borderSetComponents.get(DockLocation.BOTTOM)} </div> {borderSetComponents.get(DockLocation.RIGHT)} </div> ); } // no borders return ( <div className={classMain} ref={this.mainRef} style={{ position: "absolute", top: 0, left: 0, bottom: 0, right: 0, display: "flex", }} > {inner} </div> ); }
renderLayout() { return ( <> <Row key="__row__" layout={this} node={this.props.model.getRoot(this.windowId)} /> {this.renderEdgeIndicators()} </> ); }
renderEdgeIndicators() { const edges: React.ReactNode[] = []; const arrowIcon = this.icons.edgeArrow; if (this.state.showEdges) { const r = this.props.model.getRoot(this.windowId).getRect(); const length = edgeRectLength; const width = edgeRectWidth; const offset = edgeRectLength / 2; const className = this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT); const radius = 50; edges.push( <div key="North" style={{ top: 0, left: r.width / 2 - offset, width: length, height: width, borderBottomLeftRadius: radius, borderBottomRightRadius: radius, }} className={`${className} ${this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_TOP)}`} > <div style={{ transform: "rotate(180deg)" }}>{arrowIcon}</div> </div>, ); edges.push( <div key="West" style={{ top: r.height / 2 - offset, left: 0, width: width, height: length, borderTopRightRadius: radius, borderBottomRightRadius: radius, }} className={`${className} ${this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_LEFT)}`} > <div style={{ transform: "rotate(90deg)" }}>{arrowIcon}</div> </div>, ); edges.push( <div key="South" style={{ top: r.height - width, left: r.width / 2 - offset, width: length, height: width, borderTopLeftRadius: radius, borderTopRightRadius: radius, }} className={`${className} ${this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_BOTTOM)}`} > <div>{arrowIcon}</div> </div>, ); edges.push( <div key="East" style={{ top: r.height / 2 - offset, left: r.width - width, width: width, height: length, borderTopLeftRadius: radius, borderBottomLeftRadius: radius, }} className={`${className} ${this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_RIGHT)}`} > <div style={{ transform: "rotate(-90deg)" }}>{arrowIcon}</div> </div>, ); }
return edges; }
renderWindows() { const floatingWindows: React.ReactNode[] = []; if (this.supportsPopout) { const windows = this.props.model.getwindowsMap(); let i = 1; for (const [windowId, layoutWindow] of windows) { if (windowId !== Model.MAIN_WINDOW_ID) { floatingWindows.push( <PopoutWindow key={windowId} layout={this} title={`${this.popoutWindowName} ${i}`} layoutWindow={layoutWindow} url={`${this.popoutURL}?id=${windowId}`} onSetWindow={this.onSetWindow} onCloseWindow={this.onCloseWindow} > <div className={this.props.popoutClassName}> <LayoutInternal {...this.props} windowId={windowId} mainLayout={this} /> </div> </PopoutWindow>, ); i++; } } } return floatingWindows; }
renderTabMoveables() { const tabMoveables: React.ReactNode[] = [];
this.props.model.visitNodes((node) => { if (node instanceof TabNode) { const child = node as TabNode; const element = this.getMoveableElement(child.getId()); child.setMoveableElement(element); const selected = child.isSelected(); const rect = (child.getParent() as BorderNode | TabSetNode).getContentRect();
// only render first time if size >0 const renderTab = child.isRendered() || ((selected || !child.isEnableRenderOnDemand()) && rect.width > 0 && rect.height > 0);
if (renderTab) { const key = child.getId() + (child.isEnableWindowReMount() ? child.getWindowId() : ""); tabMoveables.push( createPortal( <SizeTracker rect={rect} selected={child.isSelected()} forceRevision={this.state.forceRevision} tabsRevision={this.props.renderRevision} key={key} > {this.props.factory(child)} </SizeTracker>, element, key, ), );
child.setRendered(renderTab); } } });
return tabMoveables; }
renderTabStamps() { const tabStamps: React.ReactNode[] = [];
this.props.model.visitNodes((node) => { if (node instanceof TabNode) { const child = node as TabNode;
// what the tab should look like when dragged (since images need to have been loaded before drag image can be taken) tabStamps.push(<DragContainer key={child.getId()} layout={this} node={child} />); } });
return tabStamps; }
renderTabs() { const tabs = new Map<string, React.ReactNode>(); this.props.model.visitWindowNodes(this.windowId, (node) => { if (node instanceof TabNode) { const child = node as TabNode; const selected = child.isSelected(); const path = child.getPath();
const renderTab = child.isRendered() || selected || !child.isEnableRenderOnDemand();
if (renderTab) { // const rect = (child.getParent() as BorderNode | TabSetNode).getContentRect(); // const key = child.getId();
tabs.set( child.getId(), // <SizeTracker rect={rect} forceRevision={this.state.forceRevision} key={key}> <Tab key={child.getId()} layout={this} path={path} node={child} selected={selected} />, // </SizeTracker> ); } } }); return tabs; }
renderMetricsElements() { return ( <div key="findBorderBarSize" ref={this.findBorderBarSizeRef} className={this.getClassName(CLASSES.FLEXLAYOUT__BORDER_SIZER)} > FindBorderBarSize </div> ); }
checkForBorderToShow(x: number, y: number) { const r = this.getBoundingClientRect(this.mainRef.current!); const c = r.getCenter(); const margin = edgeRectWidth; const offset = edgeRectLength / 2;
let overEdge = false; if ( this.props.model.isEnableEdgeDock() && this.state.showHiddenBorder === DockLocation.CENTER ) { if ((y > c.y - offset && y < c.y + offset) || (x > c.x - offset && x < c.x + offset)) { overEdge = true; } }
let location = DockLocation.CENTER; if (!overEdge) { if (x <= r.x + margin) { location = DockLocation.LEFT; } else if (x >= r.getRight() - margin) { location = DockLocation.RIGHT; } else if (y <= r.y + margin) { location = DockLocation.TOP; } else if (y >= r.getBottom() - margin) { location = DockLocation.BOTTOM; } }
if (location !== this.state.showHiddenBorder) { this.setState({ showHiddenBorder: location }); } }
updateLayoutMetrics = () => { if (this.findBorderBarSizeRef.current) { const borderBarSize = this.findBorderBarSizeRef.current.getBoundingClientRect().height; if (borderBarSize !== this.state.calculatedBorderBarSize) { this.setState({ calculatedBorderBarSize: borderBarSize }); } } };
tidyMoveablesMap() { const tabs = new Map<string, TabNode>(); this.props.model.visitNodes((node, _) => { if (node instanceof TabNode) { tabs.set(node.getId(), node); } });
for (const [nodeId, element] of this.moveableElementMap) { if (!tabs.has(nodeId)) { element.remove(); // remove from dom this.moveableElementMap.delete(nodeId); // remove map entry } } }
reorderComponents(components: Map<string, React.ReactNode>, ids: string[]) { const nextIds: string[] = []; const nextIdsSet = new Set<string>();
let reordered: React.ReactNode[] = []; // Keep any previous tabs in the same DOM order as before, removing any that have been deleted for (const id of ids) { if (components.get(id)) { nextIds.push(id); nextIdsSet.add(id); } } ids.splice(0, ids.length, ...nextIds);
// Add tabs that have been added to the DOM for (const [id, _] of components) { if (!nextIdsSet.has(id)) { ids.push(id); } }
reordered = ids.map((id) => { return components.get(id); });
return reordered; }
onModelChange = (action: Action) => { this.redrawInternal("model change"); if (this.props.onModelChange) { this.props.onModelChange(this.props.model, action); } };
redraw(type?: string) { this.mainLayout.setState((state, props) => { return { forceRevision: state.forceRevision + 1 }; }); }
redrawInternal(type: string) { this.mainLayout.setState((state, props) => { return { layoutRevision: state.layoutRevision + 1 }; }); }
doAction(action: Action): Node | undefined { if (this.props.onAction !== undefined) { const outcome = this.props.onAction(action); if (outcome !== undefined) { return this.props.model.doAction(outcome); } return undefined; } return this.props.model.doAction(action); }
updateRect = () => { const rect = this.getDomRect(); if (!rect.equals(this.state.rect) && rect.width !== 0 && rect.height !== 0) { this.setState({ rect }); if (this.windowId !== Model.MAIN_WINDOW_ID) { this.redrawInternal("rect updated"); } } };
getBoundingClientRect(div: HTMLElement): Rect { const layoutRect = this.getDomRect(); if (layoutRect) { return Rect.getBoundingClientRect(div).relativeTo(layoutRect); } return Rect.empty(); }
getMoveableContainer() { return this.moveablesRef.current; }
getMoveableElement(id: string) { let moveableElement = this.moveableElementMap.get(id); if (moveableElement === undefined) { moveableElement = document.createElement("div"); this.moveablesRef.current!.appendChild(moveableElement); moveableElement.className = CLASSES.FLEXLAYOUT__TAB_MOVEABLE; this.moveableElementMap.set(id, moveableElement); } return moveableElement; }
getMainLayout() { return this.mainLayout; }
getClassName = (defaultClassName: string) => { if (this.props.classNameMapper === undefined) { return defaultClassName; } return this.props.classNameMapper(defaultClassName); };
getCurrentDocument() { return this.currentDocument; }
getDomRect() { if (this.selfRef.current) { return Rect.fromDomRect(this.selfRef.current.getBoundingClientRect()); } return Rect.empty(); }
getWindowId() { return this.windowId; }
getRootDiv() { return this.selfRef.current; }
getMainElement() { return this.mainRef.current; }
getFactory() { return this.props.factory; }
isSupportsPopout() { return this.supportsPopout; }
isRealtimeResize() { return this.props.realtimeResize ?? false; }
getPopoutURL() { return this.popoutURL; }
setEditingTab(tabNode?: TabNode) { this.setState({ editingTab: tabNode }); }
getEditingTab() { return this.state.editingTab; }
getModel() { return this.props.model; }
onCloseWindow = (windowLayout: LayoutWindow) => { this.doAction(Actions.closeWindow(windowLayout.windowId)); };
onSetWindow = (windowLayout: LayoutWindow, window: Window) => {};
getScreenRect(inRect: Rect) { const rect = inRect.clone(); const layoutRect = this.getDomRect(); // Note: outerHeight can be less than innerHeight when window is zoomed, so cannot use // const navHeight = Math.min(65, this.currentWindow!.outerHeight - this.currentWindow!.innerHeight); // const navWidth = Math.min(65, this.currentWindow!.outerWidth - this.currentWindow!.innerWidth); const navHeight = 60; const navWidth = 2;
rect.x = this.currentWindow!.screenX + this.currentWindow!.scrollX + navWidth / 2 + layoutRect.x + rect.x; rect.y = this.currentWindow!.screenY + this.currentWindow!.scrollY + (navHeight - navWidth / 2) + layoutRect.y + rect.y; rect.height += navHeight; rect.width += navWidth; return rect; }
addTabToTabSet(tabsetId: string, json: IJsonTabNode): TabNode | undefined { const tabsetNode = this.props.model.getNodeById(tabsetId); if (tabsetNode !== undefined) { const node = this.doAction(Actions.addNode(json, tabsetId, DockLocation.CENTER, -1)); return node as TabNode; } return undefined; }
addTabToActiveTabSet(json: IJsonTabNode): TabNode | undefined { const tabsetNode = this.props.model.getActiveTabset(this.windowId); if (tabsetNode !== undefined) { const node = this.doAction( Actions.addNode(json, tabsetNode.getId(), DockLocation.CENTER, -1), ); return node as TabNode; } return undefined; }
showControlInPortal = (control: React.ReactNode, element: HTMLElement) => { const portal = createPortal(control, element) as React.ReactPortal; this.setState({ portal }); };
hideControlInPortal = () => { this.setState({ portal: undefined }); };
getIcons = () => { return this.icons; };
maximize(tabsetNode: TabSetNode) { this.doAction(Actions.maximizeToggle(tabsetNode.getId(), this.getWindowId())); }
customizeTab(tabNode: TabNode, renderValues: ITabRenderValues) { if (this.props.onRenderTab) { this.props.onRenderTab(tabNode, renderValues); } }
customizeTabSet(tabSetNode: TabSetNode | BorderNode, renderValues: ITabSetRenderValues) { if (this.props.onRenderTabSet) { this.props.onRenderTabSet(tabSetNode, renderValues); } }
i18nName(id: I18nLabel, param?: string) { let message: string | undefined; if (this.props.i18nMapper) { message = this.props.i18nMapper(id, param); } if (message === undefined) { message = id + (param === undefined ? "" : param); } return message; }
getShowOverflowMenu() { return this.props.onShowOverflowMenu; }
getTabSetPlaceHolderCallback() { return this.props.onTabSetPlaceHolder; }
showContextMenu( node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>, ) { if (this.props.onContextMenu) { this.props.onContextMenu(node, event); } }
auxMouseClick( node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>, ) { if (this.props.onAuxMouseClick) { this.props.onAuxMouseClick(node, event); } }
public showOverlay(show: boolean) { this.setState({ showOverlay: show }); enablePointerOnIFrames(!show, this.currentDocument!); }
// *************************** Start Drag Drop *************************************
addTabWithDragAndDrop( event: DragEvent, json: IJsonTabNode, onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void, ) { const tempNode = TabNode.fromJson(json, this.props.model, false); LayoutInternal.dragState = new DragState( this.mainLayout, DragSource.Add, tempNode, json, onDrop, ); }
moveTabWithDragAndDrop(event: DragEvent, node: TabNode | TabSetNode) { this.setDragNode(event, node); }
public setDragNode = (event: DragEvent, node: Node & IDraggable) => { LayoutInternal.dragState = new DragState( this.mainLayout, DragSource.Internal, node, undefined, undefined, ); // Note: can only set (very) limited types on android! so cannot set json // Note: must set text/plain for android to allow drag, // so just set a simple message indicating its a flexlayout drag (this is not used anywhere else) event.dataTransfer!.setData("text/plain", "--flexlayout--"); event.dataTransfer!.effectAllowed = "copyMove"; event.dataTransfer!.dropEffect = "move";
this.dragEnterCount = 0;
if (node instanceof TabSetNode) { let rendered = false; let content = this.i18nName(I18nLabel.Move_Tabset); if (node.getChildren().length > 0) { content = this.i18nName(I18nLabel.Move_Tabs).replace( "?", String(node.getChildren().length), ); } if (this.props.onRenderDragRect) { const dragComponent = this.props.onRenderDragRect(content, node, undefined); if (dragComponent) { this.setDragComponent(event, dragComponent, 10, 10); rendered = true; } } if (!rendered) { this.setDragComponent(event, content, 10, 10); } } else { const element = event.target as HTMLElement; const rect = element.getBoundingClientRect(); const offsetX = event.clientX - rect.left; const offsetY = event.clientY - rect.top; const parentNode = node?.getParent(); const isInVerticalBorder = parentNode instanceof BorderNode && (parentNode as BorderNode).getOrientation() === Orientation.HORZ; const x = isInVerticalBorder ? 10 : offsetX; const y = isInVerticalBorder ? 10 : offsetY;
let rendered = false; if (this.props.onRenderDragRect) { const content = <TabButtonStamp key={node.getId()} layout={this} node={node as TabNode} />; const dragComponent = this.props.onRenderDragRect(content, node, undefined); if (dragComponent) { this.setDragComponent(event, dragComponent, x, y); rendered = true; } } if (!rendered) { if (isSafari()) { // safari doesnt render the offscreen tabstamps this.setDragComponent( event, <TabButtonStamp node={node as TabNode} layout={this} />, x, y, ); } else { event.dataTransfer!.setDragImage((node as TabNode).getTabStamp()!, x, y); } } } };
public setDragComponent(event: DragEvent, component: React.ReactNode, x: number, y: number) { const dragElement = ( <div style={{ position: "unset" }} className={`${this.getClassName( CLASSES.FLEXLAYOUT__LAYOUT, )} ${this.getClassName(CLASSES.FLEXLAYOUT__DRAG_RECT)}`} > {component} </div> );
const tempDiv = this.currentDocument!.createElement("div"); tempDiv.setAttribute("data-layout-path", "/drag-rectangle"); tempDiv.style.position = "absolute"; tempDiv.style.left = "-10000px"; tempDiv.style.top = "-10000px"; this.currentDocument!.body.appendChild(tempDiv); createRoot(tempDiv).render(dragElement);
event.dataTransfer!.setDragImage(tempDiv, x, y); setTimeout(() => { this.currentDocument!.body.removeChild(tempDiv!); }, 0); }
setDraggingOverWindow(overWindow: boolean) { if (this.isDraggingOverWindow !== overWindow) { if (this.outlineDiv) { this.outlineDiv!.style.visibility = overWindow ? "hidden" : "visible"; }
if (overWindow) { this.setState({ showEdges: false }); } else { // add edge indicators if (this.props.model.getMaximizedTabset(this.windowId) === undefined) { this.setState({ showEdges: this.props.model.isEnableEdgeDock() }); } }
this.isDraggingOverWindow = overWindow; } }
onDragEnterRaw = (event: React.DragEvent<HTMLElement>) => { this.dragEnterCount++; if (this.dragEnterCount === 1) { this.onDragEnter(event); } };
onDragLeaveRaw = (event: React.DragEvent<HTMLElement>) => { this.dragEnterCount--; if (this.dragEnterCount === 0) { this.onDragLeave(event); } };
clearDragMain() { LayoutInternal.dragState = undefined; if (this.windowId === Model.MAIN_WINDOW_ID) { this.isDraggingOverWindow = false; } for (const [, layoutWindow] of this.props.model.getwindowsMap()) { layoutWindow.layout!.clearDragLocal(); } }
clearDragLocal() { this.setState({ showEdges: false }); this.showOverlay(false); this.dragEnterCount = 0; this.dragging = false; if (this.outlineDiv) { this.selfRef.current!.removeChild(this.outlineDiv); this.outlineDiv = undefined; } }
onDragEnter = (event: React.DragEvent<HTMLElement>) => { if (!LayoutInternal.dragState && this.props.onExternalDrag) { // not internal dragging const externalDrag = this.props.onExternalDrag!(event); if (externalDrag) { const tempNode = TabNode.fromJson(externalDrag.json, this.props.model, false); LayoutInternal.dragState = new DragState( this.mainLayout, DragSource.External, tempNode, externalDrag.json, externalDrag.onDrop, ); } }
if (LayoutInternal.dragState) { if ( this.windowId !== Model.MAIN_WINDOW_ID && LayoutInternal.dragState.mainLayout === this.mainLayout ) { LayoutInternal.dragState.mainLayout.setDraggingOverWindow(true); }
if (LayoutInternal.dragState.mainLayout !== this.mainLayout) { return; // drag not by this layout or its popouts }
event.preventDefault();
this.dropInfo = undefined; const rootdiv = this.selfRef.current; this.outlineDiv = this.currentDocument!.createElement("div"); this.outlineDiv.className = this.getClassName(CLASSES.FLEXLAYOUT__OUTLINE_RECT); this.outlineDiv.style.visibility = "hidden"; const speed = this.props.model.getAttribute("tabDragSpeed") as number; this.outlineDiv.style.transition = `top ${speed}s, left ${speed}s, width ${speed}s, height ${speed}s`;
rootdiv!.appendChild(this.outlineDiv);
this.dragging = true; this.showOverlay(true); // add edge indicators if ( !this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === undefined ) { this.setState({ showEdges: this.props.model.isEnableEdgeDock() }); }
const clientRect = this.selfRef.current?.getBoundingClientRect()!; const r = new Rect(event.clientX - clientRect.left, event.clientY - clientRect.top, 1, 1); r.positionElement(this.outlineDiv); } };
onDragOver = (event: React.DragEvent<HTMLElement>) => { if (this.dragging && !this.isDraggingOverWindow) { event.preventDefault(); const clientRect = this.selfRef.current?.getBoundingClientRect(); const pos = { x: event.clientX - (clientRect?.left ?? 0), y: event.clientY - (clientRect?.top ?? 0), };
this.checkForBorderToShow(pos.x, pos.y);
const dropInfo = this.props.model.findDropTargetNode( this.windowId, LayoutInternal.dragState!.dragNode!, pos.x, pos.y, ); if (dropInfo) { this.dropInfo = dropInfo; if (this.outlineDiv) { this.outlineDiv.className = this.getClassName(dropInfo.className); dropInfo.rect.positionElement(this.outlineDiv); this.outlineDiv.style.visibility = "visible"; } } } };
onDragLeave = (event: React.DragEvent<HTMLElement>) => { if (this.dragging) { if (this.windowId !== Model.MAIN_WINDOW_ID) { LayoutInternal.dragState!.mainLayout.setDraggingOverWindow(false); }
this.clearDragLocal(); } };
onDrop = (event: React.DragEvent<HTMLElement>) => { if (this.dragging) { event.preventDefault();
const dragState = LayoutInternal.dragState!; if (this.dropInfo) { if (dragState.dragJson !== undefined) { const newNode = this.doAction( Actions.addNode( dragState.dragJson, this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index, ), );
if (dragState.fnNewNodeDropped !== undefined) { dragState.fnNewNodeDropped(newNode, event); } } else if (dragState.dragNode !== undefined) { this.doAction( Actions.moveNode( dragState.dragNode.getId(), this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index, ), ); } }
this.mainLayout.clearDragMain(); } this.dragEnterCount = 0; // must set to zero here ref sublayouts };
// *************************** End Drag Drop *************************************}
export const FlexLayoutVersion = "0.8.9";
export type DragRectRenderCallback = ( content: React.ReactNode | undefined, node?: Node, json?: IJsonTabNode,) => React.ReactNode | undefined;
export type NodeMouseEvent = ( node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>,) => void;
export type ShowOverflowMenuCallback = ( node: TabSetNode | BorderNode, mouseEvent: React.MouseEvent<HTMLElement, MouseEvent>, items: { index: number; node: TabNode }[], onSelect: (item: { index: number; node: TabNode }) => void,) => void;
export type TabSetPlaceHolderCallback = (node: TabSetNode) => React.ReactNode;
export interface ITabSetRenderValues { /** components that will be added after the tabs */ stickyButtons: React.ReactNode[]; /** components that will be added at the end of the tabset */ buttons: React.ReactNode[]; /** position to insert overflow button within [...stickyButtons, ...buttons] * if left undefined position will be after the sticky buttons (if any) */ overflowPosition: number | undefined;}
export interface ITabRenderValues { /** the icon or other leading component */ leading: React.ReactNode; /** the main tab text/component */ content: React.ReactNode; /** a set of react components to add to the tab after the content */ buttons: React.ReactNode[];}
export interface IIcons { close?: React.ReactNode | ((tabNode: TabNode) => React.ReactNode); closeTabset?: React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode); popout?: React.ReactNode | ((tabNode: TabNode) => React.ReactNode); maximize?: React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode); restore?: React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode); more?: | React.ReactNode | (( tabSetNode: TabSetNode | BorderNode, hiddenTabs: { node: TabNode; index: number }[], ) => React.ReactNode); edgeArrow?: React.ReactNode; activeTabset?: React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode);}
const defaultIcons = { close: <CloseIcon />, closeTabset: <CloseIcon />, popout: <PopoutIcon />, maximize: <MaximizeIcon />, restore: <RestoreIcon />, more: <OverflowIcon />, edgeArrow: <EdgeIcon />, activeTabset: <AsterickIcon />,};
enum DragSource { Internal = "internal", External = "external", Add = "add",}
/** @internal */const defaultSupportsPopout: boolean = isDesktop();
/** @internal */const edgeRectLength = 100;/** @internal */const edgeRectWidth = 10;
// global layout drag stateclass DragState { public readonly mainLayout: LayoutInternal; public readonly dragSource: DragSource; public readonly dragNode: (Node & IDraggable) | undefined; public readonly dragJson: IJsonTabNode | undefined; public readonly fnNewNodeDropped: | ((node?: Node, event?: React.DragEvent<HTMLElement>) => void) | undefined;
public constructor( mainLayout: LayoutInternal, dragSource: DragSource, dragNode: (Node & IDraggable) | undefined, dragJson: IJsonTabNode | undefined, fnNewNodeDropped: ((node?: Node, event?: React.DragEvent<HTMLElement>) => void) | undefined, ) { this.mainLayout = mainLayout; this.dragSource = dragSource; this.dragNode = dragNode; this.dragJson = dragJson; this.fnNewNodeDropped = fnNewNodeDropped; }}