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 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 getPanelElementHeader = (node) => node.children[0];
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 < headerHeight) {
126
- size = (headerHeight / parentHeight) * total;
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 < headerHeight) {
130
- nextSize = (headerHeight / parentHeight) * total;
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.includes(name)) {
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
- pushZone("CENTER", left, top, width, height);
218
- pushZone("TAB", left, top, width, getPanelElementHeader(element).offsetHeight);
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
- export function Layout(props) {
344
- const children = React.Children.toArray(props.children);
345
- const namedChildren = Object.fromEntries(children.map((c, i) => [
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(props.defaultConfig || {
393
+ const [rootConfig, setRootConfig] = useState(defaultConfig || {
354
394
  kind: "row",
355
395
  size: 1,
356
- children: children.map((c, i) => ({
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 / children.length,
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 (props.wrapDnd) {
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.2",
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
+ };