flexlayout-react
Version:
A multi-tab docking layout manager
608 lines • 26.5 kB
JavaScript
import { Attribute } from "../Attribute";
import { AttributeDefinitions } from "../AttributeDefinitions";
import { DockLocation } from "../DockLocation";
import { Rect } from "../Rect";
import { Actions } from "./Actions";
import { BorderNode } from "./BorderNode";
import { BorderSet } from "./BorderSet";
import { RowNode } from "./RowNode";
import { TabNode } from "./TabNode";
import { TabSetNode } from "./TabSetNode";
import { randomUUID } from "./Utils";
import { LayoutWindow } from "./LayoutWindow";
/** @internal */
export const DefaultMin = 0;
/** @internal */
export const DefaultMax = 99999;
/**
* Class containing the Tree of Nodes used by the FlexLayout component
*/
export class Model {
/**
* 'private' constructor. Use the static method Model.fromJson(json) to create a model
* @internal
*/
constructor() {
this.attributes = {};
this.idMap = new Map();
this.borders = new BorderSet(this);
this.windows = new Map();
this.rootWindow = new LayoutWindow(Model.MAIN_WINDOW_ID, Rect.empty());
this.windows.set(Model.MAIN_WINDOW_ID, this.rootWindow);
this.changeListeners = [];
}
/**
* 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, windowId for createWindow
*/
doAction(action) {
var _a;
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.get(action.data.toNode);
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.get(action.data.fromNode);
if (fromNode instanceof TabNode || fromNode instanceof TabSetNode || fromNode instanceof RowNode) {
if (fromNode === this.getMaximizedTabset(fromNode.getWindowId())) {
const fromWindow = this.windows.get(fromNode.getWindowId());
fromWindow.maximizedTabSet = undefined;
}
const toNode = this.idMap.get(action.data.toNode);
if (toNode instanceof TabSetNode || toNode instanceof BorderNode || toNode instanceof RowNode) {
toNode.drop(fromNode, DockLocation.getByName(action.data.location), action.data.index, action.data.select);
}
}
this.removeEmptyWindows();
break;
}
case Actions.DELETE_TAB: {
const node = this.idMap.get(action.data.node);
if (node instanceof TabNode) {
node.delete();
}
this.removeEmptyWindows();
break;
}
case Actions.DELETE_TABSET: {
const node = this.idMap.get(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.isEnableClose()) {
child.delete();
}
}
if (node.getChildren().length === 0) {
node.delete();
}
this.tidy();
}
this.removeEmptyWindows();
break;
}
case Actions.POPOUT_TABSET: {
const node = this.idMap.get(action.data.node);
if (node instanceof TabSetNode) {
const isMaximized = node.isMaximized();
const oldLayoutWindow = this.windows.get(node.getWindowId());
const windowId = randomUUID();
const layoutWindow = new LayoutWindow(windowId, oldLayoutWindow.toScreenRectFunction(node.getRect()));
const json = {
type: "row",
children: []
};
const row = RowNode.fromJson(json, this, layoutWindow);
layoutWindow.root = row;
this.windows.set(windowId, layoutWindow);
row.drop(node, DockLocation.CENTER, 0);
if (isMaximized) {
this.rootWindow.maximizedTabSet = undefined;
}
}
this.removeEmptyWindows();
break;
}
case Actions.POPOUT_TAB: {
const node = this.idMap.get(action.data.node);
if (node instanceof TabNode) {
const windowId = randomUUID();
let r = Rect.empty();
if (node.getParent() instanceof TabSetNode) {
r = node.getParent().getRect();
}
else {
r = node.getParent().getContentRect();
}
const oldLayoutWindow = this.windows.get(node.getWindowId());
const layoutWindow = new LayoutWindow(windowId, oldLayoutWindow.toScreenRectFunction(r));
const tabsetId = randomUUID();
const json = {
type: "row",
children: [
{ type: "tabset", id: tabsetId }
]
};
const row = RowNode.fromJson(json, this, layoutWindow);
layoutWindow.root = row;
this.windows.set(windowId, layoutWindow);
const tabset = this.idMap.get(tabsetId);
tabset.drop(node, DockLocation.CENTER, 0, true);
}
this.removeEmptyWindows();
break;
}
case Actions.CLOSE_WINDOW: {
const window = this.windows.get(action.data.windowId);
if (window) {
(_a = this.rootWindow.root) === null || _a === void 0 ? void 0 : _a.drop(window === null || window === void 0 ? void 0 : window.root, DockLocation.CENTER, -1);
this.rootWindow.visitNodes((node, level) => {
if (node instanceof RowNode) {
node.setWindowId(Model.MAIN_WINDOW_ID);
}
});
// this.getFirstTabSet().drop(window?.root!,DockLocation.CENTER, -1);
this.windows.delete(action.data.windowId);
}
break;
}
case Actions.CREATE_WINDOW: {
const windowId = randomUUID();
const layoutWindow = new LayoutWindow(windowId, Rect.fromJson(action.data.rect));
const row = RowNode.fromJson(action.data.layout, this, layoutWindow);
layoutWindow.root = row;
this.windows.set(windowId, layoutWindow);
returnVal = windowId;
break;
}
case Actions.RENAME_TAB: {
const node = this.idMap.get(action.data.node);
if (node instanceof TabNode) {
node.setName(action.data.text);
}
break;
}
case Actions.SELECT_TAB: {
const tabNode = this.idMap.get(action.data.tabNode);
const windowId = action.data.windowId ? action.data.windowId : Model.MAIN_WINDOW_ID;
const window = this.windows.get(windowId);
if (tabNode instanceof TabNode) {
const parent = tabNode.getParent();
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);
}
window.activeTabSet = parent;
}
}
break;
}
case Actions.SET_ACTIVE_TABSET: {
const windowId = action.data.windowId ? action.data.windowId : Model.MAIN_WINDOW_ID;
const window = this.windows.get(windowId);
if (action.data.tabsetNode === undefined) {
window.activeTabSet = undefined;
}
else {
const tabsetNode = this.idMap.get(action.data.tabsetNode);
if (tabsetNode instanceof TabSetNode) {
window.activeTabSet = tabsetNode;
}
}
break;
}
case Actions.ADJUST_WEIGHTS: {
const row = this.idMap.get(action.data.nodeId);
const c = row.getChildren();
for (let i = 0; i < c.length; i++) {
const n = c[i];
n.setWeight(action.data.weights[i]);
}
break;
}
case Actions.ADJUST_BORDER_SPLIT: {
const node = this.idMap.get(action.data.node);
if (node instanceof BorderNode) {
node.setSize(action.data.pos);
}
break;
}
case Actions.MAXIMIZE_TOGGLE: {
const windowId = action.data.windowId ? action.data.windowId : Model.MAIN_WINDOW_ID;
const window = this.windows.get(windowId);
const node = this.idMap.get(action.data.node);
if (node instanceof TabSetNode) {
if (node === window.maximizedTabSet) {
window.maximizedTabSet = undefined;
}
else {
window.maximizedTabSet = node;
window.activeTabSet = node;
}
}
break;
}
case Actions.UPDATE_MODEL_ATTRIBUTES: {
this.updateAttrs(action.data.json);
break;
}
case Actions.UPDATE_NODE_ATTRIBUTES: {
const node = this.idMap.get(action.data.node);
node.updateAttrs(action.data.json);
break;
}
default:
break;
}
this.updateIdMap();
for (const listener of this.changeListeners) {
listener(action);
}
return returnVal;
}
/**
* Get the currently active tabset node
*/
getActiveTabset(windowId = Model.MAIN_WINDOW_ID) {
const window = this.windows.get(windowId);
if (window && window.activeTabSet && this.getNodeById(window.activeTabSet.getId())) {
return window.activeTabSet;
}
else {
return undefined;
}
}
/**
* Get the currently maximized tabset node
*/
getMaximizedTabset(windowId = Model.MAIN_WINDOW_ID) {
return this.windows.get(windowId).maximizedTabSet;
}
/**
* Gets the root RowNode of the model
* @returns {RowNode}
*/
getRoot(windowId = Model.MAIN_WINDOW_ID) {
return this.windows.get(windowId).root;
}
isRootOrientationVertical() {
return this.attributes.rootOrientationVertical;
}
isEnableRotateBorderIcons() {
return this.attributes.enableRotateBorderIcons;
}
/**
* Gets the
* @returns {BorderSet|*}
*/
getBorderSet() {
return this.borders;
}
getwindowsMap() {
return this.windows;
}
/**
* 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) {
this.borders.forEachNode(fn);
for (const [_, w] of this.windows) {
w.root.forEachNode(fn, 0);
}
}
visitWindowNodes(windowId, fn) {
if (this.windows.has(windowId)) {
if (windowId === Model.MAIN_WINDOW_ID) {
this.borders.forEachNode(fn);
}
this.windows.get(windowId).visitNodes(fn);
}
}
/**
* Gets a node by its id
* @param id the id to find
*/
getNodeById(id) {
return this.idMap.get(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.windows.get(Model.MAIN_WINDOW_ID).root) {
const child = node.getChildren()[0];
if (child instanceof TabSetNode) {
return child;
}
else {
return this.getFirstTabSet(child);
}
}
/**
* Loads the model from the given json object
* @param json the json model to load
* @returns {Model} a new Model object
*/
static fromJson(json) {
const model = new Model();
Model.attributeDefinitions.fromJson(json.global, model.attributes);
if (json.borders) {
model.borders = BorderSet.fromJson(json.borders, model);
}
if (json.popouts) {
for (const windowId in json.popouts) {
const windowJson = json.popouts[windowId];
const layoutWindow = LayoutWindow.fromJson(windowJson, model, windowId);
model.windows.set(windowId, layoutWindow);
}
}
model.rootWindow.root = RowNode.fromJson(json.layout, model, model.getwindowsMap().get(Model.MAIN_WINDOW_ID));
model.tidy(); // initial tidy of node tree
return model;
}
/**
* Converts the model to a json object
* @returns {IJsonModel} json object that represents this model
*/
toJson() {
const global = {};
Model.attributeDefinitions.toJson(global, this.attributes);
// save state of nodes
this.visitNodes((node) => {
node.fireEvent("save", {});
});
const windows = {};
for (const [id, window] of this.windows) {
if (id !== Model.MAIN_WINDOW_ID) {
windows[id] = window.toJson();
}
}
return {
global,
borders: this.borders.toJson(),
layout: this.rootWindow.root.toJson(),
popouts: windows
};
}
getSplitterSize() {
return this.attributes.splitterSize;
}
getSplitterExtra() {
return this.attributes.splitterExtra;
}
isEnableEdgeDock() {
return this.attributes.enableEdgeDock;
}
isSplitterEnableHandle() {
return this.attributes.splitterEnableHandle;
}
/**
* 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) {
this.onAllowDrop = 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) {
this.onCreateTabSet = onCreateTabSet;
}
addChangeListener(listener) {
this.changeListeners.push(listener);
}
removeChangeListener(listener) {
const pos = this.changeListeners.findIndex(l => l === listener);
if (pos !== -1) {
this.changeListeners.splice(pos, 1);
}
}
toString() {
return JSON.stringify(this.toJson());
}
/***********************internal ********************************/
/** @internal */
removeEmptyWindows() {
const emptyWindows = new Set();
for (const [windowId] of this.windows) {
if (windowId !== Model.MAIN_WINDOW_ID) {
let count = 0;
this.visitWindowNodes(windowId, (node) => {
if (node instanceof TabNode) {
count++;
}
});
if (count === 0) {
emptyWindows.add(windowId);
}
}
}
for (const windowId of emptyWindows) {
this.windows.delete(windowId);
}
}
/** @internal */
setActiveTabset(tabsetNode, windowId) {
const window = this.windows.get(windowId);
if (window) {
if (tabsetNode) {
window.activeTabSet = tabsetNode;
}
else {
window.activeTabSet = undefined;
}
}
}
/** @internal */
setMaximizedTabset(tabsetNode, windowId) {
const window = this.windows.get(windowId);
if (window) {
if (tabsetNode) {
window.maximizedTabSet = tabsetNode;
}
else {
window.maximizedTabSet = undefined;
}
}
}
/** @internal */
updateIdMap() {
// regenerate idMap to stop it building up
this.idMap.clear();
this.visitNodes((node) => {
this.idMap.set(node.getId(), node);
// if (node instanceof RowNode) {
// node.normalizeWeights();
// }
});
// console.log(JSON.stringify(Object.keys(this._idMap)));
}
/** @internal */
addNode(node) {
const id = node.getId();
if (this.idMap.has(id)) {
throw new Error(`Error: each node must have a unique id, duplicate id:${node.getId()}`);
}
this.idMap.set(id, node);
}
/** @internal */
findDropTargetNode(windowId, dragNode, x, y) {
let node = this.windows.get(windowId).root.findDropTargetNode(windowId, dragNode, x, y);
if (node === undefined && windowId === Model.MAIN_WINDOW_ID) {
node = this.borders.findDropTargetNode(dragNode, x, y);
}
return node;
}
/** @internal */
tidy() {
// console.log("before _tidy", this.toString());
for (const [_, window] of this.windows) {
window.root.tidy();
}
// console.log("after _tidy", this.toString());
}
/** @internal */
updateAttrs(json) {
Model.attributeDefinitions.update(json, this.attributes);
}
/** @internal */
nextUniqueId() {
return '#' + randomUUID();
}
/** @internal */
getAttribute(name) {
return this.attributes[name];
}
/** @internal */
getOnAllowDrop() {
return this.onAllowDrop;
}
/** @internal */
getOnCreateTabSet() {
return this.onCreateTabSet;
}
static toTypescriptInterfaces() {
Model.attributeDefinitions.pairAttributes("RowNode", RowNode.getAttributeDefinitions());
Model.attributeDefinitions.pairAttributes("TabSetNode", TabSetNode.getAttributeDefinitions());
Model.attributeDefinitions.pairAttributes("TabNode", TabNode.getAttributeDefinitions());
Model.attributeDefinitions.pairAttributes("BorderNode", BorderNode.getAttributeDefinitions());
let sb = [];
sb.push(Model.attributeDefinitions.toTypescriptInterface("Global", undefined));
sb.push(RowNode.getAttributeDefinitions().toTypescriptInterface("Row", Model.attributeDefinitions));
sb.push(TabSetNode.getAttributeDefinitions().toTypescriptInterface("TabSet", Model.attributeDefinitions));
sb.push(TabNode.getAttributeDefinitions().toTypescriptInterface("Tab", Model.attributeDefinitions));
sb.push(BorderNode.getAttributeDefinitions().toTypescriptInterface("Border", Model.attributeDefinitions));
console.log(sb.join("\n"));
}
/** @internal */
static createAttributeDefinitions() {
const attributeDefinitions = new AttributeDefinitions();
attributeDefinitions.add("enableEdgeDock", true).setType(Attribute.BOOLEAN).setDescription(`enable docking to the edges of the layout, this will show the edge indicators`);
attributeDefinitions.add("rootOrientationVertical", false).setType(Attribute.BOOLEAN).setDescription(`the top level 'row' will layout horizontally by default, set this option true to make it layout vertically`);
attributeDefinitions.add("enableRotateBorderIcons", true).setType(Attribute.BOOLEAN).setDescription(`boolean indicating if tab icons should rotate with the text in the left and right borders`);
// splitter
attributeDefinitions.add("splitterSize", 8).setType(Attribute.NUMBER).setDescription(`width in pixels of all splitters between tabsets/borders`);
attributeDefinitions.add("splitterExtra", 0).setType(Attribute.NUMBER).setDescription(`additional width in pixels of the splitter hit test area`);
attributeDefinitions.add("splitterEnableHandle", false).setType(Attribute.BOOLEAN).setDescription(`enable a small centralized handle on all splitters`);
// tab
attributeDefinitions.add("tabEnableClose", true).setType(Attribute.BOOLEAN);
attributeDefinitions.add("tabCloseType", 1).setType("ICloseType");
attributeDefinitions.add("tabEnablePopout", false).setType(Attribute.BOOLEAN).setAlias("tabEnableFloat");
attributeDefinitions.add("tabEnablePopoutIcon", true).setType(Attribute.BOOLEAN);
attributeDefinitions.add("tabEnablePopoutOverlay", 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("tabSetEnableActiveIcon", false).setType(Attribute.BOOLEAN);
attributeDefinitions.add("tabSetClassNameTabStrip", undefined).setType(Attribute.STRING);
attributeDefinitions.add("tabSetEnableTabStrip", true).setType(Attribute.BOOLEAN);
attributeDefinitions.add("tabSetEnableTabWrap", false).setType(Attribute.BOOLEAN);
attributeDefinitions.add("tabSetTabLocation", "top").setType("ITabLocation");
attributeDefinitions.add("tabMinWidth", DefaultMin).setType(Attribute.NUMBER);
attributeDefinitions.add("tabMinHeight", DefaultMin).setType(Attribute.NUMBER);
attributeDefinitions.add("tabSetMinWidth", DefaultMin).setType(Attribute.NUMBER);
attributeDefinitions.add("tabSetMinHeight", DefaultMin).setType(Attribute.NUMBER);
attributeDefinitions.add("tabMaxWidth", DefaultMax).setType(Attribute.NUMBER);
attributeDefinitions.add("tabMaxHeight", DefaultMax).setType(Attribute.NUMBER);
attributeDefinitions.add("tabSetMaxWidth", DefaultMax).setType(Attribute.NUMBER);
attributeDefinitions.add("tabSetMaxHeight", DefaultMax).setType(Attribute.NUMBER);
attributeDefinitions.add("tabSetEnableTabScrollbar", false).setType(Attribute.BOOLEAN);
// border
attributeDefinitions.add("borderSize", 200).setType(Attribute.NUMBER);
attributeDefinitions.add("borderMinSize", DefaultMin).setType(Attribute.NUMBER);
attributeDefinitions.add("borderMaxSize", DefaultMax).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);
attributeDefinitions.add("borderEnableTabScrollbar", false).setType(Attribute.BOOLEAN);
return attributeDefinitions;
}
}
Model.MAIN_WINDOW_ID = "__main_window_id__";
/** @internal */
Model.attributeDefinitions = Model.createAttributeDefinitions();
//# sourceMappingURL=Model.js.map