diff --git a/src/model/BorderNode.ts b/src/model/BorderNode.ts index d27cbd6e..7fa43d24 100755 --- a/src/model/BorderNode.ts +++ b/src/model/BorderNode.ts @@ -18,7 +18,7 @@ export class BorderNode extends Node implements IDropTarget { static readonly TYPE = "border"; /** @internal */ - static fromJson(json: IJsonBorderNode, model: Model) { + static fromJson(json: IJsonBorderNode, model: Model, extant?: BorderNode) { const location = DockLocation.getByName(json.location); const border = new BorderNode(location, json, model); if (json.children) { @@ -42,9 +42,12 @@ export class BorderNode extends Node implements IDropTarget { private location: DockLocation; /** @internal */ - constructor(location: DockLocation, json: IJsonBorderNode, model: Model) { + constructor(location: DockLocation, json: IJsonBorderNode, model: Model, extant?: BorderNode) { super(model); - + if (extant) { + this.contentRect = extant.contentRect.clone(); + this.tabHeaderRect = extant.tabHeaderRect.clone(); + } this.location = location; this.attributes.id = `border_${location.getName()}`; BorderNode.attributeDefinitions.fromJson(json, this.attributes); diff --git a/src/model/BorderSet.ts b/src/model/BorderSet.ts index 2d764918..af8991a1 100755 --- a/src/model/BorderSet.ts +++ b/src/model/BorderSet.ts @@ -7,9 +7,9 @@ import { Node } from "./Node"; export class BorderSet { /** @internal */ - static fromJson(json: any, model: Model) { + static fromJson(json: any, model: Model, extant?: BorderSet) { const borderSet = new BorderSet(model); - borderSet.borders = json.map((borderJson: any) => BorderNode.fromJson(borderJson, model)); + borderSet.borders = json.map((borderJson: any, index: number) => BorderNode.fromJson(borderJson, model, extant?.borders.at(index))); for (const border of borderSet.borders) { borderSet.borderMap.set(border.getLocation(), border); } @@ -34,7 +34,7 @@ export class BorderSet { } /** @internal */ - getLayoutHorizontal () { + getLayoutHorizontal() { return this.layoutHorizontal; } @@ -58,19 +58,18 @@ export class BorderSet { } } - /** @internal */ - setPaths() { - for (const borderNode of this.borders) { - const path = "/border/" + borderNode.getLocation().getName(); - borderNode.setPath(path); - let i = 0; - for (const node of borderNode.getChildren()) { - node.setPath( path + "/t" + i); - i++; - } + /** @internal */ + setPaths() { + for (const borderNode of this.borders) { + const path = "/border/" + borderNode.getLocation().getName(); + borderNode.setPath(path); + let i = 0; + for (const node of borderNode.getChildren()) { + node.setPath(path + "/t" + i); + i++; } } - + } /** @internal */ findDropTargetNode(dragNode: Node & IDraggable, x: number, y: number): DropInfo | undefined { diff --git a/src/model/Layout.ts b/src/model/Layout.ts index 291fe36a..d8a16d89 100644 --- a/src/model/Layout.ts +++ b/src/model/Layout.ts @@ -19,7 +19,7 @@ export class Layout { private _activeTabSet?: TabSetNode | undefined; private _toExportRectFunction: (rect: Rect, type: ILayoutType) => Rect; - constructor(layoutId: string, type: ILayoutType, rect: Rect) { + constructor(layoutId: string, type: ILayoutType, rect: Rect, extant?: Layout) { this._layoutId = layoutId; this._type = type; this._rect = rect; @@ -122,13 +122,13 @@ export class Layout { return json; } - static fromJson(layoutJson: IJsonSubLayout, model: Model, layoutId: string): Layout { + static fromJson(layoutJson: IJsonSubLayout, model: Model, layoutId: string, extant?: Layout): Layout { const count = model.getLayouts().size; const rect = layoutJson.rect ? Rect.fromJson(layoutJson.rect) : new Rect(50 + 50 * count, 50 + 50 * count, 600, 400); rect.snap(10); // snapping prevents issue where window moves 1 pixel per save/restore on Chrome - const layout = new Layout(layoutId, layoutJson.type || "window", rect); - layout.setRootRow(RowNode.fromJson(layoutJson.layout, model, layout)); + const layout = new Layout(layoutId, layoutJson.type || "window", rect, extant); + layout.setRootRow(RowNode.fromJson(layoutJson.layout, model, layout, extant?.getRootRow())); return layout; } diff --git a/src/model/Model.ts b/src/model/Model.ts index a0bdb02d..4887e23a 100755 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -136,13 +136,13 @@ export class Model { const layout = new Layout(layoutId, type, oldLayout.getToExportRectFunction()(node.getRect(), type)); const json = { - type: "row" - } + type: "row", + }; const row = RowNode.fromJson(json, this, layout); layout.setRootRow(row); this.layouts.set(layoutId, layout); row.drop(node, DockLocation.CENTER, 0); - + if (isMaximized) { this.mainLayout.setMaximizedTabSet(undefined); } @@ -152,9 +152,9 @@ export class Model { case Actions.POPOUT_TAB: { const node = this.idMap.get(action.data.node); if (node instanceof TabNode) { - const layoutId = randomUUID() + const layoutId = randomUUID(); - const parent = node.getParent() as (TabSetNode | BorderNode); + const parent = node.getParent() as TabSetNode | BorderNode; const popoutRect = parent.getContentRect(); const oldLayout = node.getLayout()!; const type = action.data.type || "window"; @@ -162,14 +162,12 @@ export class Model { const tabsetId = randomUUID(); const json: IJsonRowNode = { type: "row", - children: [ - { type: "tabset", id: tabsetId } - ] - } + children: [{ type: "tabset", id: tabsetId }], + }; const row = RowNode.fromJson(json, this, layout); layout.setRootRow(row); this.layouts.set(layoutId, layout); - + const tabset = this.idMap.get(tabsetId) as TabSetNode & IDropTarget; tabset.drop(node, DockLocation.CENTER, 0, true); } @@ -311,7 +309,6 @@ export class Model { return returnVal; } - /** * Get the currently active tabset node */ @@ -392,23 +389,23 @@ export class Model { const child = node.getChildren()[0]; if (child instanceof TabSetNode) { return child; - } - else { + } 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: IJsonModel) { + * Loads the model from the given json object + * @param json the json model to load + * @param extant an optional previous model instance + * @returns {Model} a new Model object + */ + static fromJson(json: IJsonModel, extant?: Model) { const model = new Model(); Model.attributeDefinitions.fromJson(json.global, model.attributes); if (json.borders) { - model.borders = BorderSet.fromJson(json.borders, model); + model.borders = BorderSet.fromJson(json.borders, model, extant?.borders); } const subLayouts = json.subLayouts || json.popouts; @@ -420,7 +417,7 @@ export class Model { model.layouts.set(layoutId, layout); } } - model.mainLayout.setRootRow(RowNode.fromJson(json.layout, model, model.mainLayout)); + model.mainLayout.setRootRow(RowNode.fromJson(json.layout, model, model.mainLayout, extant?.mainLayout.getRootRow())); model.tidy(); // initial tidy of node tree return model; } @@ -449,7 +446,7 @@ export class Model { global, borders: this.borders.toJson(), layout: this.mainLayout.getRootRow()!.toJson(), - subLayouts: subLayouts + subLayouts: subLayouts, }; } @@ -481,18 +478,18 @@ export class Model { * 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 + * @param onCreateTabSet */ setOnCreateTabSet(onCreateTabSet: (tabNode?: TabNode) => ITabSetAttributes) { this.onCreateTabSet = onCreateTabSet; } - addChangeListener(listener: ((action: Action) => void)) { + addChangeListener(listener: (action: Action) => void) { this.changeListeners.push(listener); } - removeChangeListener(listener: ((action: Action) => void)) { - const pos = this.changeListeners.findIndex(l => l === listener); + removeChangeListener(listener: (action: Action) => void) { + const pos = this.changeListeners.findIndex((l) => l === listener); if (pos !== -1) { this.changeListeners.splice(pos, 1); } @@ -522,7 +519,7 @@ export class Model { return priority[a.getType()] - priority[b.getType()]; }); this.layouts.clear(); - sorted.forEach(layout => this.layouts.set(layout.getLayoutId(), layout)); + sorted.forEach((layout) => this.layouts.set(layout.getLayoutId(), layout)); } /** @internal */ @@ -538,7 +535,7 @@ export class Model { } /** @internal */ - setMaximizedTabset(tabsetNode: (TabSetNode | undefined), layoutId: string) { + setMaximizedTabset(tabsetNode: TabSetNode | undefined, layoutId: string) { const layout = this.layouts.get(layoutId); if (layout) { if (tabsetNode) { @@ -554,7 +551,7 @@ export class Model { // regenerate idMap to stop it building up this.idMap.clear(); this.visitNodes((node) => { - this.idMap.set(node.getId(), node) + this.idMap.set(node.getId(), node); // if (node instanceof RowNode) { // node.normalizeWeights(); // } @@ -597,7 +594,7 @@ export class Model { /** @internal */ nextUniqueId() { - return '#' + randomUUID(); + return "#" + randomUUID(); } /** @internal */ @@ -634,18 +631,16 @@ export class Model { private static createAttributeDefinitions(): Attributes { const attributeDefinitions = new Attributes(); - attributeDefinitions.add("enableEdgeDock", true).setType(Attribute.BOOLEAN).setDescription( - `enable docking to the edges of the layout` - ); - attributeDefinitions.add("enableEdgeDockIndicators", true).setType(Attribute.BOOLEAN).setDescription( - `show the edge indicators when dragging` - ); - 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` - ); + attributeDefinitions.add("enableEdgeDock", true).setType(Attribute.BOOLEAN).setDescription(`enable docking to the edges of the layout`); + attributeDefinitions.add("enableEdgeDockIndicators", true).setType(Attribute.BOOLEAN).setDescription(`show the edge indicators when dragging`); + 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`); // tab attributeDefinitions.add("tabEnableClose", true).setType(Attribute.BOOLEAN); diff --git a/src/model/RowNode.ts b/src/model/RowNode.ts index 719e7314..c2ec5577 100755 --- a/src/model/RowNode.ts +++ b/src/model/RowNode.ts @@ -19,18 +19,23 @@ export class RowNode extends Node implements IDropTarget { static readonly TYPE = "row"; /** @internal */ - static fromJson(json: IJsonRowNode, model: Model, layout: Layout) { - const newLayoutNode = new RowNode(model, json); + static fromJson(json: IJsonRowNode, model: Model, layout: Layout, extant?: RowNode) { + const newLayoutNode = new RowNode(model, json, extant); if (json.children != null) { + let index = 0; for (const jsonChild of json.children) { + const extantChildUnknown = extant?.children.at(index); if (jsonChild.type === TabSetNode.TYPE) { - const child = TabSetNode.fromJson(jsonChild as IJsonTabSetNode, model, layout); + const extantChild = extantChildUnknown?.getType() === TabSetNode.TYPE ? (extantChildUnknown as TabSetNode) : undefined; + const child = TabSetNode.fromJson(jsonChild as IJsonTabSetNode, model, layout, extantChild); newLayoutNode.addChild(child); } else if (jsonChild.type === RowNode.TYPE) { - const child = RowNode.fromJson(jsonChild as IJsonRowNode, model, layout); + const extantChild = extantChildUnknown?.getType() === RowNode.TYPE ? (extantChildUnknown as RowNode) : undefined; + const child = RowNode.fromJson(jsonChild as IJsonRowNode, model, layout, extantChild); newLayoutNode.addChild(child); } + index++; } } @@ -52,13 +57,21 @@ export class RowNode extends Node implements IDropTarget { private maxWidth: number; /** @internal */ - constructor(model: Model, json: IJsonRowNode) { + constructor(model: Model, json: IJsonRowNode, extant?: RowNode) { super(model); - this.minHeight = DefaultMin; - this.minWidth = DefaultMin; - this.maxHeight = DefaultMax; - this.maxWidth = DefaultMax; + if (extant) { + this.minHeight = extant.minHeight; + this.minWidth = extant.minWidth; + this.maxHeight = extant.maxHeight; + this.maxWidth = extant.maxWidth; + } else { + this.minHeight = DefaultMin; + this.minWidth = DefaultMin; + this.maxHeight = DefaultMax; + this.maxWidth = DefaultMax; + } + RowNode.attributeDefinitions.fromJson(json, this.attributes); this.normalizeWeights(); model.addNode(this); @@ -74,7 +87,7 @@ export class RowNode extends Node implements IDropTarget { json.children = []; for (const child of this.children) { - json.children.push((child as (RowNode | TabSetNode)).toJson()); + json.children.push((child as RowNode | TabSetNode).toJson()); } return json; @@ -101,7 +114,7 @@ export class RowNode extends Node implements IDropTarget { /** @internal */ getSplitterBounds(index: number) { const h = this.getOrientation() === Orientation.HORZ; - const c = this.getChildren() + const c = this.getChildren(); const ss = this.model.getSplitterSize()!; const fr = c[0].getRect(); const lr = c[c.length - 1].getRect(); @@ -146,7 +159,7 @@ export class RowNode extends Node implements IDropTarget { sum += s; } - const startRect = c[index].getRect() + const startRect = c[index].getRect(); const startPosition = (h ? startRect.x : startRect.y) - ss; return { initialSizes, sum, startPosition }; @@ -161,7 +174,8 @@ export class RowNode extends Node implements IDropTarget { const sizes = [...initialSizes]; - if (splitterPos < startPosition) { // moved left + if (splitterPos < startPosition) { + // moved left let shift = startPosition - splitterPos; let altShift = 0; if (sizes[index] + shift > smax) { @@ -194,8 +208,6 @@ export class RowNode extends Node implements IDropTarget { sizes[i] = m; } } - - } else { let shift = splitterPos - startPosition; let altShift = 0; @@ -232,7 +244,7 @@ export class RowNode extends Node implements IDropTarget { } // 0.1 is to prevent weight ever going to zero - const weights = sizes.map(s => Math.max(0.1, s) * 100 / sum); + const weights = sizes.map((s) => (Math.max(0.1, s) * 100) / sum); // console.log(splitterPos, startPosition, "sizes", sizes); // console.log("weights",weights); @@ -372,7 +384,6 @@ export class RowNode extends Node implements IDropTarget { this.addChild(child); } } - } /** @internal */ @@ -448,14 +459,13 @@ export class RowNode extends Node implements IDropTarget { if (dragNode instanceof TabSetNode || dragNode instanceof RowNode) { node = dragNode; // need to turn round if same orientation unless docking oposite direction - if (node instanceof RowNode && node.getOrientation() === this.getOrientation() && - (location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) { + if (node instanceof RowNode && node.getOrientation() === this.getOrientation() && (location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) { node = new RowNode(this.model, {}); node.addChild(dragNode); } } else { const callback = this.model.getOnCreateTabSet(); - const json : ITabAttributes = callback ? callback(dragNode as TabNode) : {}; + const json: ITabAttributes = callback ? callback(dragNode as TabNode) : {}; node = new TabSetNode(this.model, json); node.addChild(dragNode); } @@ -476,11 +486,11 @@ export class RowNode extends Node implements IDropTarget { } else { this.addChild(node, index); } - } else if (horz && dockLocation === DockLocation.LEFT || !horz && dockLocation === DockLocation.TOP) { + } else if ((horz && dockLocation === DockLocation.LEFT) || (!horz && dockLocation === DockLocation.TOP)) { this.addChild(node, 0); - } else if (horz && dockLocation === DockLocation.RIGHT || !horz && dockLocation === DockLocation.BOTTOM) { + } else if ((horz && dockLocation === DockLocation.RIGHT) || (!horz && dockLocation === DockLocation.BOTTOM)) { this.addChild(node); - } else if (horz && dockLocation === DockLocation.TOP || !horz && dockLocation === DockLocation.LEFT) { + } else if ((horz && dockLocation === DockLocation.TOP) || (!horz && dockLocation === DockLocation.LEFT)) { const vrow = new RowNode(this.model, {}); const hrow = new RowNode(this.model, {}); hrow.setWeight(75); @@ -492,7 +502,7 @@ export class RowNode extends Node implements IDropTarget { vrow.addChild(node); vrow.addChild(hrow); this.addChild(vrow); - } else if (horz && dockLocation === DockLocation.BOTTOM || !horz && dockLocation === DockLocation.RIGHT) { + } else if ((horz && dockLocation === DockLocation.BOTTOM) || (!horz && dockLocation === DockLocation.RIGHT)) { const vrow = new RowNode(this.model, {}); const hrow = new RowNode(this.model, {}); hrow.setWeight(75); @@ -513,8 +523,6 @@ export class RowNode extends Node implements IDropTarget { this.model.tidy(); } - - /** @internal */ isEnableDrop() { return true; @@ -535,12 +543,11 @@ export class RowNode extends Node implements IDropTarget { return RowNode.attributeDefinitions; } - - // NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize + // NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize normalizeWeights() { let sum = 0; for (const n of this.children) { - const node = (n as TabSetNode | RowNode); + const node = n as TabSetNode | RowNode; sum += node.getWeight(); } @@ -549,8 +556,8 @@ export class RowNode extends Node implements IDropTarget { } for (const n of this.children) { - const node = (n as TabSetNode | RowNode); - node.setWeight(Math.max(0.001, 100 * node.getWeight() / sum)); + const node = n as TabSetNode | RowNode; + node.setWeight(Math.max(0.001, (100 * node.getWeight()) / sum)); } } @@ -558,12 +565,8 @@ export class RowNode extends Node implements IDropTarget { private static createAttributeDefinitions(): Attributes { const attributeDefinitions = new Attributes(); attributeDefinitions.add("type", RowNode.TYPE, true).setType(Attribute.STRING).setFixed(); - attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription( - `the unique id of the row, if left undefined a uuid will be assigned` - ); - attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription( - `relative weight for sizing of this row in parent row` - ); + attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription(`the unique id of the row, if left undefined a uuid will be assigned`); + attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription(`relative weight for sizing of this row in parent row`); return attributeDefinitions; } diff --git a/src/model/TabNode.ts b/src/model/TabNode.ts index 71bc4aef..8b7e1c00 100755 --- a/src/model/TabNode.ts +++ b/src/model/TabNode.ts @@ -13,8 +13,8 @@ export class TabNode extends Node implements IDraggable { static readonly TYPE = "tab"; /** @internal */ - static fromJson(json: IJsonTabNode, model: Model, addToModel: boolean = true) { - const newLayoutNode = new TabNode(model, json, addToModel); + static fromJson(json: IJsonTabNode, model: Model, addToModel: boolean = true, extant?: TabNode) { + const newLayoutNode = new TabNode(model, json, addToModel, extant); return newLayoutNode; } @@ -30,15 +30,25 @@ export class TabNode extends Node implements IDraggable { private scrollLeft?: number; /** @internal */ - constructor(model: Model, json: IJsonTabNode, addToModel: boolean = true) { + constructor(model: Model, json: IJsonTabNode, addToModel: boolean = true, extant?: TabNode) { super(model); - this.extra = {}; // extra data added to node not saved in json - this.moveableElement = document.createElement("div"); - this.moveableElement.className = CLASSES.FLEXLAYOUT__TAB_MOVEABLE; - this.tabStamp = null; - this.rendered = false; - this.visible = false; + if (extant === undefined) { + this.extra = {}; // extra data added to node not saved in json + this.moveableElement = document.createElement("div"); + this.moveableElement.className = CLASSES.FLEXLAYOUT__TAB_MOVEABLE; + this.tabStamp = null; + this.rendered = false; + this.visible = false; + } else { + this.extra = { ...extant.extra }; + this.moveableElement = extant.moveableElement; + this.tabStamp = extant.tabStamp; + this.rendered = extant.rendered; + this.visible = extant.visible; + this.scrollTop = extant.scrollTop; + this.scrollLeft = extant.scrollLeft; + } TabNode.attributeDefinitions.fromJson(json, this.attributes); if (addToModel === true) { @@ -49,7 +59,7 @@ export class TabNode extends Node implements IDraggable { getName() { return this.getAttr("name") as string; } - + getIcon() { return this.getAttr("icon") as string | undefined; } @@ -100,7 +110,7 @@ export class TabNode extends Node implements IDraggable { isSelected() { return (this.getParent() as TabSetNode | BorderNode).getSelectedNode() === this; } - + isCloseable() { let closeable = this.isEnableClose(); if (closeable && this.getSubLayoutId()) { @@ -118,7 +128,7 @@ export class TabNode extends Node implements IDraggable { } return allowed; } - + isEnableClose() { return this.getAttr("enableClose") as boolean; } @@ -350,90 +360,59 @@ export class TabNode extends Node implements IDraggable { private static createAttributeDefinitions(): Attributes { const attributeDefinitions = new Attributes(); attributeDefinitions.add("type", TabNode.TYPE, true).setType(Attribute.STRING).setFixed(); - attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription( - `the unique id of the tab, if left undefined a uuid will be assigned` - ); - attributeDefinitions.add("name", "[Unnamed Tab]").setType(Attribute.STRING).setDescription( - `name of tab to be displayed in the tab button` - ); - attributeDefinitions.add("component", undefined).setType(Attribute.STRING).setDescription( - `string identifying which component to render in this tab (used in the layout factory function)` - ); - attributeDefinitions.add("subLayoutId", undefined).setType(Attribute.STRING).setDescription( - `the Id of the sub layout to render in this tab, defined in the subLayouts section of the model json (if - component is also defined then use the component in the factory to render the sublayout)` - ); - attributeDefinitions.add("altName", undefined).setType(Attribute.STRING).setDescription( - `if there is no name specifed then this value will be used in the overflow menu` - ); - attributeDefinitions.add("helpText", undefined).setType(Attribute.STRING).setDescription( - `help text for the tab to be displayed upon tab hover.` - ); - attributeDefinitions.add("config", undefined).setType("any").setDescription( - `a place to hold json config for the hosted component` - ); - attributeDefinitions.add("tabsetClassName", undefined).setType(Attribute.STRING).setDescription( - `class applied to parent tabset when this is the only tab and it is stretched to fill the tabset` - ); - attributeDefinitions.add("enableWindowReMount", false).setType(Attribute.BOOLEAN).setDescription( - `if enabled the tab will re-mount when popped out/in` - ); - attributeDefinitions.addInherited("enableClose", "tabEnableClose").setType(Attribute.BOOLEAN).setDescription( - `allow user to close tab via close button` - ); - attributeDefinitions.addInherited("closeType", "tabCloseType").setType("ICloseType").setDescription( - `see values in ICloseType` - ); - attributeDefinitions.addInherited("enableDrag", "tabEnableDrag").setType(Attribute.BOOLEAN).setDescription( - `allow user to drag tab to new location` - ); - attributeDefinitions.addInherited("enableRename", "tabEnableRename").setType(Attribute.BOOLEAN).setDescription( - `allow user to rename tabs by double clicking` - ); - attributeDefinitions.addInherited("className", "tabClassName").setType(Attribute.STRING).setDescription( - `class applied to tab button` - ); - attributeDefinitions.addInherited("contentClassName", "tabContentClassName").setType(Attribute.STRING).setDescription( - `class applied to tab content` - ); - attributeDefinitions.addInherited("icon", "tabIcon").setType(Attribute.STRING).setDescription( - `the tab icon` - ); - attributeDefinitions.addInherited("enableRenderOnDemand", "tabEnableRenderOnDemand").setType(Attribute.BOOLEAN).setDescription( - `whether to avoid rendering component until tab is visible` - ); - attributeDefinitions.addInherited("enablePopout", "tabEnablePopout").setType(Attribute.BOOLEAN).setAlias("enableFloat").setDescription( - `enable window popout (in popout capable browser), to show an icon in the tabset header also set the enablePopoutIcon attribute` - ); - attributeDefinitions.addInherited("enablePopoutIcon", "tabEnablePopoutIcon").setType(Attribute.BOOLEAN).setDescription( - `whether to show the popout icon in the tabset header if this tab enables popouts` - ); - attributeDefinitions.addInherited("enablePopoutFloatIcon", "tabEnablePopoutFloatIcon").setType(Attribute.BOOLEAN).setDescription( - `whether to show the popout float icon in the tabset header if this tab enables floating popouts` - ); - attributeDefinitions.addInherited("enablePopoutOverlay", "tabEnablePopoutOverlay").setType(Attribute.BOOLEAN).setDescription( - `if this tab will not work correctly in a popout window when the main window is backgrounded (inactive) - then enabling this option will gray out this tab` - ); - - attributeDefinitions.addInherited("borderWidth", "tabBorderWidth").setType(Attribute.NUMBER).setDescription( - `width when added to border, -1 will use border size` - ); - attributeDefinitions.addInherited("borderHeight", "tabBorderHeight").setType(Attribute.NUMBER).setDescription( - `height when added to border, -1 will use border size` - ); - attributeDefinitions.addInherited("minWidth", "tabMinWidth").setType(Attribute.NUMBER).setDescription( - `the min width of this tab` - ); - attributeDefinitions.addInherited("minHeight", "tabMinHeight").setType(Attribute.NUMBER).setDescription( - `the min height of this tab` - ); - attributeDefinitions.addInherited("maxWidth", "tabMaxWidth").setType(Attribute.NUMBER).setDescription( - `the max width of this tab` - ); - attributeDefinitions.addInherited("maxHeight", "tabMaxHeight").setType(Attribute.NUMBER).setDescription( - `the max height of this tab` - ); + attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription(`the unique id of the tab, if left undefined a uuid will be assigned`); + attributeDefinitions.add("name", "[Unnamed Tab]").setType(Attribute.STRING).setDescription(`name of tab to be displayed in the tab button`); + attributeDefinitions.add("component", undefined).setType(Attribute.STRING).setDescription(`string identifying which component to render in this tab (used in the layout factory function)`); + attributeDefinitions + .add("subLayoutId", undefined) + .setType(Attribute.STRING) + .setDescription( + `the Id of the sub layout to render in this tab, defined in the subLayouts section of the model json (if + component is also defined then use the component in the factory to render the sublayout)`, + ); + attributeDefinitions.add("altName", undefined).setType(Attribute.STRING).setDescription(`if there is no name specifed then this value will be used in the overflow menu`); + attributeDefinitions.add("helpText", undefined).setType(Attribute.STRING).setDescription(`help text for the tab to be displayed upon tab hover.`); + attributeDefinitions.add("config", undefined).setType("any").setDescription(`a place to hold json config for the hosted component`); + attributeDefinitions + .add("tabsetClassName", undefined) + .setType(Attribute.STRING) + .setDescription(`class applied to parent tabset when this is the only tab and it is stretched to fill the tabset`); + attributeDefinitions.add("enableWindowReMount", false).setType(Attribute.BOOLEAN).setDescription(`if enabled the tab will re-mount when popped out/in`); + attributeDefinitions.addInherited("enableClose", "tabEnableClose").setType(Attribute.BOOLEAN).setDescription(`allow user to close tab via close button`); + attributeDefinitions.addInherited("closeType", "tabCloseType").setType("ICloseType").setDescription(`see values in ICloseType`); + attributeDefinitions.addInherited("enableDrag", "tabEnableDrag").setType(Attribute.BOOLEAN).setDescription(`allow user to drag tab to new location`); + attributeDefinitions.addInherited("enableRename", "tabEnableRename").setType(Attribute.BOOLEAN).setDescription(`allow user to rename tabs by double clicking`); + attributeDefinitions.addInherited("className", "tabClassName").setType(Attribute.STRING).setDescription(`class applied to tab button`); + attributeDefinitions.addInherited("contentClassName", "tabContentClassName").setType(Attribute.STRING).setDescription(`class applied to tab content`); + attributeDefinitions.addInherited("icon", "tabIcon").setType(Attribute.STRING).setDescription(`the tab icon`); + attributeDefinitions.addInherited("enableRenderOnDemand", "tabEnableRenderOnDemand").setType(Attribute.BOOLEAN).setDescription(`whether to avoid rendering component until tab is visible`); + attributeDefinitions + .addInherited("enablePopout", "tabEnablePopout") + .setType(Attribute.BOOLEAN) + .setAlias("enableFloat") + .setDescription(`enable window popout (in popout capable browser), to show an icon in the tabset header also set the enablePopoutIcon attribute`); + attributeDefinitions + .addInherited("enablePopoutIcon", "tabEnablePopoutIcon") + .setType(Attribute.BOOLEAN) + .setDescription(`whether to show the popout icon in the tabset header if this tab enables popouts`); + attributeDefinitions + .addInherited("enablePopoutFloatIcon", "tabEnablePopoutFloatIcon") + .setType(Attribute.BOOLEAN) + .setDescription(`whether to show the popout float icon in the tabset header if this tab enables floating popouts`); + attributeDefinitions + .addInherited("enablePopoutOverlay", "tabEnablePopoutOverlay") + .setType(Attribute.BOOLEAN) + .setDescription( + `if this tab will not work correctly in a popout window when the main window is backgrounded (inactive) + then enabling this option will gray out this tab`, + ); + + attributeDefinitions.addInherited("borderWidth", "tabBorderWidth").setType(Attribute.NUMBER).setDescription(`width when added to border, -1 will use border size`); + attributeDefinitions.addInherited("borderHeight", "tabBorderHeight").setType(Attribute.NUMBER).setDescription(`height when added to border, -1 will use border size`); + attributeDefinitions.addInherited("minWidth", "tabMinWidth").setType(Attribute.NUMBER).setDescription(`the min width of this tab`); + attributeDefinitions.addInherited("minHeight", "tabMinHeight").setType(Attribute.NUMBER).setDescription(`the min height of this tab`); + attributeDefinitions.addInherited("maxWidth", "tabMaxWidth").setType(Attribute.NUMBER).setDescription(`the max width of this tab`); + attributeDefinitions.addInherited("maxHeight", "tabMaxHeight").setType(Attribute.NUMBER).setDescription(`the max height of this tab`); return attributeDefinitions; } diff --git a/src/model/TabSetNode.ts b/src/model/TabSetNode.ts index c129187e..f7d9e32a 100755 --- a/src/model/TabSetNode.ts +++ b/src/model/TabSetNode.ts @@ -21,25 +21,51 @@ export class TabSetNode extends Node implements IDraggable, IDropTarget { static readonly TYPE = "tabset"; /** @internal */ - static fromJson(json: IJsonTabSetNode, model: Model, layout: Layout) { - const newLayoutNode = new TabSetNode(model, json); + static fromJson(json: IJsonTabSetNode, model: Model, layout: Layout, extant?: TabSetNode) { + const newLayoutNode = new TabSetNode(model, json, extant); if (json.children != null) { + let index = 0; for (const jsonChild of json.children) { + const prevChildUnknown = extant?.children.at(index); + let prevChild: TabNode | undefined; + if ( + prevChildUnknown !== undefined && + prevChildUnknown.getType() === TabNode.TYPE + ) { + // Do the cast here so we can verify the component matches the previous component + // mounted in the child node for re-use. + const prevChildAsTabNode = prevChildUnknown as TabNode; + if (prevChildAsTabNode.getComponent() === jsonChild.component) { + prevChild = prevChildAsTabNode; + } + } const child = TabNode.fromJson(jsonChild, model); newLayoutNode.addChild(child); + index++; } } - if (newLayoutNode.children.length === 0) { - newLayoutNode.setSelected(-1); - } - if (json.maximized && json.maximized === true) { - layout.setMaximizedTabSet(newLayoutNode); - } + if (extant === undefined) { + if (newLayoutNode.children.length === 0) { + newLayoutNode.setSelected(-1); + } + + if (json.maximized && json.maximized === true) { + layout.setMaximizedTabSet(newLayoutNode); + } - if (json.active && json.active === true) { - layout.setActiveTabSet(newLayoutNode); + if (json.active && json.active === true) { + layout.setActiveTabSet(newLayoutNode); + } + } else { + newLayoutNode.setSelected(extant.getSelected()); + if (layout.getMaximizedTabSet() === extant) { + layout.setMaximizedTabSet(newLayoutNode); + } + if (layout.getMaximizedTabSet() === extant) { + layout.setMaximizedTabSet(newLayoutNode); + } } return newLayoutNode; @@ -54,12 +80,20 @@ export class TabSetNode extends Node implements IDraggable, IDropTarget { private calculatedMaxWidth: number; /** @internal */ - constructor(model: Model, json: IJsonTabSetNode) { + constructor(model: Model, json: IJsonTabSetNode, extant?: TabSetNode) { super(model); - this.calculatedMinHeight = 0; - this.calculatedMinWidth = 0; - this.calculatedMaxHeight = 0; - this.calculatedMaxWidth = 0; + + if (extant === undefined) { + this.calculatedMinHeight = 0; + this.calculatedMinWidth = 0; + this.calculatedMaxHeight = 0; + this.calculatedMaxWidth = 0; + } else { + this.calculatedMinHeight = extant.calculatedMinHeight; + this.calculatedMinWidth = extant.calculatedMinWidth; + this.calculatedMaxHeight = extant.calculatedMaxHeight; + this.calculatedMaxWidth = extant.calculatedMaxWidth; + } TabSetNode.attributeDefinitions.fromJson(json, this.attributes); model.addNode(this);