react-simple-dock 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.css +195 -0
- package/index.d.ts +30 -0
- package/index.js +60 -23
- package/package.json +3 -8
- package/types.d.ts +67 -0
- package/types.js +1 -0
- package/utils.d.ts +26 -0
- package/utils.js +248 -0
package/index.css
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* Light Theme, feel free to override */
|
|
2
|
+
:root {
|
|
3
|
+
--sd-grid-gap: 3px;
|
|
4
|
+
--sd-panel-border: solid;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:root[data-theme="light"] {
|
|
8
|
+
--sd-background-color: #fff;
|
|
9
|
+
--sd-text-color: #000;
|
|
10
|
+
--sd-tab-border-color: #bdbdbd;
|
|
11
|
+
--sd-tab-background-color: white;
|
|
12
|
+
--sd-header-background-color: white;
|
|
13
|
+
--sd-header-border-color: #cecece;
|
|
14
|
+
--sd-overlay-color: rgba(69, 159, 232, 0.51);
|
|
15
|
+
--sd-highlight-color: #4591e8;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Dark Theme, feel free to override */
|
|
19
|
+
:root[data-theme="dark"] {
|
|
20
|
+
--sd-background-color: #22272e;
|
|
21
|
+
--sd-text-color: #fff;
|
|
22
|
+
--sd-border-color: #666;
|
|
23
|
+
--sd-tab-border-color: #111;
|
|
24
|
+
--sd-highlight-color: #4591e8;
|
|
25
|
+
--sd-tab-background-color: #1c2128;
|
|
26
|
+
--sd-header-background-color: #333;
|
|
27
|
+
--sd-header-border-color: #444;
|
|
28
|
+
--sd-overlay-color: rgba(105, 159, 232, 0.51);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body[data-jp-theme-light] {
|
|
32
|
+
/* don't care about the theme, just that we're in JupyterLab */
|
|
33
|
+
--sd-grid-gap: 7px;
|
|
34
|
+
--sd-background-color: var(--jp-layout-color3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.resize-border {
|
|
38
|
+
position: absolute;
|
|
39
|
+
background-color: var(--sd-background-color, #fff);
|
|
40
|
+
opacity: 0;
|
|
41
|
+
z-index: 10;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.resize-border:hover {
|
|
45
|
+
opacity: 0.8;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.resize-border.bottom {
|
|
49
|
+
bottom: calc(0px - var(--sd-grid-gap));
|
|
50
|
+
left: 0;
|
|
51
|
+
right: 0;
|
|
52
|
+
height: calc((var(--sd-grid-gap)));
|
|
53
|
+
cursor: row-resize;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.resize-border.right {
|
|
57
|
+
top: 0;
|
|
58
|
+
bottom: 0;
|
|
59
|
+
right: calc(0px - var(--sd-grid-gap));
|
|
60
|
+
width: calc((var(--sd-grid-gap)));
|
|
61
|
+
cursor: col-resize;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.container {
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.tab-handle-overlay {
|
|
70
|
+
position: absolute;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
display: none;
|
|
73
|
+
top: 0;
|
|
74
|
+
left: 0;
|
|
75
|
+
right: 0;
|
|
76
|
+
bottom: 0;
|
|
77
|
+
z-index: 100;
|
|
78
|
+
background: var(--sd-overlay-color, rgba(69, 159, 232, 0.51));
|
|
79
|
+
border: 2px dashed var(--sd-highlight-color, #4591e8);
|
|
80
|
+
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
transition: 0s linear;
|
|
83
|
+
transition-property: left, right, top, bottom, width, height;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.panel {
|
|
87
|
+
position: relative;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex: 1 100000000 0;
|
|
90
|
+
align-self: stretch;
|
|
91
|
+
height: 100%;
|
|
92
|
+
width: 100%;
|
|
93
|
+
background: var(--sd-background-color);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.panel.leaf {
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.row > .panel-content,
|
|
101
|
+
.column > .panel-content {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-gap: var(--sd-grid-gap);
|
|
104
|
+
position: absolute;
|
|
105
|
+
left: 0;
|
|
106
|
+
top: 0;
|
|
107
|
+
width: 100%;
|
|
108
|
+
height: 100%;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.leaf > .panel-content {
|
|
112
|
+
position: relative;
|
|
113
|
+
flex: 1;
|
|
114
|
+
border: var(--sd-panel-border, solid) var(--sd-tab-border-color, #bdbdbd);
|
|
115
|
+
border-width: 0 1px 1px 1px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.leaf > .panel-content > div {
|
|
119
|
+
background: var(--sd-tab-background-color);
|
|
120
|
+
position: absolute;
|
|
121
|
+
top: 0;
|
|
122
|
+
left: 0;
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: 100%;
|
|
125
|
+
overflow: scroll;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* TAB HEADER */
|
|
129
|
+
|
|
130
|
+
.tab-header {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: row;
|
|
133
|
+
height: var(--sd-header-height);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.tab-header-border {
|
|
137
|
+
position: relative;
|
|
138
|
+
height: 1px;
|
|
139
|
+
background: var(--sd-tab-border-color, #cecece);
|
|
140
|
+
margin-top: -2px;
|
|
141
|
+
z-index: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.tab-header-bottom {
|
|
145
|
+
position: relative;
|
|
146
|
+
background: var(--sd-tab-background-color, white);
|
|
147
|
+
height: 3px;
|
|
148
|
+
z-index: 2;
|
|
149
|
+
border: solid var(--sd-tab-border-color, #bdbdbd);
|
|
150
|
+
border-width: 0 1px 1px 1px;
|
|
151
|
+
/*
|
|
152
|
+
Why did I put this ? It messes up the overall width of the layout
|
|
153
|
+
margin-left: -1px;
|
|
154
|
+
margin-right: -1px;
|
|
155
|
+
*/
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.tab-placeholder {
|
|
159
|
+
width: 0;
|
|
160
|
+
transition: width 0.1s linear;
|
|
161
|
+
height: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.tab-handle {
|
|
165
|
+
position: relative;
|
|
166
|
+
box-sizing: border-box;
|
|
167
|
+
padding: 5px;
|
|
168
|
+
color: var(--sd-text-color, #000);
|
|
169
|
+
background: var(--sd-tab-background-color, white);
|
|
170
|
+
font-size: calc((var(--sd-header-height) - 12px) * 0.8);
|
|
171
|
+
border: solid var(--sd-tab-border-color, #bdbdbd);
|
|
172
|
+
border-width: 1px 1px 1px 1px;
|
|
173
|
+
/* Too overlap the border of the next handle and avoid double borders */
|
|
174
|
+
margin-right: -1px;
|
|
175
|
+
z-index: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.tab-handle:nth-child(2) {
|
|
179
|
+
border-left-width: 1px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.tab-handle.tab-handle__hidden {
|
|
183
|
+
z-index: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.tab-handle.tab-handle__visible:before {
|
|
187
|
+
content: "";
|
|
188
|
+
display: block;
|
|
189
|
+
background: var(--sd-highlight-color, #4591e8);
|
|
190
|
+
position: absolute;
|
|
191
|
+
top: 0;
|
|
192
|
+
left: 0;
|
|
193
|
+
right: 0;
|
|
194
|
+
height: 2px;
|
|
195
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "./index.css";
|
|
3
|
+
import { LayoutConfig, PanelProps } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* A Panel component.
|
|
6
|
+
*
|
|
7
|
+
* This component represents a Panel within the layout.
|
|
8
|
+
*
|
|
9
|
+
* @param props.name The unique identifier of the panel.
|
|
10
|
+
* @param props.header The content to render in the panel header.
|
|
11
|
+
* @param props.children The content to render within the panel.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Panel: (props: PanelProps) => any;
|
|
14
|
+
/**
|
|
15
|
+
* Main layout component that organizes panels and handles drag and drop.
|
|
16
|
+
*
|
|
17
|
+
* The Layout component takes child panel components, constructs an initial layout configuration,
|
|
18
|
+
* and renders the panel structure using NestedPanel. It also wraps the layout in a DndProvider
|
|
19
|
+
* if drag and drop support is enabled.
|
|
20
|
+
*
|
|
21
|
+
* @param children The children `Panel` components to render within the layout.
|
|
22
|
+
* @param defaultConfig The default layout configuration to use.
|
|
23
|
+
* @param wrapDnd A boolean flag to enable or disable drag and drop support (default: true).
|
|
24
|
+
* @returns A React element representing the complete panel layout.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Layout({ children, defaultConfig, wrapDnd, }: {
|
|
27
|
+
children: React.ReactElement<PanelProps>[] | React.ReactElement<PanelProps>;
|
|
28
|
+
defaultConfig?: LayoutConfig;
|
|
29
|
+
wrapDnd?: boolean;
|
|
30
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/index.js
CHANGED
|
@@ -4,14 +4,24 @@ import "./index.css";
|
|
|
4
4
|
import { DndProvider, useDrag, useDragDropManager, useDrop } from "react-dnd";
|
|
5
5
|
import { filterPanels, movePanel } from "./utils";
|
|
6
6
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
7
|
-
export const Panel = (props) => {
|
|
8
|
-
return null;
|
|
9
|
-
};
|
|
10
7
|
function useForceUpdate() {
|
|
11
8
|
const [value, setValue] = useState(0); // integer state
|
|
12
9
|
return () => setValue((value) => value + 1); // update state to force render
|
|
13
10
|
}
|
|
14
|
-
const
|
|
11
|
+
const getPanelElementMaxHeaderHeight = (config, panelElements) => {
|
|
12
|
+
// If we have a leaf, then the header height is the max of each tabs header height
|
|
13
|
+
if (config.kind === "leaf") {
|
|
14
|
+
return Math.max(...config.tabs.map((tab) => panelElements.get(config).children[0].offsetHeight));
|
|
15
|
+
}
|
|
16
|
+
// If we have a row, then the header height is the max of each child's max header height
|
|
17
|
+
else if (config.kind === "row") {
|
|
18
|
+
return Math.max(...config.children.map((c) => getPanelElementMaxHeaderHeight(c, panelElements)));
|
|
19
|
+
}
|
|
20
|
+
// If we have a column, then the header height is the sum of each child's max header height
|
|
21
|
+
else {
|
|
22
|
+
return config.children.reduce((a, b) => a + getPanelElementMaxHeaderHeight(b, panelElements), 0);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
15
25
|
const TabHandle = ({ name, index, visible, onClick, children, }) => {
|
|
16
26
|
const getItem = () => ({
|
|
17
27
|
name,
|
|
@@ -119,15 +129,16 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
119
129
|
let size = savedSizes.current[idx] * ratio;
|
|
120
130
|
let nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
|
|
121
131
|
const total = savedSizes.current.reduce((a, b) => a + b, 0);
|
|
122
|
-
const headerHeight = getPanelElementHeader(target.parentElement).offsetHeight;
|
|
123
132
|
if (config.kind === "column") {
|
|
133
|
+
const headerHeightBefore = getPanelElementMaxHeaderHeight(config.children[idx], panelElements);
|
|
134
|
+
const headerHeightAfter = getPanelElementMaxHeaderHeight(config.children[idx + 1], panelElements);
|
|
124
135
|
const parentHeight = panelContentRef.current.offsetHeight;
|
|
125
|
-
if ((size * parentHeight) / total <
|
|
126
|
-
size = (
|
|
136
|
+
if ((size * parentHeight) / total < headerHeightBefore) {
|
|
137
|
+
size = (headerHeightBefore / parentHeight) * total;
|
|
127
138
|
nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
|
|
128
139
|
}
|
|
129
|
-
else if ((nextSize * parentHeight) / total <
|
|
130
|
-
nextSize = (
|
|
140
|
+
else if ((nextSize * parentHeight) / total < headerHeightAfter) {
|
|
141
|
+
nextSize = (headerHeightAfter / parentHeight) * total;
|
|
131
142
|
size = savedSizes.current[idx] + (savedSizes.current[idx + 1] - nextSize);
|
|
132
143
|
}
|
|
133
144
|
}
|
|
@@ -148,7 +159,7 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
148
159
|
};
|
|
149
160
|
const panelContentRef = useRef(null);
|
|
150
161
|
const panelRef = useRef(null);
|
|
151
|
-
return (_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (config.tabIndex < config.tabs.length ? (_jsx("div", { children: leaves[config.tabs[config.tabIndex]].element })) : (_jsx("div", { style: { width: "100%", height: "100%" } }))) : (config.children.map((c, i) => (_jsx(NestedPanel, { config: c, leaves: leaves, saveSizes: handleSaveSizes, index: i, onResize: handleResize, isLast: i === config.children.length - 1, direction: config.kind, panelElements: panelElements }, i)))) }), !isLast && direction === "column" && (_jsx("div", { className: "resize-border bottom", onMouseDown: (e) => handleMouseDown(e, "bottom") })), !isLast && direction === "row" && (_jsx("div", { className: "resize-border right", onMouseDown: (e) => handleMouseDown(e, "right") }))] }));
|
|
162
|
+
return (_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (config.tabIndex < config.tabs.length ? (_jsx("div", { children: leaves[config.tabs[config.tabIndex]].element }, config.tabs[config.tabIndex])) : (_jsx("div", { style: { width: "100%", height: "100%" } }))) : (config.children.map((c, i) => (_jsx(NestedPanel, { config: c, leaves: leaves, saveSizes: handleSaveSizes, index: i, onResize: handleResize, isLast: i === config.children.length - 1, direction: config.kind, panelElements: panelElements }, i)))) }), !isLast && direction === "column" && (_jsx("div", { className: "resize-border bottom", onMouseDown: (e) => handleMouseDown(e, "bottom") })), !isLast && direction === "row" && (_jsx("div", { className: "resize-border right", onMouseDown: (e) => handleMouseDown(e, "right") }))] }));
|
|
152
163
|
});
|
|
153
164
|
const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
154
165
|
const closestRef = useRef(null);
|
|
@@ -208,14 +219,19 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
208
219
|
zones.push({ rect, config, index, element });
|
|
209
220
|
};
|
|
210
221
|
if (config.kind === "leaf") {
|
|
211
|
-
if (!config.tabs.
|
|
222
|
+
if (!(config.tabs.length == 1 && config.tabs[0] == name)) {
|
|
212
223
|
pushZone("TOP", left, top, width, height / 2);
|
|
213
224
|
pushZone("BOTTOM", left, top + height / 2, width, height / 2);
|
|
214
225
|
pushZone("LEFT", left, top, width / 2, height);
|
|
215
226
|
pushZone("RIGHT", left + width / 2, top, width / 2, height);
|
|
216
227
|
}
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
// Only allow center zone if it's for the panel to stay at the same spot.
|
|
229
|
+
// Indeed, it was confusing since it appears like it's going to create
|
|
230
|
+
// a new panel, when in reality it just creates a new tab in the target panel.
|
|
231
|
+
else {
|
|
232
|
+
pushZone("CENTER", left, top, width, height);
|
|
233
|
+
}
|
|
234
|
+
pushZone("TAB", left, top, width, element.children[0].offsetHeight);
|
|
219
235
|
}
|
|
220
236
|
else {
|
|
221
237
|
const firstTabs = config.children?.[0]?.tabs || [null];
|
|
@@ -340,9 +356,33 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
340
356
|
const overlayRef = useRef(null);
|
|
341
357
|
return _jsx("div", { className: "tab-handle-overlay", ref: overlayRef });
|
|
342
358
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
/**
|
|
360
|
+
* A Panel component.
|
|
361
|
+
*
|
|
362
|
+
* This component represents a Panel within the layout.
|
|
363
|
+
*
|
|
364
|
+
* @param props.name The unique identifier of the panel.
|
|
365
|
+
* @param props.header The content to render in the panel header.
|
|
366
|
+
* @param props.children The content to render within the panel.
|
|
367
|
+
*/
|
|
368
|
+
export const Panel = (props) => {
|
|
369
|
+
return null;
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Main layout component that organizes panels and handles drag and drop.
|
|
373
|
+
*
|
|
374
|
+
* The Layout component takes child panel components, constructs an initial layout configuration,
|
|
375
|
+
* and renders the panel structure using NestedPanel. It also wraps the layout in a DndProvider
|
|
376
|
+
* if drag and drop support is enabled.
|
|
377
|
+
*
|
|
378
|
+
* @param children The children `Panel` components to render within the layout.
|
|
379
|
+
* @param defaultConfig The default layout configuration to use.
|
|
380
|
+
* @param wrapDnd A boolean flag to enable or disable drag and drop support (default: true).
|
|
381
|
+
* @returns A React element representing the complete panel layout.
|
|
382
|
+
*/
|
|
383
|
+
export function Layout({ children, defaultConfig, wrapDnd = true, }) {
|
|
384
|
+
const children_array = React.Children.toArray(children);
|
|
385
|
+
const namedChildren = Object.fromEntries(children_array.map((c, i) => [
|
|
346
386
|
c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`),
|
|
347
387
|
{
|
|
348
388
|
element: c.props.children,
|
|
@@ -350,14 +390,14 @@ export function Layout(props) {
|
|
|
350
390
|
},
|
|
351
391
|
]));
|
|
352
392
|
const panelElements = useRef(new Map());
|
|
353
|
-
const [rootConfig, setRootConfig] = useState(
|
|
393
|
+
const [rootConfig, setRootConfig] = useState(defaultConfig || {
|
|
354
394
|
kind: "row",
|
|
355
395
|
size: 1,
|
|
356
|
-
children:
|
|
396
|
+
children: children_array.map((c, i) => ({
|
|
357
397
|
kind: "leaf",
|
|
358
398
|
tabs: [c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`)],
|
|
359
399
|
tabIndex: 0,
|
|
360
|
-
size: 100 /
|
|
400
|
+
size: 100 / children_array.length,
|
|
361
401
|
})),
|
|
362
402
|
});
|
|
363
403
|
let config = rootConfig;
|
|
@@ -373,11 +413,8 @@ export function Layout(props) {
|
|
|
373
413
|
setRootConfig(newConfig);
|
|
374
414
|
};
|
|
375
415
|
const container = (_jsxs("div", { className: "container", children: [_jsx(NestedPanel, { leaves: namedChildren, config: config, panelElements: panelElements.current }), _jsx(Overlay, { panelElements: panelElements, onDrop: handleDrop, rootConfig: config })] }));
|
|
376
|
-
if (
|
|
416
|
+
if (wrapDnd) {
|
|
377
417
|
return _jsx(DndProvider, { backend: HTML5Backend, children: container });
|
|
378
418
|
}
|
|
379
419
|
return container;
|
|
380
420
|
}
|
|
381
|
-
Layout.defaultProps = {
|
|
382
|
-
wrapDnd: true,
|
|
383
|
-
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-simple-dock",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"main": "index.js",
|
|
5
|
-
"types": "index.d.ts",
|
|
6
5
|
"description": "Simple dock component for React",
|
|
7
6
|
"repository": "https://github.com/percevalw/react-simple-dock",
|
|
8
7
|
"author": "Perceval Wajsbürt <perceval.wajsburt@gmail.com>",
|
|
@@ -14,11 +13,6 @@
|
|
|
14
13
|
"react": ">=17.0.0",
|
|
15
14
|
"react-dom": ">=17.0.0"
|
|
16
15
|
},
|
|
17
|
-
"files": [
|
|
18
|
-
"dist",
|
|
19
|
-
"LICENSE",
|
|
20
|
-
"README.md"
|
|
21
|
-
],
|
|
22
16
|
"browserslist": {
|
|
23
17
|
"production": [
|
|
24
18
|
">0.2%",
|
|
@@ -30,5 +24,6 @@
|
|
|
30
24
|
"last 1 firefox version",
|
|
31
25
|
"last 1 safari version"
|
|
32
26
|
]
|
|
33
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"types": "index.d.ts"
|
|
34
29
|
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Leaf layout configuration.
|
|
4
|
+
*
|
|
5
|
+
* Represents a panel that contains one or more tabs along with the current tab index and its size.
|
|
6
|
+
*/
|
|
7
|
+
export type LeafLayoutConfig = {
|
|
8
|
+
kind: "leaf";
|
|
9
|
+
tabs: string[];
|
|
10
|
+
tabIndex: number;
|
|
11
|
+
size: number;
|
|
12
|
+
nesting?: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Container layout configuration.
|
|
16
|
+
*
|
|
17
|
+
* Represents a container panel that arranges its child panels in either a row or a column.
|
|
18
|
+
*/
|
|
19
|
+
export type ContainerLayoutConfig = {
|
|
20
|
+
kind: "row" | "column";
|
|
21
|
+
children: LayoutConfig[];
|
|
22
|
+
size: number;
|
|
23
|
+
nesting?: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Layout configuration.
|
|
27
|
+
*
|
|
28
|
+
* A union type that represents either a leaf panel (with tabs) or a container panel (row/column with children).
|
|
29
|
+
*/
|
|
30
|
+
export type LayoutConfig = LeafLayoutConfig | ContainerLayoutConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Properties for a panel component.
|
|
33
|
+
*
|
|
34
|
+
* Defines the children to render within the panel, an optional name, and an optional header.
|
|
35
|
+
*/
|
|
36
|
+
export type PanelProps = {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
name?: string;
|
|
39
|
+
header?: React.ReactNode;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Represents a drop zone within the layout.
|
|
43
|
+
*
|
|
44
|
+
* Includes the rectangular coordinates, the associated layout configuration,
|
|
45
|
+
* an a to indicate the drop position, and a reference to the related DOM element.
|
|
46
|
+
*/
|
|
47
|
+
export type Zone = {
|
|
48
|
+
rect: {
|
|
49
|
+
left: number;
|
|
50
|
+
top: number;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
};
|
|
54
|
+
config: LayoutConfig;
|
|
55
|
+
index: "LEFT" | "RIGHT" | "TOP" | "BOTTOM" | "CENTER" | "TAB";
|
|
56
|
+
element: HTMLElement;
|
|
57
|
+
before?: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Drag and drop item for tab header drag operations.
|
|
61
|
+
*
|
|
62
|
+
* Contains the panel name and a reference to the draggable element.
|
|
63
|
+
*/
|
|
64
|
+
export type DnDItem = {
|
|
65
|
+
name: string;
|
|
66
|
+
handleElement: HTMLDivElement;
|
|
67
|
+
};
|
package/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/utils.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Zone, LayoutConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Moves a panel within the layout based on a drop zone and panel name.
|
|
4
|
+
*
|
|
5
|
+
* This function traverses the layout config tree, removing the panel from its original position,
|
|
6
|
+
* and re-inserting it based on the provided drop zone. It handles both leaf and container nodes,
|
|
7
|
+
* ensuring proper size calculations and layout updates.
|
|
8
|
+
*
|
|
9
|
+
* @param zone - The drop zone where the panel is to be moved.
|
|
10
|
+
* @param name - The name of the panel being moved.
|
|
11
|
+
* @param inside - The layout config in which the move operation takes place.
|
|
12
|
+
* @returns The updated layout config after moving the panel.
|
|
13
|
+
*/
|
|
14
|
+
export declare const movePanel: (zone: Zone | null, name: string, inside: LayoutConfig) => LayoutConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Filters panels from a layout configuration based on a provided list of panel names.
|
|
17
|
+
*
|
|
18
|
+
* Traverses the layout configuration tree and removes any panels (or tabs within panels)
|
|
19
|
+
* whose names are not included in the specified list. It simplifies nodes by filtering
|
|
20
|
+
* out unwanted tabs and recursively updating container nodes.
|
|
21
|
+
*
|
|
22
|
+
* @param names - An array of panel names that should remain in the layout.
|
|
23
|
+
* @param inside - The layout configuration to filter.
|
|
24
|
+
* @returns The updated layout configuration with only the specified panels, or null if empty.
|
|
25
|
+
*/
|
|
26
|
+
export declare const filterPanels: (names: string[], inside: LayoutConfig) => LayoutConfig;
|
package/utils.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplifies a layout configuration by flattening nested layouts with the same kind.
|
|
3
|
+
*
|
|
4
|
+
* If the layout configuration is a row or column, this function will flatten any nested child configurations
|
|
5
|
+
* that have the same kind as the parent. It recalculates the size of nested items based on their parent's size.
|
|
6
|
+
*
|
|
7
|
+
* @param config - The layout configuration to simplify.
|
|
8
|
+
* @returns The simplified layout configuration.
|
|
9
|
+
*/
|
|
10
|
+
const simplifyLayout = (config) => {
|
|
11
|
+
if (config.kind === "row" || config.kind === "column") {
|
|
12
|
+
let expandedChildren = [];
|
|
13
|
+
let changed = false;
|
|
14
|
+
config.children.forEach((child) => {
|
|
15
|
+
if (child.kind === config.kind) {
|
|
16
|
+
changed = true;
|
|
17
|
+
const totalChildContainerSize = child.children.reduce((a, b) => a + b.size, 0);
|
|
18
|
+
child.children.forEach((grandChild) => {
|
|
19
|
+
expandedChildren.push({
|
|
20
|
+
...grandChild,
|
|
21
|
+
size: (child.size * grandChild.size) / totalChildContainerSize,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
expandedChildren.push(child);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (changed) {
|
|
30
|
+
config = { ...config, children: expandedChildren };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Moves a panel within the layout based on a drop zone and panel name.
|
|
37
|
+
*
|
|
38
|
+
* This function traverses the layout config tree, removing the panel from its original position,
|
|
39
|
+
* and re-inserting it based on the provided drop zone. It handles both leaf and container nodes,
|
|
40
|
+
* ensuring proper size calculations and layout updates.
|
|
41
|
+
*
|
|
42
|
+
* @param zone - The drop zone where the panel is to be moved.
|
|
43
|
+
* @param name - The name of the panel being moved.
|
|
44
|
+
* @param inside - The layout config in which the move operation takes place.
|
|
45
|
+
* @returns The updated layout config after moving the panel.
|
|
46
|
+
*/
|
|
47
|
+
export const movePanel = (zone, name, inside) => {
|
|
48
|
+
const editLayout = (visitedConfig) => {
|
|
49
|
+
let config = visitedConfig;
|
|
50
|
+
if (config.kind === "leaf" && config.tabs.includes(name)) {
|
|
51
|
+
/* If it's a simple leaf, try to remove the matching tab if it was
|
|
52
|
+
* the tab that was picked by the user (since we're moving it) */
|
|
53
|
+
const newTabs = config.tabs.filter((tabName) => tabName !== name);
|
|
54
|
+
config = {
|
|
55
|
+
...config,
|
|
56
|
+
tabs: newTabs,
|
|
57
|
+
tabIndex: Math.min(newTabs.length - 1, config.tabIndex),
|
|
58
|
+
};
|
|
59
|
+
if (config.tabs.length === 0) {
|
|
60
|
+
config = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/* If this zone is the zone targeted as drop-zone by the user */
|
|
64
|
+
if (zone && visitedConfig === zone.config) {
|
|
65
|
+
if (config === null) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "leaf",
|
|
68
|
+
tabs: [name],
|
|
69
|
+
tabIndex: 0,
|
|
70
|
+
size: visitedConfig.size,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// or it's a container zone
|
|
74
|
+
else if (config.kind === "row" || config.kind === "column") {
|
|
75
|
+
const totalSize = config.children.reduce((a, b) => a + b.size, 0);
|
|
76
|
+
const fraction = config.kind === "column"
|
|
77
|
+
? zone.rect.height / zone.element.parentElement.offsetHeight
|
|
78
|
+
: zone.rect.width / zone.element.parentElement.offsetWidth;
|
|
79
|
+
// x / (y + x) = a
|
|
80
|
+
// x = a y + a x
|
|
81
|
+
// x = a y / ( 1 - a )
|
|
82
|
+
const newConfigKind = zone.index === "TOP" || zone.index === "BOTTOM" ? "column" : "row";
|
|
83
|
+
if (config.kind === newConfigKind) {
|
|
84
|
+
const newZone = {
|
|
85
|
+
kind: "leaf",
|
|
86
|
+
tabs: [name],
|
|
87
|
+
tabIndex: 0,
|
|
88
|
+
size: (totalSize * fraction) / (1 - fraction),
|
|
89
|
+
};
|
|
90
|
+
config = {
|
|
91
|
+
...config,
|
|
92
|
+
children: zone.index === "LEFT" || zone.index === "TOP"
|
|
93
|
+
? [newZone, ...config.children.map(editLayout).filter((c) => c !== null)]
|
|
94
|
+
: [...config.children.map(editLayout).filter((c) => c !== null), newZone],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const newZone = {
|
|
99
|
+
kind: "leaf",
|
|
100
|
+
tabs: [name],
|
|
101
|
+
tabIndex: 0,
|
|
102
|
+
size: 50,
|
|
103
|
+
};
|
|
104
|
+
const oldConfig = {
|
|
105
|
+
...config,
|
|
106
|
+
// Remove the panel that is being moved
|
|
107
|
+
children: config.children.map(editLayout).filter((c) => c !== null),
|
|
108
|
+
size: 50,
|
|
109
|
+
};
|
|
110
|
+
config = {
|
|
111
|
+
kind: newConfigKind,
|
|
112
|
+
children: zone.index === "TOP" || zone.index === "LEFT" ? [newZone, oldConfig] : [oldConfig, newZone],
|
|
113
|
+
size: config.size,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Or it's a leaf zone
|
|
118
|
+
else if (config.kind === "leaf") {
|
|
119
|
+
if (zone.index === "CENTER") {
|
|
120
|
+
const newTabs = [...config.tabs.filter((tabName) => tabName !== name), name];
|
|
121
|
+
config = {
|
|
122
|
+
...config,
|
|
123
|
+
tabs: newTabs,
|
|
124
|
+
tabIndex: newTabs.length - 1,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
else if (zone.index === "TAB") {
|
|
128
|
+
const newTabs = [...config.tabs];
|
|
129
|
+
let insertIndex = newTabs.findIndex((tabName) => tabName === zone.before);
|
|
130
|
+
if (insertIndex === -1) {
|
|
131
|
+
insertIndex = newTabs.length;
|
|
132
|
+
}
|
|
133
|
+
newTabs.splice(insertIndex, 0, name);
|
|
134
|
+
config = {
|
|
135
|
+
...config,
|
|
136
|
+
tabs: newTabs,
|
|
137
|
+
tabIndex: insertIndex,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const newZone = {
|
|
142
|
+
kind: "leaf",
|
|
143
|
+
tabs: [name],
|
|
144
|
+
tabIndex: 0,
|
|
145
|
+
size: 50,
|
|
146
|
+
};
|
|
147
|
+
config = {
|
|
148
|
+
kind: zone.index === "TOP" || zone.index === "BOTTOM" ? "column" : "row",
|
|
149
|
+
children: zone.index === "TOP" || zone.index === "LEFT"
|
|
150
|
+
? [newZone, { ...config, size: 50 }]
|
|
151
|
+
: [{ ...config, size: 50, }, newZone],
|
|
152
|
+
size: config.size,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// If there is nothing left after removing the dropped zone
|
|
158
|
+
else if (config === null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// Otherwise, recurse into the node
|
|
162
|
+
else {
|
|
163
|
+
let hasChanged = false;
|
|
164
|
+
if (config.kind !== "leaf") {
|
|
165
|
+
const children = config.children
|
|
166
|
+
.map((child) => {
|
|
167
|
+
const updated = editLayout(child);
|
|
168
|
+
if (updated !== child) {
|
|
169
|
+
hasChanged = true;
|
|
170
|
+
}
|
|
171
|
+
return updated;
|
|
172
|
+
})
|
|
173
|
+
.filter((child) => child !== null);
|
|
174
|
+
if (hasChanged) {
|
|
175
|
+
config = { ...config, children };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if ((config.kind === "leaf" && config.tabs.length === 0) ||
|
|
180
|
+
(config.kind !== "leaf" && config.children.length === 0)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
/* Simplify a node of the layout by flattening row children of rows
|
|
184
|
+
* or column children of columns */
|
|
185
|
+
config = simplifyLayout(config);
|
|
186
|
+
return config;
|
|
187
|
+
};
|
|
188
|
+
return editLayout(inside);
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Filters panels from a layout configuration based on a provided list of panel names.
|
|
192
|
+
*
|
|
193
|
+
* Traverses the layout configuration tree and removes any panels (or tabs within panels)
|
|
194
|
+
* whose names are not included in the specified list. It simplifies nodes by filtering
|
|
195
|
+
* out unwanted tabs and recursively updating container nodes.
|
|
196
|
+
*
|
|
197
|
+
* @param names - An array of panel names that should remain in the layout.
|
|
198
|
+
* @param inside - The layout configuration to filter.
|
|
199
|
+
* @returns The updated layout configuration with only the specified panels, or null if empty.
|
|
200
|
+
*/
|
|
201
|
+
export const filterPanels = (names, inside) => {
|
|
202
|
+
const editLayout = (visitedConfig) => {
|
|
203
|
+
let config = visitedConfig;
|
|
204
|
+
if (config.kind === "leaf" && !config.tabs.every((name) => names.includes(name))) {
|
|
205
|
+
/* If it's a simple leaf, try to remove the matching tab if it was
|
|
206
|
+
* the tab that was picked by the user (since we're moving it) */
|
|
207
|
+
config = {
|
|
208
|
+
...config,
|
|
209
|
+
tabs: config.tabs.filter((name) => names.includes(name)),
|
|
210
|
+
tabIndex: Math.min(config.tabs.length - 1, config.tabIndex),
|
|
211
|
+
};
|
|
212
|
+
if (config.tabs.length === 0) {
|
|
213
|
+
config = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// If there is nothing left after removing the dropped zone
|
|
217
|
+
if (config === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
// Otherwise, recurse into the node
|
|
221
|
+
else {
|
|
222
|
+
let hasChanged = false;
|
|
223
|
+
if (config.kind !== "leaf") {
|
|
224
|
+
const children = config.children
|
|
225
|
+
.map((child) => {
|
|
226
|
+
const updated = editLayout(child);
|
|
227
|
+
if (updated !== child) {
|
|
228
|
+
hasChanged = true;
|
|
229
|
+
}
|
|
230
|
+
return updated;
|
|
231
|
+
})
|
|
232
|
+
.filter((child) => child !== null);
|
|
233
|
+
if (hasChanged) {
|
|
234
|
+
config = { ...config, children };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if ((config.kind === "leaf" && config.tabs.length === 0) ||
|
|
239
|
+
(config.kind !== "leaf" && config.children.length === 0)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
/* Simplify a node of the layout by flattening row children of rows
|
|
243
|
+
* or column children of columns */
|
|
244
|
+
config = simplifyLayout(config);
|
|
245
|
+
return config;
|
|
246
|
+
};
|
|
247
|
+
return editLayout(inside);
|
|
248
|
+
};
|