From ff21e9623f834af67694197e42fe7563982869e0 Mon Sep 17 00:00:00 2001 From: nmd Date: Thu, 14 Oct 2021 15:30:18 +0100 Subject: [PATCH] Border autoHide attribute --- ChangeLog.txt | 1 + README.md | 2 + examples/demo/App.tsx | 19 ++++++---- examples/demo/layouts/newfeatures.layout | 1 + examples/demo/newfeatures.html | 21 ++++++++++- src/Rect.ts | 8 +++- src/model/BorderNode.ts | 15 +++++++- src/model/BorderSet.ts | 32 ++++++++-------- src/model/IJsonModel.ts | 2 + src/model/Model.ts | 16 ++++++++ src/view/Layout.tsx | 47 +++++++++++++++++++++++- 11 files changed, 134 insertions(+), 30 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index a12141b7..59c0db5b 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,5 +1,6 @@ 0.5.18 Added onRenderDragRect callback prop for rendering the drag rectangles +New border attribute: enableAutoHide, to hide border if it has zero tabs 0.5.17 New global option, splitterExtra, to allow splitters to have an extended hit test areas. diff --git a/README.md b/README.md index d18f106f..1e5d570a 100755 --- a/README.md +++ b/README.md @@ -349,6 +349,7 @@ Attributes allowed in the 'global' element | tabSetHeaderHeight | 0 | height of tabset header in pixels; if left as 0 then the value will be calculated from the current fontSize | | tabSetTabStripHeight | 0 | height of tabset tab bar in pixels; if left as 0 then the value will be calculated from the current fontSize | | borderBarSize | 0 | size of the border bars in pixels; if left as 0 then the value will be calculated from the current fontSize | +| borderEnableAutoHide | false | hide border if it has zero tabs | | borderEnableDrop | true | allow user to drag tabs into this border | | borderAutoSelectTabWhenOpen | true | whether to select new/moved tabs in border when the border is already open | | borderAutoSelectTabWhenClosed | false | whether to select new/moved tabs in border when the border is curently closed | @@ -456,6 +457,7 @@ Inherited defaults will take their value from the associated global attributes ( | id | auto generated | border_ + border name e.g. border_left | | config | null | a place to hold json config used in your own code | | show | true | show/hide this border | +| enableAutoHide | false | hide border if it has zero tabs | | children | *required* | a list of tab nodes | | barSize | *inherited* | size of this border's bar in pixels; if left as 0 then the value will be calculated from the current fontSize | | enableDrop | *inherited* | | diff --git a/examples/demo/App.tsx b/examples/demo/App.tsx index c8620a5d..f6da3146 100755 --- a/examples/demo/App.tsx +++ b/examples/demo/App.tsx @@ -143,12 +143,17 @@ class App extends React.Component { - if (node !== undefined) { - return
{JSON.stringify(node?.toJson())}
- } else if (json !== undefined) { - return
{JSON.stringify(json)}
+ if (this.state.layoutFile === "newfeatures") { + return ( +
{text} +

+ This is a customized
+ drag rectangle +
+ ); + } else { + return undefined; // use default rendering } - return undefined; } onExternalDrag = (e: React.DragEvent) => { @@ -348,7 +353,7 @@ class App extends React.Component { @@ -635,7 +640,7 @@ function TabStorage({ tab, layout }: { tab: TabNode, layout: FlexLayout.Layout } if (absY - rootY < tabRect.height / 5) { setScrollDown(false) - } else if (absY - rootY > tabRect.height * 4/5) { + } else if (absY - rootY > tabRect.height * 4 / 5) { setScrollDown(true) } else { setScrollDown(null) diff --git a/examples/demo/layouts/newfeatures.layout b/examples/demo/layouts/newfeatures.layout index fe885a96..8235bd69 100755 --- a/examples/demo/layouts/newfeatures.layout +++ b/examples/demo/layouts/newfeatures.layout @@ -4,6 +4,7 @@ "tabSetMinHeight":100, "tabSetMinWidth":100, "borderMinSize":100, + "borderEnableAutoHide": true, "tabSetEnableClose":true, "splitterSize":1, "splitterExtra":4 diff --git a/examples/demo/newfeatures.html b/examples/demo/newfeatures.html index d3009b04..a309eb9b 100644 --- a/examples/demo/newfeatures.html +++ b/examples/demo/newfeatures.html @@ -5,9 +5,20 @@ + - +
  • @@ -30,6 +41,14 @@ Tab attributes: borderWidth, borderHeight to allow tabs to have individual sizes in borders:
    Try the 'With border sizes' tab
  • +
  • + Customize the drag rectangle using the callback property: onRenderDragRect
    + In this layout all drag rectangles are custom rendered +
  • +
  • + New border attribute: enableAutoHide, to hide border if it has zero tabs:
    + Try moving all tabs from any of the borders +
diff --git a/src/Rect.ts b/src/Rect.ts index 0ec5c7aa..057d7e7e 100755 --- a/src/Rect.ts +++ b/src/Rect.ts @@ -18,8 +18,8 @@ class Rect { } static fromElement(element: Element) { - let {x, y, width, height} = element.getBoundingClientRect(); - return new Rect(x, y, width, height); + let { x, y, width, height } = element.getBoundingClientRect(); + return new Rect(x, y, width, height); } clone() { @@ -42,6 +42,10 @@ class Rect { return this.x + this.width; } + getCenter() { + return { x: this.x + this.width / 2, y: this.y + this.height / 2 }; + } + positionElement(element: HTMLElement, position?: string) { this.styleWithPosition(element.style, position); } diff --git a/src/model/BorderNode.ts b/src/model/BorderNode.ts index 7b84e234..72f006e3 100755 --- a/src/model/BorderNode.ts +++ b/src/model/BorderNode.ts @@ -50,6 +50,7 @@ class BorderNode extends Node implements IDropTarget { attributeDefinitions.addInherited("autoSelectTabWhenClosed", "borderAutoSelectTabWhenClosed").setType(Attribute.BOOLEAN); attributeDefinitions.addInherited("size", "borderSize").setType(Attribute.NUMBER); attributeDefinitions.addInherited("minSize", "borderMinSize").setType(Attribute.NUMBER); + attributeDefinitions.addInherited("enableAutoHide", "borderEnableAutoHide").setType(Attribute.BOOLEAN); return attributeDefinitions; } @@ -178,7 +179,19 @@ class BorderNode extends Node implements IDropTarget { } isShowing() { - return this._attributes.show as boolean; + const show = this._attributes.show as boolean; + if (show) { + if (this._model._getShowHiddenBorder() !== this._location && this.isAutoHide() && this._children.length === 0) { + return false; + } + return true; + } else { + return false; + } + } + + isAutoHide() { + return this._getAttr("enableAutoHide") as boolean; } /** @hidden @internal */ diff --git a/src/model/BorderSet.ts b/src/model/BorderSet.ts index 8d930e1e..8ca0708b 100755 --- a/src/model/BorderSet.ts +++ b/src/model/BorderSet.ts @@ -58,23 +58,21 @@ class BorderSet { // sum size of borders to see they will fit for (const border of showingBorders) { - if (border.isShowing()) { - border._setAdjustedSize(border.getSize()); - const visible = border.getSelected() !== -1; - if (border.getLocation().getOrientation() === Orientation.HORZ) { - sumWidth += border.getBorderBarSize() ; - if (visible) { - width -= this._model.getSplitterSize(); - sumWidth += border.getSize(); - adjustableWidth += border.getSize(); - } - } else { - sumHeight += border.getBorderBarSize(); - if (visible) { - height -= this._model.getSplitterSize(); - sumHeight += border.getSize(); - adjustableHeight += border.getSize(); - } + border._setAdjustedSize(border.getSize()); + const visible = border.getSelected() !== -1; + if (border.getLocation().getOrientation() === Orientation.HORZ) { + sumWidth += border.getBorderBarSize() ; + if (visible) { + width -= this._model.getSplitterSize(); + sumWidth += border.getSize(); + adjustableWidth += border.getSize(); + } + } else { + sumHeight += border.getBorderBarSize(); + if (visible) { + height -= this._model.getSplitterSize(); + sumHeight += border.getSize(); + adjustableHeight += border.getSize(); } } } diff --git a/src/model/IJsonModel.ts b/src/model/IJsonModel.ts index 3ed8e83f..0228e500 100755 --- a/src/model/IJsonModel.ts +++ b/src/model/IJsonModel.ts @@ -35,6 +35,7 @@ export interface IGlobalAttributes { borderAutoSelectTabWhenOpen?: boolean; // default: true borderBarSize?: number; // default: 0 borderClassName?: string; + borderEnableAutoHide?: boolean; // default: false borderEnableDrop?: boolean; // default: true borderMinSize?: number; // default: 0 borderSize?: number; // default: 200 @@ -131,6 +132,7 @@ export interface IBorderAttributes { barSize?: number; // default: 0 - inherited from global borderBarSize className?: string; // - inherited from global borderClassName config?: any; + enableAutoHide?: boolean; // default: false - inherited from global borderEnableAutoHide enableDrop?: boolean; // default: true - inherited from global borderEnableDrop minSize?: number; // default: 0 - inherited from global borderMinSize selected?: number; // default: -1 diff --git a/src/model/Model.ts b/src/model/Model.ts index dce51080..e4a3c947 100755 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -102,6 +102,8 @@ class Model { 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; } @@ -127,6 +129,9 @@ class Model { private _pointerFine: boolean; /** @hidden @internal */ private _onCreateTabSet? : (tabNode?: TabNode) => ITabSetAttributes; + /** @hidden @internal */ + private _showHiddenBorder: DockLocation; + /** * 'private' constructor. Use the static method Model.fromJson(json) to create a model @@ -138,6 +143,7 @@ class Model { this._idMap = {}; this._borders = new BorderSet(this); this._pointerFine = true; + this._showHiddenBorder = DockLocation.CENTER; } /** @hidden @internal */ @@ -156,6 +162,16 @@ class Model { } } + /** @hidden @internal */ + _getShowHiddenBorder() { + return this._showHiddenBorder; + } + + /** @hidden @internal */ + _setShowHiddenBorder(location: DockLocation) { + this._showHiddenBorder = location; + } + /** @hidden @internal */ _setActiveTabset(tabsetNode: TabSetNode | undefined) { this._activeTabSet = tabsetNode; diff --git a/src/view/Layout.tsx b/src/view/Layout.tsx index a968fc05..6ff62317 100755 --- a/src/view/Layout.tsx +++ b/src/view/Layout.tsx @@ -102,6 +102,7 @@ export interface ILayoutState { calculatedTabBarSize: number; calculatedBorderBarSize: number; editingTab?: TabNode; + showHiddenBorder: DockLocation; } export interface IIcons { @@ -174,6 +175,7 @@ const defaultSupportsPopout: boolean = isDesktop && !isIEorEdge; * A React component that hosts a multi-tabbed layout */ export class Layout extends React.Component { + /** @hidden @internal */ private selfRef: React.RefObject; /** @hidden @internal */ @@ -264,6 +266,7 @@ export class Layout extends React.Component { calculatedTabBarSize: 26, calculatedBorderBarSize: 30, editingTab: undefined, + showHiddenBorder: DockLocation.CENTER, }; this.onDragEnter = this.onDragEnter.bind(this); @@ -450,6 +453,7 @@ export class Layout extends React.Component { tabBarSize: this.state.calculatedTabBarSize, borderBarSize: this.state.calculatedBorderBarSize, }; + this.props.model._setShowHiddenBorder(this.state.showHiddenBorder); this.centerRect = this.props.model._layout(this.state.rect, metrics); @@ -750,6 +754,8 @@ export class Layout extends React.Component { this.newTabJson = undefined; this.customDrop = undefined; } + this.setState({ showHiddenBorder: DockLocation.CENTER }); + }; /** @hidden @internal */ @@ -778,8 +784,8 @@ export class Layout extends React.Component { /** @hidden @internal */ dragRectRender = (text: String, node?: Node, json?: IJsonTabNode, onRendered?: () => void) => { - let content: React.ReactElement | undefined =
{text.replace("
", "\n")}
; - + let content: React.ReactElement | undefined =
{text.replace("
", "\n")}
; + if (this.props.onRenderDragRect !== undefined) { const customContent = this.props.onRenderDragRect(text, node, json); if (customContent !== undefined) { @@ -842,6 +848,8 @@ export class Layout extends React.Component { y: event.clientY - clientRect.top, }; + this.checkForBorderToShow(pos.x, pos.y); + // keep it between left & right const dragRect = this.dragDiv!.getBoundingClientRect(); let newLeft = pos.x - dragRect.width / 2; @@ -950,6 +958,7 @@ export class Layout extends React.Component { this.doAction(Actions.moveNode(this.dragNode.getId(), this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index)); } } + this.setState({ showHiddenBorder: DockLocation.CENTER }); }; /** @hidden @internal */ @@ -967,6 +976,40 @@ export class Layout extends React.Component { } } + + /** @hidden @internal */ + checkForBorderToShow(x: number, y: number) { + const r = this.props.model._getOuterInnerRects().outer; + const c = r.getCenter(); + const margin = this.edgeRectWidth; + const offset = this.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 }); + } + } + /** @hidden @internal */ showEdges(rootdiv: HTMLElement) { if (this.props.model.isEnableEdgeDock()) {