) => {
109 | if (event.code === 'Escape') {
110 | // esc
111 | layout.setEditingTab(undefined);
112 | } else if (event.code === 'Enter' || event.code === 'NumpadEnter') {
113 | // enter
114 | layout.setEditingTab(undefined);
115 | layout.doAction(Actions.renameTab(node.getId(), (event.target as HTMLInputElement).value));
116 | }
117 | };
118 |
119 | const cm = layout.getClassName;
120 | let classNames = cm(CLASSES.FLEXLAYOUT__BORDER_BUTTON) + " " + cm(CLASSES.FLEXLAYOUT__BORDER_BUTTON_ + border);
121 |
122 | if (selected) {
123 | classNames += " " + cm(CLASSES.FLEXLAYOUT__BORDER_BUTTON__SELECTED);
124 | } else {
125 | classNames += " " + cm(CLASSES.FLEXLAYOUT__BORDER_BUTTON__UNSELECTED);
126 | }
127 |
128 | if (node.getClassName() !== undefined) {
129 | classNames += " " + node.getClassName();
130 | }
131 |
132 | let iconAngle = 0;
133 | if (node.getModel().isEnableRotateBorderIcons() === false) {
134 | if (border === "left") {
135 | iconAngle = 90;
136 | } else if (border === "right") {
137 | iconAngle = -90;
138 | }
139 | }
140 |
141 | const renderState = getRenderStateEx(layout, node, iconAngle);
142 |
143 | let content = renderState.content ? (
144 |
145 | {renderState.content}
146 |
) : null;
147 |
148 | const leading = renderState.leading ? (
149 |
150 | {renderState.leading}
151 |
) : null;
152 |
153 | if (layout.getEditingTab() === node) {
154 | content = (
155 |
165 | );
166 | }
167 |
168 | if (node.isEnableClose()) {
169 | const closeTitle = layout.i18nName(I18nLabel.Close_Tab);
170 | renderState.buttons.push(
171 |
178 | {(typeof icons.close === "function") ? icons.close(node) : icons.close}
179 |
180 | );
181 | }
182 |
183 | return (
184 |
196 | {leading}
197 | {content}
198 | {renderState.buttons}
199 |
200 | );
201 | };
202 |
--------------------------------------------------------------------------------
/src/view/TabButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { I18nLabel } from "../I18nLabel";
3 | import { Actions } from "../model/Actions";
4 | import { TabNode } from "../model/TabNode";
5 | import { TabSetNode } from "../model/TabSetNode";
6 | import { LayoutInternal } from "./Layout";
7 | import { ICloseType } from "../model/ICloseType";
8 | import { CLASSES } from "../Types";
9 | import { getRenderStateEx, isAuxMouseEvent } from "./Utils";
10 |
11 | /** @internal */
12 | export interface ITabButtonProps {
13 | layout: LayoutInternal;
14 | node: TabNode;
15 | selected: boolean;
16 | path: string;
17 | }
18 |
19 | /** @internal */
20 | export const TabButton = (props: ITabButtonProps) => {
21 | const { layout, node, selected, path } = props;
22 | const selfRef = React.useRef(null);
23 | const contentRef = React.useRef(null);
24 | const icons = layout.getIcons();
25 |
26 | React.useLayoutEffect(() => {
27 | node.setTabRect(layout.getBoundingClientRect(selfRef.current!));
28 | if (layout.getEditingTab() === node) {
29 | (contentRef.current! as HTMLInputElement).select();
30 | }
31 | });
32 |
33 | const onDragStart = (event: React.DragEvent) => {
34 | if (node.isEnableDrag()) {
35 | event.stopPropagation(); // prevent starting a tabset drag as well
36 | layout.setDragNode(event.nativeEvent, node as TabNode);
37 | } else {
38 | event.preventDefault();
39 | }
40 | };
41 |
42 | const onDragEnd = (event: React.DragEvent) => {
43 | layout.clearDragMain();
44 | };
45 |
46 | const onAuxMouseClick = (event: React.MouseEvent) => {
47 | if (isAuxMouseEvent(event)) {
48 | layout.auxMouseClick(node, event);
49 | }
50 | };
51 |
52 | const onContextMenu = (event: React.MouseEvent) => {
53 | layout.showContextMenu(node, event);
54 | };
55 |
56 | const onClick = () => {
57 | layout.doAction(Actions.selectTab(node.getId()));
58 | };
59 |
60 | const onDoubleClick = (event: React.MouseEvent) => {
61 | if (node.isEnableRename()) {
62 | onRename();
63 | event.stopPropagation();
64 | }
65 | };
66 |
67 | const onRename = () => {
68 | layout.setEditingTab(node);
69 | layout.getCurrentDocument()!.body.addEventListener("pointerdown", onEndEdit);
70 | };
71 |
72 | const onEndEdit = (event: Event) => {
73 | if (event.target !== contentRef.current!) {
74 | layout.getCurrentDocument()!.body.removeEventListener("pointerdown", onEndEdit);
75 | layout.setEditingTab(undefined);
76 | }
77 | };
78 |
79 | const isClosable = () => {
80 | const closeType = node.getCloseType();
81 | if (selected || closeType === ICloseType.Always) {
82 | return true;
83 | }
84 | if (closeType === ICloseType.Visible) {
85 | // not selected but x should be visible due to hover
86 | if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
87 | return true;
88 | }
89 | }
90 | return false;
91 | };
92 |
93 | const onClose = (event: React.MouseEvent) => {
94 | if (isClosable()) {
95 | layout.doAction(Actions.deleteTab(node.getId()));
96 | event.stopPropagation();
97 | }
98 | };
99 |
100 | const onClosePointerDown = (event: React.PointerEvent) => {
101 | event.stopPropagation();
102 | };
103 |
104 | const onTextBoxPointerDown = (event: React.PointerEvent) => {
105 | event.stopPropagation();
106 | };
107 |
108 | const onTextBoxKeyPress = (event: React.KeyboardEvent) => {
109 | if (event.code === 'Escape') {
110 | // esc
111 | layout.setEditingTab(undefined);
112 | } else if (event.code === 'Enter' || event.code === 'NumpadEnter') {
113 | // enter
114 | layout.setEditingTab(undefined);
115 | layout.doAction(Actions.renameTab(node.getId(), (event.target as HTMLInputElement).value));
116 | }
117 | };
118 |
119 | const cm = layout.getClassName;
120 | const parentNode = node.getParent() as TabSetNode;
121 |
122 | const isStretch = parentNode.isEnableSingleTabStretch() && parentNode.getChildren().length === 1;
123 | const baseClassName = isStretch ? CLASSES.FLEXLAYOUT__TAB_BUTTON_STRETCH : CLASSES.FLEXLAYOUT__TAB_BUTTON;
124 | let classNames = cm(baseClassName);
125 | classNames += " " + cm(baseClassName + "_" + parentNode.getTabLocation());
126 |
127 | if (!isStretch) {
128 | if (selected) {
129 | classNames += " " + cm(baseClassName + "--selected");
130 | } else {
131 | classNames += " " + cm(baseClassName + "--unselected");
132 | }
133 | }
134 |
135 | if (node.getClassName() !== undefined) {
136 | classNames += " " + node.getClassName();
137 | }
138 |
139 | const renderState = getRenderStateEx(layout, node);
140 |
141 | let content = renderState.content ? (
142 |
143 | {renderState.content}
144 |
) : null;
145 |
146 | const leading = renderState.leading ? (
147 |
148 | {renderState.leading}
149 |
) : null;
150 |
151 | if (layout.getEditingTab() === node) {
152 | content = (
153 |
163 | );
164 | }
165 |
166 | if (node.isEnableClose() && !isStretch) {
167 | const closeTitle = layout.i18nName(I18nLabel.Close_Tab);
168 | renderState.buttons.push(
169 |
176 | {(typeof icons.close === "function") ? icons.close(node) : icons.close}
177 |
178 | );
179 | }
180 |
181 | return (
182 |
195 | {leading}
196 | {content}
197 | {renderState.buttons}
198 |
199 | );
200 | };
201 |
--------------------------------------------------------------------------------
/src/model/Actions.ts:
--------------------------------------------------------------------------------
1 | import { DockLocation } from "../DockLocation";
2 | import { Action } from "./Action";
3 | import { IJsonRect, IJsonRowNode } from "./IJsonModel";
4 |
5 | /**
6 | * The Action creator class for FlexLayout model actions
7 | */
8 | export class Actions {
9 | static ADD_NODE = "FlexLayout_AddNode";
10 | static MOVE_NODE = "FlexLayout_MoveNode";
11 | static DELETE_TAB = "FlexLayout_DeleteTab";
12 | static DELETE_TABSET = "FlexLayout_DeleteTabset";
13 | static RENAME_TAB = "FlexLayout_RenameTab";
14 | static SELECT_TAB = "FlexLayout_SelectTab";
15 | static SET_ACTIVE_TABSET = "FlexLayout_SetActiveTabset";
16 | static ADJUST_WEIGHTS = "FlexLayout_AdjustWeights";
17 | static ADJUST_BORDER_SPLIT = "FlexLayout_AdjustBorderSplit";
18 | static MAXIMIZE_TOGGLE = "FlexLayout_MaximizeToggle";
19 | static UPDATE_MODEL_ATTRIBUTES = "FlexLayout_UpdateModelAttributes";
20 | static UPDATE_NODE_ATTRIBUTES = "FlexLayout_UpdateNodeAttributes";
21 | static POPOUT_TAB = "FlexLayout_PopoutTab";
22 | static POPOUT_TABSET = "FlexLayout_PopoutTabset";
23 | static CLOSE_WINDOW = "FlexLayout_CloseWindow";
24 | static CREATE_WINDOW = "FlexLayout_CreateWindow";
25 |
26 | /**
27 | * Adds a tab node to the given tabset node
28 | * @param json the json for the new tab node e.g {type:"tab", component:"table"}
29 | * @param toNodeId the new tab node will be added to the tabset with this node id
30 | * @param location the location where the new tab will be added, one of the DockLocation enum values.
31 | * @param index for docking to the center this value is the index of the tab, use -1 to add to the end.
32 | * @param select (optional) whether to select the new tab, overriding autoSelectTab
33 | * @returns {Action} the action
34 | */
35 | static addNode(json: any, toNodeId: string, location: DockLocation, index: number, select?: boolean): Action {
36 | return new Action(Actions.ADD_NODE, {
37 | json,
38 | toNode: toNodeId,
39 | location: location.getName(),
40 | index,
41 | select,
42 | });
43 | }
44 |
45 | /**
46 | * Moves a node (tab or tabset) from one location to another
47 | * @param fromNodeId the id of the node to move
48 | * @param toNodeId the id of the node to receive the moved node
49 | * @param location the location where the moved node will be added, one of the DockLocation enum values.
50 | * @param index for docking to the center this value is the index of the tab, use -1 to add to the end.
51 | * @param select (optional) whether to select the moved tab(s) in new tabset, overriding autoSelectTab
52 | * @returns {Action} the action
53 | */
54 | static moveNode(fromNodeId: string, toNodeId: string, location: DockLocation, index: number, select?: boolean): Action {
55 | return new Action(Actions.MOVE_NODE, {
56 | fromNode: fromNodeId,
57 | toNode: toNodeId,
58 | location: location.getName(),
59 | index,
60 | select,
61 | });
62 | }
63 |
64 | /**
65 | * Deletes a tab node from the layout
66 | * @param tabNodeId the id of the tab node to delete
67 | * @returns {Action} the action
68 | */
69 | static deleteTab(tabNodeId: string): Action {
70 | return new Action(Actions.DELETE_TAB, { node: tabNodeId });
71 | }
72 |
73 | /**
74 | * Deletes a tabset node and all it's child tab nodes from the layout
75 | * @param tabsetNodeId the id of the tabset node to delete
76 | * @returns {Action} the action
77 | */
78 | static deleteTabset(tabsetNodeId: string): Action {
79 | return new Action(Actions.DELETE_TABSET, { node: tabsetNodeId });
80 | }
81 |
82 | /**
83 | * Change the given nodes tab text
84 | * @param tabNodeId the id of the node to rename
85 | * @param text the test of the tab
86 | * @returns {Action} the action
87 | */
88 | static renameTab(tabNodeId: string, text: string): Action {
89 | return new Action(Actions.RENAME_TAB, { node: tabNodeId, text });
90 | }
91 |
92 | /**
93 | * Selects the given tab in its parent tabset
94 | * @param tabNodeId the id of the node to set selected
95 | * @returns {Action} the action
96 | */
97 | static selectTab(tabNodeId: string): Action {
98 | return new Action(Actions.SELECT_TAB, { tabNode: tabNodeId });
99 | }
100 |
101 | /**
102 | * Set the given tabset node as the active tabset
103 | * @param tabsetNodeId the id of the tabset node to set as active
104 | * @returns {Action} the action
105 | */
106 | static setActiveTabset(tabsetNodeId: string | undefined, windowId?: string | undefined): Action {
107 | return new Action(Actions.SET_ACTIVE_TABSET, { tabsetNode: tabsetNodeId, windowId: windowId });
108 | }
109 |
110 | /**
111 | * Adjust the weights of a row, used when the splitter is moved
112 | * @param nodeId the row node whose childrens weights are being adjusted
113 | * @param weights an array of weights to be applied to the children
114 | * @returns {Action} the action
115 | */
116 | static adjustWeights(nodeId: string, weights: number[]): Action {
117 | return new Action(Actions.ADJUST_WEIGHTS, {nodeId, weights});
118 | }
119 |
120 | static adjustBorderSplit(nodeId: string, pos: number): Action {
121 | return new Action(Actions.ADJUST_BORDER_SPLIT, { node: nodeId, pos });
122 | }
123 |
124 | /**
125 | * Maximizes the given tabset
126 | * @param tabsetNodeId the id of the tabset to maximize
127 | * @returns {Action} the action
128 | */
129 | static maximizeToggle(tabsetNodeId: string, windowId?: string | undefined): Action {
130 | return new Action(Actions.MAXIMIZE_TOGGLE, { node: tabsetNodeId, windowId: windowId });
131 | }
132 |
133 | /**
134 | * Updates the global model jsone attributes
135 | * @param attributes the json for the model attributes to update (merge into the existing attributes)
136 | * @returns {Action} the action
137 | */
138 | static updateModelAttributes(attributes: any): Action {
139 | return new Action(Actions.UPDATE_MODEL_ATTRIBUTES, { json: attributes });
140 | }
141 |
142 | /**
143 | * Updates the given nodes json attributes
144 | * @param nodeId the id of the node to update
145 | * @param attributes the json attributes to update (merge with the existing attributes)
146 | * @returns {Action} the action
147 | */
148 | static updateNodeAttributes(nodeId: string, attributes: any): Action {
149 | return new Action(Actions.UPDATE_NODE_ATTRIBUTES, { node: nodeId, json: attributes });
150 | }
151 |
152 | /**
153 | * Pops out the given tab node into a new browser window
154 | * @param nodeId the tab node to popout
155 | * @returns
156 | */
157 | static popoutTab(nodeId: string): Action {
158 | return new Action(Actions.POPOUT_TAB, { node: nodeId });
159 | }
160 |
161 | /**
162 | * Pops out the given tab set node into a new browser window
163 | * @param nodeId the tab set node to popout
164 | * @returns
165 | */
166 | static popoutTabset(nodeId: string): Action {
167 | return new Action(Actions.POPOUT_TABSET, { node: nodeId });
168 | }
169 |
170 | /**
171 | * Closes the popout window
172 | * @param windowId the id of the popout window to close
173 | * @returns
174 | */
175 | static closeWindow(windowId: string): Action {
176 | return new Action(Actions.CLOSE_WINDOW, { windowId });
177 | }
178 |
179 | /**
180 | * Creates a new empty popout window with the given layout
181 | * @param layout the json layout for the new window
182 | * @param rect the window rectangle in screen coordinates
183 | * @returns
184 | */
185 | static createWindow(layout: IJsonRowNode, rect: IJsonRect): Action {
186 | return new Action(Actions.CREATE_WINDOW, { layout, rect});
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/model/Node.ts:
--------------------------------------------------------------------------------
1 | import { AttributeDefinitions } from "../AttributeDefinitions";
2 | import { DockLocation } from "../DockLocation";
3 | import { DropInfo } from "../DropInfo";
4 | import { Orientation } from "../Orientation";
5 | import { Rect } from "../Rect";
6 | import { IDraggable } from "./IDraggable";
7 | import { IJsonBorderNode, IJsonRowNode, IJsonTabNode, IJsonTabSetNode } from "./IJsonModel";
8 | import { Model } from "./Model";
9 |
10 | export abstract class Node {
11 | /** @internal */
12 | protected model: Model;
13 | /** @internal */
14 | protected attributes: Record;
15 | /** @internal */
16 | protected parent?: Node;
17 | /** @internal */
18 | protected children: Node[];
19 | /** @internal */
20 | protected rect: Rect;
21 | /** @internal */
22 | protected path: string;
23 | /** @internal */
24 | protected listeners: Map void>;
25 |
26 | /** @internal */
27 | protected constructor(_model: Model) {
28 | this.model = _model;
29 | this.attributes = {};
30 | this.children = [];
31 | this.rect = Rect.empty();
32 | this.listeners = new Map();
33 | this.path = "";
34 | }
35 |
36 | getId() {
37 | let id = this.attributes.id;
38 | if (id !== undefined) {
39 | return id as string;
40 | }
41 |
42 | id = this.model.nextUniqueId();
43 | this.setId(id);
44 |
45 | return id as string;
46 | }
47 |
48 | getModel() {
49 | return this.model;
50 | }
51 |
52 | getType() {
53 | return this.attributes.type as string;
54 | }
55 |
56 | getParent() {
57 | return this.parent;
58 | }
59 |
60 | getChildren() {
61 | return this.children;
62 | }
63 |
64 | getRect() {
65 | return this.rect;
66 | }
67 |
68 | getPath() {
69 | return this.path;
70 | }
71 |
72 | getOrientation(): Orientation {
73 | if (this.parent === undefined) {
74 | return this.model.isRootOrientationVertical() ? Orientation.VERT : Orientation.HORZ;
75 | } else {
76 | return Orientation.flip(this.parent.getOrientation());
77 | }
78 | }
79 |
80 | // event can be: resize, visibility, maximize (on tabset), close
81 | setEventListener(event: string, callback: (params: any) => void) {
82 | this.listeners.set(event, callback);
83 | }
84 |
85 | removeEventListener(event: string) {
86 | this.listeners.delete(event);
87 | }
88 |
89 | abstract toJson(): IJsonRowNode | IJsonBorderNode | IJsonTabSetNode | IJsonTabNode | undefined;
90 |
91 | /** @internal */
92 | setId(id: string) {
93 | this.attributes.id = id;
94 | }
95 |
96 | /** @internal */
97 | fireEvent(event: string, params: any) {
98 | // console.log(this._type, " fireEvent " + event + " " + JSON.stringify(params));
99 | if (this.listeners.has(event)) {
100 | this.listeners.get(event)!(params);
101 | }
102 | }
103 |
104 | /** @internal */
105 | getAttr(name: string) {
106 | let val = this.attributes[name];
107 |
108 | if (val === undefined) {
109 | const modelName = this.getAttributeDefinitions().getModelName(name);
110 | if (modelName !== undefined) {
111 | val = this.model.getAttribute(modelName);
112 | }
113 | }
114 |
115 | // console.log(name + "=" + val);
116 | return val;
117 | }
118 |
119 | /** @internal */
120 | forEachNode(fn: (node: Node, level: number) => void, level: number) {
121 | fn(this, level);
122 | level++;
123 | for (const node of this.children) {
124 | node.forEachNode(fn, level);
125 | }
126 | }
127 |
128 | /** @internal */
129 | setPaths(path: string) {
130 | let i = 0;
131 |
132 | for (const node of this.children) {
133 | let newPath = path;
134 | if (node.getType() === "row") {
135 | newPath += "/r" + i;
136 | } else if (node.getType() === "tabset") {
137 | newPath += "/ts" + i;
138 | } else if (node.getType() === "tab") {
139 | newPath += "/t" + i;
140 | }
141 |
142 | node.path = newPath;
143 |
144 | node.setPaths(newPath);
145 | i++;
146 | }
147 | }
148 |
149 | /** @internal */
150 | setParent(parent: Node) {
151 | this.parent = parent;
152 | }
153 |
154 | /** @internal */
155 | setRect(rect: Rect) {
156 | this.rect = rect;
157 | }
158 |
159 | /** @internal */
160 | setPath(path: string) {
161 | this.path = path;
162 | }
163 |
164 | /** @internal */
165 | setWeight(weight: number) {
166 | this.attributes.weight = weight;
167 | }
168 |
169 | /** @internal */
170 | setSelected(index: number) {
171 | this.attributes.selected = index;
172 | }
173 |
174 | /** @internal */
175 | findDropTargetNode(windowId: string, dragNode: Node & IDraggable, x: number, y: number): DropInfo | undefined {
176 | let rtn: DropInfo | undefined;
177 | if (this.rect.contains(x, y)) {
178 | if (this.model.getMaximizedTabset(windowId) !== undefined) {
179 | rtn = this.model.getMaximizedTabset(windowId)!.canDrop(dragNode, x, y);
180 | } else {
181 | rtn = this.canDrop(dragNode, x, y);
182 | if (rtn === undefined) {
183 | if (this.children.length !== 0) {
184 | for (const child of this.children) {
185 | rtn = child.findDropTargetNode(windowId, dragNode, x, y);
186 | if (rtn !== undefined) {
187 | break;
188 | }
189 | }
190 | }
191 | }
192 | }
193 | }
194 |
195 | return rtn;
196 | }
197 |
198 | /** @internal */
199 | canDrop(dragNode: Node & IDraggable, x: number, y: number): DropInfo | undefined {
200 | return undefined;
201 | }
202 |
203 | /** @internal */
204 | canDockInto(dragNode: Node & IDraggable, dropInfo: DropInfo | undefined): boolean {
205 | if (dropInfo != null) {
206 | if (dropInfo.location === DockLocation.CENTER && dropInfo.node.isEnableDrop() === false) {
207 | return false;
208 | }
209 |
210 | // prevent named tabset docking into another tabset, since this would lose the header
211 | if (dropInfo.location === DockLocation.CENTER && dragNode.getType() === "tabset" && dragNode.getName() !== undefined) {
212 | return false;
213 | }
214 |
215 | if (dropInfo.location !== DockLocation.CENTER && dropInfo.node.isEnableDivide() === false) {
216 | return false;
217 | }
218 |
219 | // finally check model callback to check if drop allowed
220 | if (this.model.getOnAllowDrop()) {
221 | return (this.model.getOnAllowDrop() as (dragNode: Node, dropInfo: DropInfo) => boolean)(dragNode, dropInfo);
222 | }
223 | }
224 | return true;
225 | }
226 |
227 | /** @internal */
228 | removeChild(childNode: Node) {
229 | const pos = this.children.indexOf(childNode);
230 | if (pos !== -1) {
231 | this.children.splice(pos, 1);
232 | }
233 | return pos;
234 | }
235 |
236 | /** @internal */
237 | addChild(childNode: Node, pos?: number) {
238 | if (pos != null) {
239 | this.children.splice(pos, 0, childNode);
240 | } else {
241 | this.children.push(childNode);
242 | pos = this.children.length - 1;
243 | }
244 | childNode.parent = this;
245 | return pos;
246 | }
247 |
248 | /** @internal */
249 | removeAll() {
250 | this.children = [];
251 | }
252 |
253 | /** @internal */
254 | styleWithPosition(style?: Record) {
255 | if (style == null) {
256 | style = {};
257 | }
258 | return this.rect.styleWithPosition(style);
259 | }
260 |
261 | /** @internal */
262 | isEnableDivide() {
263 | return true;
264 | }
265 |
266 | /** @internal */
267 | toAttributeString() {
268 | return JSON.stringify(this.attributes, undefined, "\t");
269 | }
270 |
271 | // implemented by subclasses
272 | /** @internal */
273 | abstract updateAttrs(json: any): void;
274 | /** @internal */
275 | abstract getAttributeDefinitions(): AttributeDefinitions;
276 | }
277 |
--------------------------------------------------------------------------------
/src/view/Splitter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Actions } from "../model/Actions";
3 | import { BorderNode } from "../model/BorderNode";
4 | import { RowNode } from "../model/RowNode";
5 | import { Orientation } from "../Orientation";
6 | import { CLASSES } from "../Types";
7 | import { LayoutInternal } from "./Layout";
8 | import { enablePointerOnIFrames, isDesktop, startDrag } from "./Utils";
9 | import { Rect } from "../Rect";
10 |
11 | /** @internal */
12 | export interface ISplitterProps {
13 | layout: LayoutInternal;
14 | node: RowNode | BorderNode;
15 | index: number;
16 | horizontal: boolean;
17 | }
18 |
19 | /** @internal */
20 | export let splitterDragging:boolean = false; // used in tabset & borderTab
21 |
22 | /** @internal */
23 | export const Splitter = (props: ISplitterProps) => {
24 | const { layout, node, index, horizontal } = props;
25 |
26 | const [dragging, setDragging] = React.useState(false);
27 | const selfRef = React.useRef(null);
28 | const extendedRef = React.useRef(null);
29 | const pBounds = React.useRef([]);
30 | const outlineDiv = React.useRef(undefined);
31 | const handleDiv = React.useRef(undefined);
32 | const dragStartX = React.useRef(0);
33 | const dragStartY = React.useRef(0);
34 | const initalSizes = React.useRef<{ initialSizes: number[], sum: number, startPosition: number }>({ initialSizes: [], sum: 0, startPosition: 0 })
35 | // const throttleTimer = React.useRef(undefined);
36 |
37 | const size = node.getModel().getSplitterSize();
38 | let extra = node.getModel().getSplitterExtra();
39 |
40 | if (!isDesktop()) {
41 | // make hit test area on mobile at least 20px
42 | extra = Math.max(20, extra + size) - size;
43 | }
44 |
45 | React.useEffect(() => {
46 | // Android fix: must have passive touchstart handler to prevent default handling
47 | selfRef.current?.addEventListener("touchstart", onTouchStart, { passive: false });
48 | extendedRef.current?.addEventListener("touchstart", onTouchStart, { passive: false });
49 | return () => {
50 | selfRef.current?.removeEventListener("touchstart", onTouchStart);
51 | extendedRef.current?.removeEventListener("touchstart", onTouchStart);
52 | }
53 | }, []);
54 |
55 | const onTouchStart = (event: TouchEvent) => {
56 | event.preventDefault();
57 | event.stopImmediatePropagation();
58 | }
59 |
60 | const onPointerDown = (event: React.PointerEvent) => {
61 | event.stopPropagation();
62 | if (node instanceof RowNode) {
63 | initalSizes.current = node.getSplitterInitials(index);
64 | }
65 |
66 | enablePointerOnIFrames(false, layout.getCurrentDocument()!);
67 | startDrag(event.currentTarget.ownerDocument, event, onDragMove, onDragEnd, onDragCancel);
68 |
69 | pBounds.current = node.getSplitterBounds(index, true);
70 | const rootdiv = layout.getRootDiv();
71 | outlineDiv.current = layout.getCurrentDocument()!.createElement("div");
72 | outlineDiv.current.style.flexDirection = horizontal ? "row" : "column";
73 | outlineDiv.current.className = layout.getClassName(CLASSES.FLEXLAYOUT__SPLITTER_DRAG);
74 | outlineDiv.current.style.cursor = node.getOrientation() === Orientation.VERT ? "ns-resize" : "ew-resize";
75 |
76 | if (node.getModel().isSplitterEnableHandle()) {
77 | handleDiv.current = layout.getCurrentDocument()!.createElement("div");
78 | handleDiv.current.className = cm(CLASSES.FLEXLAYOUT__SPLITTER_HANDLE) + " " +
79 | (horizontal ? cm(CLASSES.FLEXLAYOUT__SPLITTER_HANDLE_HORZ) : cm(CLASSES.FLEXLAYOUT__SPLITTER_HANDLE_VERT));
80 | outlineDiv.current.appendChild(handleDiv.current);
81 | }
82 |
83 | const r = selfRef.current?.getBoundingClientRect()!;
84 | const rect = new Rect(
85 | r.x - layout.getDomRect()!.x,
86 | r.y - layout.getDomRect()!.y,
87 | r.width,
88 | r.height
89 | );
90 |
91 | dragStartX.current = event.clientX - r.x;
92 | dragStartY.current = event.clientY - r.y;
93 |
94 | rect.positionElement(outlineDiv.current);
95 | if (rootdiv) {
96 | rootdiv.appendChild(outlineDiv.current);
97 | }
98 |
99 | setDragging(true);
100 | splitterDragging = true;
101 | };
102 |
103 | const onDragCancel = () => {
104 | const rootdiv = layout.getRootDiv();
105 | if (rootdiv && outlineDiv.current) {
106 | rootdiv.removeChild(outlineDiv.current as Element);
107 | }
108 | outlineDiv.current = undefined;
109 | setDragging(false);
110 | splitterDragging = false;
111 | };
112 |
113 | const onDragMove = (x: number, y: number) => {
114 |
115 | if (outlineDiv.current) {
116 | const clientRect = layout.getDomRect();
117 | if (!clientRect) {
118 | return;
119 | }
120 | if (node.getOrientation() === Orientation.VERT) {
121 | outlineDiv.current!.style.top = getBoundPosition(y - clientRect.y - dragStartY.current) + "px";
122 | } else {
123 | outlineDiv.current!.style.left = getBoundPosition(x - clientRect.x - dragStartX.current) + "px";
124 | }
125 |
126 | if (layout.isRealtimeResize()) {
127 | updateLayout(true);
128 | }
129 | }
130 | };
131 |
132 | const onDragEnd = () => {
133 | if (outlineDiv.current) {
134 | updateLayout(false);
135 |
136 | const rootdiv = layout.getRootDiv();
137 | if (rootdiv && outlineDiv.current) {
138 | rootdiv.removeChild(outlineDiv.current as HTMLElement);
139 | }
140 | outlineDiv.current = undefined;
141 | }
142 | enablePointerOnIFrames(true, layout.getCurrentDocument()!);
143 | setDragging(false);
144 | splitterDragging = false;
145 | };
146 |
147 | const updateLayout = (realtime: boolean) => {
148 |
149 | const redraw = () => {
150 | if (outlineDiv.current) {
151 | let value = 0;
152 | if (node.getOrientation() === Orientation.VERT) {
153 | value = outlineDiv.current!.offsetTop;
154 | } else {
155 | value = outlineDiv.current!.offsetLeft;
156 | }
157 |
158 |
159 | if (node instanceof BorderNode) {
160 | const pos = (node as BorderNode).calculateSplit(node, value);
161 | layout.doAction(Actions.adjustBorderSplit(node.getId(), pos));
162 | } else {
163 | const init = initalSizes.current;
164 | const weights = node.calculateSplit(index, value, init.initialSizes, init.sum, init.startPosition);
165 | layout.doAction(Actions.adjustWeights(node.getId(), weights));
166 | }
167 | }
168 | };
169 |
170 | redraw();
171 | };
172 |
173 | const getBoundPosition = (p: number) => {
174 | const bounds = pBounds.current as number[];
175 | let rtn = p;
176 | if (p < bounds[0]) {
177 | rtn = bounds[0];
178 | }
179 | if (p > bounds[1]) {
180 | rtn = bounds[1];
181 | }
182 |
183 | return rtn;
184 | };
185 |
186 | const cm = layout.getClassName;
187 | const style: Record = {
188 | cursor: horizontal ? "ew-resize" : "ns-resize",
189 | flexDirection: horizontal ? "column" : "row"
190 | };
191 | let className = cm(CLASSES.FLEXLAYOUT__SPLITTER) + " " + cm(CLASSES.FLEXLAYOUT__SPLITTER_ + node.getOrientation().getName());
192 |
193 | if (node instanceof BorderNode) {
194 | className += " " + cm(CLASSES.FLEXLAYOUT__SPLITTER_BORDER);
195 | } else {
196 | if (node.getModel().getMaximizedTabset(layout.getWindowId()) !== undefined) {
197 | style.display = "none";
198 | }
199 | }
200 |
201 | if (horizontal) {
202 | style.width = size + "px";
203 | style.minWidth = size + "px";
204 | } else {
205 | style.height = size + "px";
206 | style.minHeight = size + "px";
207 | }
208 |
209 | let handle;
210 | if (!dragging && node.getModel().isSplitterEnableHandle()) {
211 | handle = (
212 |
216 |
217 | );
218 | }
219 |
220 | if (extra === 0) {
221 | return (
227 | {handle}
228 |
);
229 | } else {
230 | // add extended transparent div for hit testing
231 |
232 | const style2: Record = {};
233 | if (node.getOrientation() === Orientation.HORZ) {
234 | style2.height = "100%";
235 | style2.width = size + extra + "px";
236 | style2.cursor = "ew-resize";
237 | } else {
238 | style2.height = size + extra + "px";
239 | style2.width = "100%";
240 | style2.cursor = "ns-resize";
241 | }
242 |
243 | const className2 = cm(CLASSES.FLEXLAYOUT__SPLITTER_EXTRA);
244 |
245 | return (
246 | );
260 | }
261 | };
262 |
263 |
--------------------------------------------------------------------------------
/src/view/BorderTabSet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { DockLocation } from "../DockLocation";
3 | import { BorderNode } from "../model/BorderNode";
4 | import { TabNode } from "../model/TabNode";
5 | import { BorderButton } from "./BorderButton";
6 | import { LayoutInternal, ITabSetRenderValues } from "./Layout";
7 | import { showPopup } from "./PopupMenu";
8 | import { Actions } from "../model/Actions";
9 | import { I18nLabel } from "../I18nLabel";
10 | import { useTabOverflow } from "./TabOverflowHook";
11 | import { Orientation } from "../Orientation";
12 | import { CLASSES } from "../Types";
13 | import { isAuxMouseEvent } from "./Utils";
14 |
15 | /** @internal */
16 | export interface IBorderTabSetProps {
17 | border: BorderNode;
18 | layout: LayoutInternal;
19 | size: number;
20 | }
21 |
22 | /** @internal */
23 | export const BorderTabSet = (props: IBorderTabSetProps) => {
24 | const { border, layout, size } = props;
25 |
26 | const toolbarRef = React.useRef(null);
27 | const miniScrollRef = React.useRef(null);
28 | const overflowbuttonRef = React.useRef(null);
29 | const stickyButtonsRef = React.useRef(null);
30 | const tabStripInnerRef = React.useRef(null);
31 |
32 | const icons = layout.getIcons();
33 |
34 | React.useLayoutEffect(() => {
35 | border.setTabHeaderRect(layout.getBoundingClientRect(selfRef.current!));
36 | });
37 |
38 | const { selfRef, userControlledPositionRef, onScroll, onScrollPointerDown, hiddenTabs, onMouseWheel, isDockStickyButtons, isShowHiddenTabs } =
39 | useTabOverflow(layout, border, Orientation.flip(border.getOrientation()), tabStripInnerRef, miniScrollRef,
40 | layout.getClassName(CLASSES.FLEXLAYOUT__BORDER_BUTTON)
41 | );
42 |
43 | const onAuxMouseClick = (event: React.MouseEvent) => {
44 | if (isAuxMouseEvent(event)) {
45 | layout.auxMouseClick(border, event);
46 | }
47 | };
48 |
49 | const onContextMenu = (event: React.MouseEvent) => {
50 | layout.showContextMenu(border, event);
51 | };
52 |
53 | const onInterceptPointerDown = (event: React.PointerEvent) => {
54 | event.stopPropagation();
55 | };
56 |
57 | const onOverflowClick = (event: React.MouseEvent) => {
58 | const callback = layout.getShowOverflowMenu();
59 | const items = hiddenTabs.map(h => { return { index: h, node: (border.getChildren()[h] as TabNode) }; });
60 | if (callback !== undefined) {
61 |
62 | callback(border, event, items, onOverflowItemSelect);
63 | } else {
64 | const element = overflowbuttonRef.current!;
65 | showPopup(
66 | element,
67 | border,
68 | items,
69 | onOverflowItemSelect,
70 | layout);
71 | }
72 | event.stopPropagation();
73 | };
74 |
75 | const onOverflowItemSelect = (item: { node: TabNode; index: number }) => {
76 | layout.doAction(Actions.selectTab(item.node.getId()));
77 | userControlledPositionRef.current = false;
78 | };
79 |
80 | const onPopoutTab = (event: React.MouseEvent) => {
81 | const selectedTabNode = border.getChildren()[border.getSelected()] as TabNode;
82 | if (selectedTabNode !== undefined) {
83 | layout.doAction(Actions.popoutTab(selectedTabNode.getId()));
84 | }
85 | event.stopPropagation();
86 | };
87 |
88 | const cm = layout.getClassName;
89 |
90 | const tabButtons: any = [];
91 |
92 | const layoutTab = (i: number) => {
93 | const isSelected = border.getSelected() === i;
94 | const child = border.getChildren()[i] as TabNode;
95 |
96 | tabButtons.push(
97 |
106 | );
107 | if (i < border.getChildren().length - 1) {
108 | tabButtons.push(
109 |
110 | );
111 | }
112 | };
113 |
114 | for (let i = 0; i < border.getChildren().length; i++) {
115 | layoutTab(i);
116 | }
117 |
118 | let borderClasses = cm(CLASSES.FLEXLAYOUT__BORDER) + " " + cm(CLASSES.FLEXLAYOUT__BORDER_ + border.getLocation().getName());
119 | if (border.getClassName() !== undefined) {
120 | borderClasses += " " + border.getClassName();
121 | }
122 |
123 | // allow customization of tabset
124 | let leading : React.ReactNode = undefined;
125 | let buttons: any[] = [];
126 | let stickyButtons: any[] = [];
127 | const renderState: ITabSetRenderValues = { leading, buttons, stickyButtons: stickyButtons, overflowPosition: undefined };
128 | layout.customizeTabSet(border, renderState);
129 | leading = renderState.leading;
130 | stickyButtons = renderState.stickyButtons;
131 | buttons = renderState.buttons;
132 |
133 | if (renderState.overflowPosition === undefined) {
134 | renderState.overflowPosition = stickyButtons.length;
135 | }
136 |
137 | if (stickyButtons.length > 0) {
138 | if (isDockStickyButtons) {
139 | buttons = [...stickyButtons, ...buttons];
140 | } else {
141 | tabButtons.push( { e.preventDefault() }}
146 | className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_STICKY_BUTTONS_CONTAINER)}
147 | >
148 | {stickyButtons}
149 |
);
150 | }
151 | }
152 |
153 | if (isShowHiddenTabs) {
154 | const overflowTitle = layout.i18nName(I18nLabel.Overflow_Menu_Tooltip);
155 | let overflowContent;
156 | if (typeof icons.more === "function") {
157 | const items = hiddenTabs.map(h => { return { index: h, node: (border.getChildren()[h] as TabNode) }; });
158 |
159 | overflowContent = icons.more(border, items);
160 | } else {
161 | overflowContent = (<>
162 | {icons.more}
163 | {hiddenTabs.length>0?hiddenTabs.length: ""}
164 | >);
165 | }
166 | buttons.splice(Math.min(renderState.overflowPosition, buttons.length), 0,
167 |
175 | {overflowContent}
176 |
177 | );
178 | }
179 |
180 | const selectedIndex = border.getSelected();
181 | if (selectedIndex !== -1) {
182 | const selectedTabNode = border.getChildren()[selectedIndex] as TabNode;
183 | if (selectedTabNode !== undefined && layout.isSupportsPopout() && selectedTabNode.isEnablePopout()) {
184 | const popoutTitle = layout.i18nName(I18nLabel.Popout_Tab);
185 | buttons.push(
186 |
193 | {(typeof icons.popout === "function") ? icons.popout(selectedTabNode) : icons.popout}
194 |
195 | );
196 | }
197 | }
198 | const toolbar = (
199 |
200 | {buttons}
201 |
202 | );
203 |
204 | let innerStyle = {};
205 | let outerStyle = {};
206 | const borderHeight = size - 1;
207 | if (border.getLocation() === DockLocation.LEFT) {
208 | innerStyle = { right: "100%", top: 0 };
209 | outerStyle = { width: borderHeight, overflowY: "auto" };
210 | } else if (border.getLocation() === DockLocation.RIGHT) {
211 | innerStyle = { left: "100%", top: 0 };
212 | outerStyle = { width: borderHeight, overflowY: "auto" };
213 | } else {
214 | innerStyle = { left: 0 };
215 | outerStyle = { height: borderHeight, overflowX: "auto" };
216 | }
217 |
218 | let miniScrollbar = undefined;
219 | if (border.isEnableTabScrollbar()) {
220 | miniScrollbar = (
221 |
225 | );
226 | }
227 |
228 | let leadingContainer: React.ReactNode = undefined;
229 | if (leading) {
230 | leadingContainer = (
231 |
232 | {leading}
233 |
234 | );
235 | }
236 |
237 | return (
238 |
251 | {leadingContainer}
252 |
253 |
259 |
263 | {tabButtons}
264 |
265 |
266 | {miniScrollbar}
267 |
268 | {toolbar}
269 |
270 | );
271 |
272 | };
273 |
--------------------------------------------------------------------------------
/src/view/TabOverflowHook.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { TabSetNode } from "../model/TabSetNode";
3 | import { BorderNode } from "../model/BorderNode";
4 | import { Orientation } from "../Orientation";
5 | import { LayoutInternal } from "./Layout";
6 | import { TabNode } from "../model/TabNode";
7 | import { startDrag } from "./Utils";
8 | import { Rect } from "../Rect";
9 |
10 | /** @internal */
11 | export const useTabOverflow = (
12 | layout: LayoutInternal,
13 | node: TabSetNode | BorderNode,
14 | orientation: Orientation,
15 | tabStripRef: React.RefObject,
16 | miniScrollRef: React.RefObject,
17 | tabClassName: string
18 | ) => {
19 | const [hiddenTabs, setHiddenTabs] = React.useState([]);
20 | const [isShowHiddenTabs, setShowHiddenTabs] = React.useState(false);
21 | const [isDockStickyButtons, setDockStickyButtons] = React.useState(false);
22 |
23 | const selfRef = React.useRef(null);
24 | const userControlledPositionRef = React.useRef(false);
25 | const updateHiddenTabsTimerRef = React.useRef(undefined);
26 | const hiddenTabsRef = React.useRef([]);
27 | const thumbInternalPos = React.useRef(0);
28 | const repositioningRef = React.useRef(false);
29 | hiddenTabsRef.current = hiddenTabs;
30 |
31 | // if node id changes (new model) then reset scroll to 0
32 | React.useLayoutEffect(() => {
33 | if (tabStripRef.current) {
34 | setScrollPosition(0);
35 | }
36 | }, [node.getId()]);
37 |
38 | // if selected node or tabset/border rectangle change then unset usercontrolled (so selected tab will be kept in view)
39 | React.useLayoutEffect(() => {
40 | userControlledPositionRef.current = false;
41 | }, [node.getSelectedNode(), node.getRect().width, node.getRect().height]);
42 |
43 | React.useLayoutEffect(() => {
44 | checkForOverflow(); // if tabs + sticky buttons length > scroll area => move sticky buttons to right buttons
45 |
46 | if (userControlledPositionRef.current === false) {
47 | scrollIntoView();
48 | }
49 |
50 | updateScrollMetrics();
51 | updateHiddenTabs();
52 | });
53 |
54 | React.useEffect(() => {
55 | selfRef.current?.addEventListener("wheel", onWheel, { passive: false });
56 | return () => {
57 | selfRef.current?.removeEventListener("wheel", onWheel);
58 | };
59 | }, [selfRef.current]);
60 |
61 | // needed to prevent default mouse wheel over tabset/border when page scrolled (cannot do with react event?)
62 | const onWheel = (event: Event) => {
63 | event.preventDefault();
64 | };
65 |
66 | function scrollIntoView() {
67 | const selectedTabNode = node.getSelectedNode() as TabNode;
68 | if (selectedTabNode && tabStripRef.current) {
69 | const stripRect = layout.getBoundingClientRect(tabStripRef.current);
70 | const selectedRect = selectedTabNode.getTabRect()!;
71 |
72 | let shift = getNear(stripRect) - getNear(selectedRect);
73 | if (shift > 0 || getSize(selectedRect) > getSize(stripRect)) {
74 | setScrollPosition(getScrollPosition(tabStripRef.current) - shift);
75 | repositioningRef.current = true; // prevent onScroll setting userControlledPosition
76 | } else {
77 | shift = getFar(selectedRect) - getFar(stripRect);
78 | if (shift > 0) {
79 | setScrollPosition(getScrollPosition(tabStripRef.current) + shift);
80 | repositioningRef.current = true;
81 | }
82 | }
83 | }
84 | }
85 |
86 | const updateScrollMetrics = () => {
87 | if (tabStripRef.current && miniScrollRef.current) {
88 | const t = tabStripRef.current;
89 | const s = miniScrollRef.current;
90 |
91 | const size = getElementSize(t);
92 | const scrollSize = getScrollSize(t);
93 | const position = getScrollPosition(t);
94 |
95 | if (scrollSize > size && scrollSize > 0) {
96 | let thumbSize = size * size / scrollSize;
97 | let adjust = 0;
98 | if (thumbSize < 20) {
99 | adjust = 20 - thumbSize;
100 | thumbSize = 20;
101 | }
102 | const thumbPos = position * (size - adjust) / scrollSize;
103 | if (orientation === Orientation.HORZ) {
104 | s.style.width = thumbSize + "px";
105 | s.style.left = thumbPos + "px";
106 | } else {
107 | s.style.height = thumbSize + "px";
108 | s.style.top = thumbPos + "px";
109 | }
110 | s.style.display = "block";
111 | } else {
112 | s.style.display = "none";
113 | }
114 |
115 | if (orientation === Orientation.HORZ) {
116 | s.style.bottom = "0px";
117 | } else {
118 | s.style.right = "0px";
119 | }
120 | }
121 | }
122 |
123 | const updateHiddenTabs = () => {
124 | const newHiddenTabs = findHiddenTabs();
125 | const showHidden = newHiddenTabs.length > 0;
126 |
127 | if (showHidden !== isShowHiddenTabs) {
128 | setShowHiddenTabs(showHidden);
129 | }
130 |
131 | if (updateHiddenTabsTimerRef.current === undefined) {
132 | // throttle updates to prevent Maximum update depth exceeded error
133 | updateHiddenTabsTimerRef.current = setTimeout(() => {
134 | const newHiddenTabs = findHiddenTabs();
135 | if (!arraysEqual(newHiddenTabs, hiddenTabsRef.current)) {
136 | setHiddenTabs(newHiddenTabs);
137 | }
138 |
139 | updateHiddenTabsTimerRef.current = undefined;
140 | }, 100);
141 | }
142 | }
143 |
144 | const onScroll = () => {
145 | if (!repositioningRef.current){
146 | userControlledPositionRef.current=true;
147 | }
148 | repositioningRef.current = false;
149 | updateScrollMetrics()
150 | updateHiddenTabs();
151 | };
152 |
153 | const onScrollPointerDown = (event: React.PointerEvent) => {
154 | event.stopPropagation();
155 | miniScrollRef.current!.setPointerCapture(event.pointerId)
156 | const r = miniScrollRef.current?.getBoundingClientRect()!;
157 | if (orientation === Orientation.HORZ) {
158 | thumbInternalPos.current = event.clientX - r.x;
159 | } else {
160 | thumbInternalPos.current = event.clientY - r.y;
161 | }
162 | startDrag(event.currentTarget.ownerDocument, event, onDragMove, onDragEnd, onDragCancel);
163 | }
164 |
165 | const onDragMove = (x: number, y: number) => {
166 | if (tabStripRef.current && miniScrollRef.current) {
167 | const t = tabStripRef.current;
168 | const s = miniScrollRef.current;
169 | const size = getElementSize(t);
170 | const scrollSize = getScrollSize(t);
171 | const thumbSize = getElementSize(s);
172 |
173 | const r = t.getBoundingClientRect()!;
174 | let thumb = 0;
175 | if (orientation === Orientation.HORZ) {
176 | thumb = x - r.x - thumbInternalPos.current;
177 | } else {
178 | thumb = y - r.y - thumbInternalPos.current
179 | }
180 |
181 | thumb = Math.max(0, Math.min(scrollSize - thumbSize, thumb));
182 | if (size > 0) {
183 | const scrollPos = thumb * scrollSize / size;
184 | setScrollPosition(scrollPos);
185 | }
186 | }
187 | }
188 |
189 | const onDragEnd = () => {
190 | }
191 |
192 | const onDragCancel = () => {
193 | }
194 |
195 | const checkForOverflow = () => {
196 | if (tabStripRef.current) {
197 | const strip = tabStripRef.current;
198 | const tabContainer = strip.firstElementChild!;
199 |
200 | const offset = isDockStickyButtons ? 10 : 0; // prevents flashing, after sticky buttons docked set, must be 10 pixels smaller before unsetting
201 | const dock = (getElementSize(tabContainer) + offset) > getElementSize(tabStripRef.current);
202 | if (dock !== isDockStickyButtons) {
203 | setDockStickyButtons(dock);
204 | }
205 | }
206 | }
207 |
208 | const findHiddenTabs: () => number[] = () => {
209 | const hidden: number[] = [];
210 | if (tabStripRef.current) {
211 | const strip = tabStripRef.current;
212 | const stripRect = strip.getBoundingClientRect();
213 | const visibleNear = getNear(stripRect) - 1;
214 | const visibleFar = getFar(stripRect) + 1;
215 |
216 | const tabContainer = strip.firstElementChild!;
217 |
218 | let i = 0;
219 | Array.from(tabContainer.children).forEach((child) => {
220 | const tabRect = child.getBoundingClientRect();
221 |
222 | if (child.classList.contains(tabClassName)) {
223 | if (getNear(tabRect) < visibleNear || getFar(tabRect) > visibleFar) {
224 | hidden.push(i);
225 | }
226 | i++;
227 | }
228 | });
229 | }
230 |
231 | return hidden;
232 | };
233 |
234 | const onMouseWheel = (event: React.WheelEvent) => {
235 | if (tabStripRef.current) {
236 | if (node.getChildren().length === 0) return;
237 |
238 | let delta = 0;
239 | if (Math.abs(event.deltaY) > 0) {
240 | delta = -event.deltaY;
241 | if (event.deltaMode === 1) {
242 | // DOM_DELTA_LINE 0x01 The delta values are specified in lines.
243 | delta *= 40;
244 | }
245 | const newPos = getScrollPosition(tabStripRef.current) - delta;
246 | const maxScroll = getScrollSize(tabStripRef.current) - getElementSize(tabStripRef.current);
247 | const p = Math.max(0, Math.min(maxScroll, newPos));
248 | setScrollPosition(p);
249 | event.stopPropagation();
250 | }
251 | }
252 | };
253 |
254 | // orientation helpers:
255 |
256 | const getNear = (rect: DOMRect | Rect) => {
257 | if (orientation === Orientation.HORZ) {
258 | return rect.x;
259 | } else {
260 | return rect.y;
261 | }
262 | };
263 |
264 | const getFar = (rect: DOMRect | Rect) => {
265 | if (orientation === Orientation.HORZ) {
266 | return rect.right;
267 | } else {
268 | return rect.bottom;
269 | }
270 | };
271 |
272 | const getElementSize = (elm: Element) => {
273 | if (orientation === Orientation.HORZ) {
274 | return elm.clientWidth;
275 | } else {
276 | return elm.clientHeight;
277 | }
278 | }
279 |
280 | const getSize = (rect: DOMRect | Rect) => {
281 | if (orientation === Orientation.HORZ) {
282 | return rect.width;
283 | } else {
284 | return rect.height;
285 | }
286 | }
287 |
288 | const getScrollSize = (elm: Element) => {
289 | if (orientation === Orientation.HORZ) {
290 | return elm.scrollWidth;
291 | } else {
292 | return elm.scrollHeight;
293 | }
294 | }
295 |
296 | const setScrollPosition = (p: number) => {
297 | if (orientation === Orientation.HORZ) {
298 | tabStripRef.current!.scrollLeft = p;
299 | } else {
300 | tabStripRef.current!.scrollTop = p;
301 | }
302 | }
303 |
304 | const getScrollPosition = (elm: Element) => {
305 | if (orientation === Orientation.HORZ) {
306 | return elm.scrollLeft;
307 | } else {
308 | return elm.scrollTop;
309 | }
310 | }
311 |
312 | return { selfRef, userControlledPositionRef, onScroll, onScrollPointerDown, hiddenTabs, onMouseWheel, isDockStickyButtons, isShowHiddenTabs };
313 | };
314 |
315 | function arraysEqual(arr1: number[], arr2: number[]) {
316 | return arr1.length === arr2.length && arr1.every((val, index) => val === arr2[index]);
317 | }
318 |
--------------------------------------------------------------------------------