UNPKG

flexlayout-react-v7-react-19

Version:

A multi-tab docking layout manager

590 lines (522 loc) 21.9 kB
import { Attribute } from "../Attribute"; import { AttributeDefinitions } from "../AttributeDefinitions"; import { DockLocation } from "../DockLocation"; import { DropInfo } from "../DropInfo"; import { Orientation } from "../Orientation"; import { Rect } from "../Rect"; import { Action } from "./Action"; import { Actions } from "./Actions"; import { BorderNode } from "./BorderNode"; import { BorderSet } from "./BorderSet"; import { IDraggable } from "./IDraggable"; import { IDropTarget } from "./IDropTarget"; import { IJsonModel, ITabSetAttributes } from "./IJsonModel"; import { Node } from "./Node"; import { RowNode } from "./RowNode"; import { TabNode } from "./TabNode"; import { TabSetNode } from "./TabSetNode"; import { adjustSelectedIndexAfterDock, adjustSelectedIndexAfterFloat, randomUUID } from "./Utils"; /** @internal */ export interface ILayoutMetrics { headerBarSize: number; tabBarSize: number; borderBarSize: number; } /** * Class containing the Tree of Nodes used by the FlexLayout component */ export class Model { /** * Loads the model from the given json object * @param json the json model to load * @returns {Model} a new Model object */ static fromJson(json: IJsonModel) { const model = new Model(); Model._attributeDefinitions.fromJson(json.global, model._attributes); if (json.borders) { model._borders = BorderSet._fromJson(json.borders, model); } model._root = RowNode._fromJson(json.layout, model); model._tidy(); // initial tidy of node tree return model; } /** @internal */ private static _attributeDefinitions: AttributeDefinitions = Model._createAttributeDefinitions(); /** @internal */ private static _createAttributeDefinitions(): AttributeDefinitions { const attributeDefinitions = new AttributeDefinitions(); attributeDefinitions.add("legacyOverflowMenu", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("enableEdgeDock", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("rootOrientationVertical", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("marginInsets", { top: 0, right: 0, bottom: 0, left: 0 }) .setType("IInsets"); attributeDefinitions.add("enableUseVisibility", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("enableRotateBorderIcons", true).setType(Attribute.BOOLEAN); // splitter attributeDefinitions.add("splitterSize", -1).setType(Attribute.NUMBER); attributeDefinitions.add("splitterExtra", 0).setType(Attribute.NUMBER); // tab attributeDefinitions.add("tabEnableClose", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabCloseType", 1).setType("ICloseType"); attributeDefinitions.add("tabEnableFloat", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabEnableDrag", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabEnableRename", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabContentClassName", undefined).setType(Attribute.STRING); attributeDefinitions.add("tabClassName", undefined).setType(Attribute.STRING); attributeDefinitions.add("tabIcon", undefined).setType(Attribute.STRING); attributeDefinitions.add("tabEnableRenderOnDemand", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabDragSpeed", 0.3).setType(Attribute.NUMBER); attributeDefinitions.add("tabBorderWidth", -1).setType(Attribute.NUMBER); attributeDefinitions.add("tabBorderHeight", -1).setType(Attribute.NUMBER); // tabset attributeDefinitions.add("tabSetEnableDeleteWhenEmpty", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDrop", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDrag", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDivide", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableMaximize", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableClose", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableSingleTabStretch", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetAutoSelectTab", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetClassNameTabStrip", undefined).setType(Attribute.STRING); attributeDefinitions.add("tabSetClassNameHeader", undefined).setType(Attribute.STRING); attributeDefinitions.add("tabSetEnableTabStrip", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetHeaderHeight", 0).setType(Attribute.NUMBER); attributeDefinitions.add("tabSetTabStripHeight", 0).setType(Attribute.NUMBER); attributeDefinitions.add("tabSetMarginInsets", { top: 0, right: 0, bottom: 0, left: 0 }) .setType("IInsets"); attributeDefinitions.add("tabSetBorderInsets", { top: 0, right: 0, bottom: 0, left: 0 }) .setType("IInsets"); attributeDefinitions.add("tabSetTabLocation", "top").setType("ITabLocation"); attributeDefinitions.add("tabSetMinWidth", 0).setType(Attribute.NUMBER); attributeDefinitions.add("tabSetMinHeight", 0).setType(Attribute.NUMBER); // border attributeDefinitions.add("borderSize", 200).setType(Attribute.NUMBER); attributeDefinitions.add("borderMinSize", 0).setType(Attribute.NUMBER); attributeDefinitions.add("borderBarSize", 0).setType(Attribute.NUMBER); attributeDefinitions.add("borderEnableDrop", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("borderAutoSelectTabWhenOpen", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("borderAutoSelectTabWhenClosed", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("borderClassName", undefined).setType(Attribute.STRING); attributeDefinitions.add("borderEnableAutoHide", false).setType(Attribute.BOOLEAN); return attributeDefinitions; } /** @internal */ private _attributes: Record<string, any>; /** @internal */ private _idMap: Record<string, Node>; /** @internal */ private _changeListener?: (action: Action) => void; /** @internal */ private _root?: RowNode; /** @internal */ private _borders: BorderSet; /** @internal */ private _onAllowDrop?: (dragNode: Node, dropInfo: DropInfo) => boolean; /** @internal */ private _maximizedTabSet?: TabSetNode; /** @internal */ private _activeTabSet?: TabSetNode; /** @internal */ private _borderRects: { inner: Rect; outer: Rect } = { inner: Rect.empty(), outer: Rect.empty() }; /** @internal */ private _pointerFine: boolean; /** @internal */ private _onCreateTabSet?: (tabNode?: TabNode) => ITabSetAttributes; /** @internal */ private _showHiddenBorder: DockLocation; /** * 'private' constructor. Use the static method Model.fromJson(json) to create a model * @internal */ private constructor() { this._attributes = {}; this._idMap = {}; this._borders = new BorderSet(this); this._pointerFine = true; this._showHiddenBorder = DockLocation.CENTER; } /** @internal */ _setChangeListener(listener: ((action: Action) => void) | undefined) { this._changeListener = listener; } /** * Get the currently active tabset node */ getActiveTabset() { if (this._activeTabSet && this.getNodeById(this._activeTabSet.getId())) { return this._activeTabSet; } else { return undefined; } } /** @internal */ _getShowHiddenBorder() { return this._showHiddenBorder; } /** @internal */ _setShowHiddenBorder(location: DockLocation) { this._showHiddenBorder = location; } /** @internal */ _setActiveTabset(tabsetNode: TabSetNode | undefined) { this._activeTabSet = tabsetNode; } /** * Get the currently maximized tabset node */ getMaximizedTabset() { return this._maximizedTabSet; } /** @internal */ _setMaximizedTabset(tabsetNode: (TabSetNode | undefined)) { this._maximizedTabSet = tabsetNode; } /** * Gets the root RowNode of the model * @returns {RowNode} */ getRoot() { return this._root as RowNode; } isRootOrientationVertical() { return this._attributes.rootOrientationVertical as boolean; } isUseVisibility() { return this._attributes.enableUseVisibility as boolean; } isEnableRotateBorderIcons() { return this._attributes.enableRotateBorderIcons as boolean; } /** * Gets the * @returns {BorderSet|*} */ getBorderSet() { return this._borders; } /** @internal */ _getOuterInnerRects() { return this._borderRects; } /** @internal */ _getPointerFine() { return this._pointerFine; } /** @internal */ _setPointerFine(pointerFine: boolean) { this._pointerFine = pointerFine; } /** * Visits all the nodes in the model and calls the given function for each * @param fn a function that takes visited node and a integer level as parameters */ visitNodes(fn: (node: Node, level: number) => void) { this._borders._forEachNode(fn); (this._root as RowNode)._forEachNode(fn, 0); } /** * Gets a node by its id * @param id the id to find */ getNodeById(id: string): Node | undefined { return this._idMap[id]; } /** * Finds the first/top left tab set of the given node. * @param node The top node you want to begin searching from, deafults to the root node * @returns The first Tab Set */ getFirstTabSet(node = this._root as Node): Node { const child = node.getChildren()[0]; if (child instanceof TabSetNode) { return child; } else { return this.getFirstTabSet(child); } } /** * Update the node tree by performing the given action, * Actions should be generated via static methods on the Actions class * @param action the action to perform * @returns added Node for Actions.addNode; undefined otherwise */ doAction(action: Action): Node | undefined { let returnVal = undefined; // console.log(action); switch (action.type) { case Actions.ADD_NODE: { const newNode = new TabNode(this, action.data.json, true); const toNode = this._idMap[action.data.toNode] as Node & IDraggable; if (toNode instanceof TabSetNode || toNode instanceof BorderNode || toNode instanceof RowNode) { toNode.drop(newNode, DockLocation.getByName(action.data.location), action.data.index, action.data.select); returnVal = newNode; } break; } case Actions.MOVE_NODE: { const fromNode = this._idMap[action.data.fromNode] as Node & IDraggable; if (fromNode instanceof TabNode || fromNode instanceof TabSetNode) { const toNode = this._idMap[action.data.toNode] as Node & IDropTarget; if (toNode instanceof TabSetNode || toNode instanceof BorderNode || toNode instanceof RowNode) { toNode.drop(fromNode, DockLocation.getByName(action.data.location), action.data.index, action.data.select); } } break; } case Actions.DELETE_TAB: { const node = this._idMap[action.data.node]; if (node instanceof TabNode) { node._delete(); } break; } case Actions.DELETE_TABSET: { const node = this._idMap[action.data.node]; if (node instanceof TabSetNode) { // first delete all child tabs that are closeable const children = [...node.getChildren()]; for (let i = 0; i < children.length; i++) { const child = children[i]; if ((child as TabNode).isEnableClose()) { (child as TabNode)._delete(); } } if (node.getChildren().length === 0) { node._delete(); } this._tidy(); } break; } case Actions.FLOAT_TAB: { const node = this._idMap[action.data.node]; if (node instanceof TabNode) { node._setFloating(true); adjustSelectedIndexAfterFloat(node); } break; } case Actions.UNFLOAT_TAB: { const node = this._idMap[action.data.node]; if (node instanceof TabNode) { node._setFloating(false); adjustSelectedIndexAfterDock(node); } break; } case Actions.RENAME_TAB: { const node = this._idMap[action.data.node]; if (node instanceof TabNode) { node._setName(action.data.text); } break; } case Actions.SELECT_TAB: { const tabNode = this._idMap[action.data.tabNode]; if (tabNode instanceof TabNode) { const parent = tabNode.getParent() as Node; const pos = parent.getChildren().indexOf(tabNode); if (parent instanceof BorderNode) { if (parent.getSelected() === pos) { parent._setSelected(-1); } else { parent._setSelected(pos); } } else if (parent instanceof TabSetNode) { if (parent.getSelected() !== pos) { parent._setSelected(pos); } this._activeTabSet = parent; } } break; } case Actions.SET_ACTIVE_TABSET: { if (action.data.tabsetNode === undefined) { this._activeTabSet = undefined; } else { const tabsetNode = this._idMap[action.data.tabsetNode]; if (tabsetNode instanceof TabSetNode) { this._activeTabSet = tabsetNode; } } break; } case Actions.ADJUST_SPLIT: { const node1 = this._idMap[action.data.node1]; const node2 = this._idMap[action.data.node2]; if ((node1 instanceof TabSetNode || node1 instanceof RowNode) && (node2 instanceof TabSetNode || node2 instanceof RowNode)) { this._adjustSplitSide(node1, action.data.weight1, action.data.pixelWidth1); this._adjustSplitSide(node2, action.data.weight2, action.data.pixelWidth2); } break; } case Actions.ADJUST_BORDER_SPLIT: { const node = this._idMap[action.data.node]; if (node instanceof BorderNode) { node._setSize(action.data.pos); } break; } case Actions.MAXIMIZE_TOGGLE: { const node = this._idMap[action.data.node]; if (node instanceof TabSetNode) { if (node === this._maximizedTabSet) { this._maximizedTabSet = undefined; } else { this._maximizedTabSet = node; this._activeTabSet = node; } } break; } case Actions.UPDATE_MODEL_ATTRIBUTES: { this._updateAttrs(action.data.json); break; } case Actions.UPDATE_NODE_ATTRIBUTES: { const node = this._idMap[action.data.node]; node._updateAttrs(action.data.json); break; } default: break; } this._updateIdMap(); if (this._changeListener !== undefined) { this._changeListener(action); } return returnVal; } /** @internal */ _updateIdMap() { // regenerate idMap to stop it building up this._idMap = {}; this.visitNodes((node) => (this._idMap[node.getId()] = node)); // console.log(JSON.stringify(Object.keys(this._idMap))); } /** @internal */ _adjustSplitSide(node: TabSetNode | RowNode, weight: number, pixels: number) { node._setWeight(weight); if (node.getWidth() != null && node.getOrientation() === Orientation.VERT) { node._updateAttrs({ width: pixels }); } else if (node.getHeight() != null && node.getOrientation() === Orientation.HORZ) { node._updateAttrs({ height: pixels }); } } /** * Converts the model to a json object * @returns {IJsonModel} json object that represents this model */ toJson(): IJsonModel { const global: any = {}; Model._attributeDefinitions.toJson(global, this._attributes); // save state of nodes this.visitNodes((node) => { node._fireEvent("save", undefined); }); return { global, borders: this._borders._toJson(), layout: (this._root as RowNode).toJson() }; } getSplitterSize() { let splitterSize = this._attributes.splitterSize as number; if (splitterSize === -1) { // use defaults splitterSize = this._pointerFine ? 8 : 12; // larger for mobile } return splitterSize; } isLegacyOverflowMenu() { return this._attributes.legacyOverflowMenu as boolean; } getSplitterExtra() { return this._attributes.splitterExtra as number; } isEnableEdgeDock() { return this._attributes.enableEdgeDock as boolean; } /** @internal */ _addNode(node: Node) { const id = node.getId(); if (this._idMap[id] !== undefined) { throw new Error(`Error: each node must have a unique id, duplicate id:${node.getId()}`); } if (node.getType() !== "splitter") { this._idMap[id] = node; } } /** @internal */ _layout(rect: Rect, metrics: ILayoutMetrics) { // let start = Date.now(); this._borderRects = this._borders._layoutBorder({ outer: rect, inner: rect }, metrics); rect = this._borderRects.inner.removeInsets(this._getAttribute("marginInsets")); this._root?.calcMinSize(); (this._root as RowNode)._layout(rect, metrics); // console.log("layout time: " + (Date.now() - start)); return rect; } /** @internal */ _findDropTargetNode(dragNode: Node & IDraggable, x: number, y: number) { let node = (this._root as RowNode)._findDropTargetNode(dragNode, x, y); if (node === undefined) { node = this._borders._findDropTargetNode(dragNode, x, y); } return node; } /** @internal */ _tidy() { // console.log("before _tidy", this.toString()); (this._root as RowNode)._tidy(); // console.log("after _tidy", this.toString()); } /** @internal */ _updateAttrs(json: any) { Model._attributeDefinitions.update(json, this._attributes); } /** @internal */ _nextUniqueId() { return '#' + randomUUID(); } /** @internal */ _getAttribute(name: string): any { return this._attributes[name]; } /** * Sets a function to allow/deny dropping a node * @param onAllowDrop function that takes the drag node and DropInfo and returns true if the drop is allowed */ setOnAllowDrop(onAllowDrop: (dragNode: Node, dropInfo: DropInfo) => boolean) { this._onAllowDrop = onAllowDrop; } /** @internal */ _getOnAllowDrop() { return this._onAllowDrop; } /** * set callback called when a new TabSet is created. * The tabNode can be undefined if it's the auto created first tabset in the root row (when the last * tab is deleted, the root tabset can be recreated) * @param onCreateTabSet */ setOnCreateTabSet(onCreateTabSet: (tabNode?: TabNode) => ITabSetAttributes) { this._onCreateTabSet = onCreateTabSet; } /** @internal */ _getOnCreateTabSet() { return this._onCreateTabSet; } static toTypescriptInterfaces() { console.log(Model._attributeDefinitions.toTypescriptInterface("Global", undefined)); console.log(RowNode.getAttributeDefinitions().toTypescriptInterface("Row", Model._attributeDefinitions)); console.log(TabSetNode.getAttributeDefinitions().toTypescriptInterface("TabSet", Model._attributeDefinitions)); console.log(TabNode.getAttributeDefinitions().toTypescriptInterface("Tab", Model._attributeDefinitions)); console.log(BorderNode.getAttributeDefinitions().toTypescriptInterface("Border", Model._attributeDefinitions)); } toString() { return JSON.stringify(this.toJson()); } }