react-simple-dock 0.1.3 → 0.2.2
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/README.md +4 -0
- package/index.css +4 -1
- package/index.d.ts +26 -8
- package/index.js +117 -30
- package/package.json +1 -1
- package/types.d.ts +17 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# React-Simple-Dock
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/pret-simple-dock/)
|
|
4
|
+
[](https://www.npmjs.com/package/react-simple-dock)
|
|
5
|
+
[](https://github.com/percevalw/react-simple-dock/actions/workflows/playwright.yml)
|
|
6
|
+
|
|
3
7
|
A set of React components to create a dockable interface, allowing to arrange and resize tabs.
|
|
4
8
|
|
|
5
9
|
## Installation of the javascript package
|
package/index.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Light Theme, feel free to override */
|
|
2
2
|
:root {
|
|
3
3
|
--sd-grid-gap: 3px;
|
|
4
|
+
--sd-panel-border: solid;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
:root[data-theme="light"] {
|
|
@@ -110,6 +111,8 @@ body[data-jp-theme-light] {
|
|
|
110
111
|
.leaf > .panel-content {
|
|
111
112
|
position: relative;
|
|
112
113
|
flex: 1;
|
|
114
|
+
border: var(--sd-panel-border, solid) var(--sd-tab-border-color, #bdbdbd);
|
|
115
|
+
border-width: 0 1px 1px 1px;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
.leaf > .panel-content > div {
|
|
@@ -119,7 +122,7 @@ body[data-jp-theme-light] {
|
|
|
119
122
|
left: 0;
|
|
120
123
|
width: 100%;
|
|
121
124
|
height: 100%;
|
|
122
|
-
overflow:
|
|
125
|
+
overflow: auto;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/* TAB HEADER */
|
package/index.d.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import "./index.css";
|
|
3
|
-
import {
|
|
3
|
+
import { PanelProps, DefaultLayoutConfig } 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
|
+
*/
|
|
4
13
|
export declare const Panel: (props: PanelProps) => any;
|
|
5
|
-
|
|
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
|
+
* @param collapseTabsOnMobile If true, auto-detect mobile devices and collapse the whole layout into a single tabbed panel.
|
|
25
|
+
* @returns A React element representing the complete panel layout.
|
|
26
|
+
*/
|
|
27
|
+
export declare function Layout({ children, defaultConfig, wrapDnd, collapseTabsOnMobile, }: {
|
|
6
28
|
children: React.ReactElement<PanelProps>[] | React.ReactElement<PanelProps>;
|
|
7
|
-
defaultConfig?:
|
|
29
|
+
defaultConfig?: DefaultLayoutConfig;
|
|
8
30
|
wrapDnd?: boolean;
|
|
31
|
+
collapseTabsOnMobile?: boolean | string[];
|
|
9
32
|
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
export declare namespace Layout {
|
|
11
|
-
var defaultProps: {
|
|
12
|
-
wrapDnd: boolean;
|
|
13
|
-
};
|
|
14
|
-
}
|
package/index.js
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { DndProvider, useDrag, useDrop, useDragDropManager } from "react-dnd";
|
|
3
4
|
import "./index.css";
|
|
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 isMobileDevice = () => {
|
|
12
|
+
if (typeof window === "undefined" || typeof navigator === "undefined")
|
|
13
|
+
return false;
|
|
14
|
+
return /Mobi|Android|iPhone|iPad|iPod|Windows Phone|IEMobile|Opera Mini/i.test(navigator.userAgent || "");
|
|
15
|
+
};
|
|
16
|
+
const getPanelElementMaxHeaderHeight = (config, panelElements) => {
|
|
17
|
+
// If we have a leaf, then the header height is the max of each tabs header height
|
|
18
|
+
if (config.kind === "leaf") {
|
|
19
|
+
return Math.max(...config.tabs.map((tab) => panelElements.get(config).children[0].offsetHeight));
|
|
20
|
+
}
|
|
21
|
+
// If we have a row, then the header height is the max of each child's max header height
|
|
22
|
+
else if (config.kind === "row") {
|
|
23
|
+
return Math.max(...config.children.map((c) => getPanelElementMaxHeaderHeight(c, panelElements)));
|
|
24
|
+
}
|
|
25
|
+
// If we have a column, then the header height is the sum of each child's max header height
|
|
26
|
+
else {
|
|
27
|
+
return config.children.reduce((a, b) => a + getPanelElementMaxHeaderHeight(b, panelElements), 0);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function normalizeConfig(config, siblingsCount, depth = 0, default_kind = depth % 2 === 0 ? "row" : "column") {
|
|
31
|
+
if (typeof config === "string") {
|
|
32
|
+
config = { tabs: [config] };
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(config)) {
|
|
35
|
+
config = { children: config };
|
|
36
|
+
}
|
|
37
|
+
// Leaf panel if tabs provided
|
|
38
|
+
if (config.tabs) {
|
|
39
|
+
const leaf = config;
|
|
40
|
+
const tabIndex = typeof leaf.tabIndex === "number" ? leaf.tabIndex : 0;
|
|
41
|
+
const size = typeof leaf.size === "number" ? leaf.size : 100 / siblingsCount;
|
|
42
|
+
return { kind: "leaf", tabs: leaf.tabs, tabIndex, size, nesting: leaf.nesting };
|
|
43
|
+
}
|
|
44
|
+
const container = config;
|
|
45
|
+
const kind = container.kind || default_kind;
|
|
46
|
+
const size = typeof container.size === "number" ? container.size : 100 / siblingsCount;
|
|
47
|
+
const children = container.children.map((child) => normalizeConfig(child, container.children.length, depth + 1, kind === "row" ? "column" : "row"));
|
|
48
|
+
return { kind, children, size, nesting: container.nesting };
|
|
49
|
+
}
|
|
15
50
|
const TabHandle = ({ name, index, visible, onClick, children, }) => {
|
|
16
51
|
const getItem = () => ({
|
|
17
52
|
name,
|
|
@@ -119,15 +154,16 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
119
154
|
let size = savedSizes.current[idx] * ratio;
|
|
120
155
|
let nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
|
|
121
156
|
const total = savedSizes.current.reduce((a, b) => a + b, 0);
|
|
122
|
-
const headerHeight = getPanelElementHeader(target.parentElement).offsetHeight;
|
|
123
157
|
if (config.kind === "column") {
|
|
158
|
+
const headerHeightBefore = getPanelElementMaxHeaderHeight(config.children[idx], panelElements);
|
|
159
|
+
const headerHeightAfter = getPanelElementMaxHeaderHeight(config.children[idx + 1], panelElements);
|
|
124
160
|
const parentHeight = panelContentRef.current.offsetHeight;
|
|
125
|
-
if ((size * parentHeight) / total <
|
|
126
|
-
size = (
|
|
161
|
+
if ((size * parentHeight) / total < headerHeightBefore) {
|
|
162
|
+
size = (headerHeightBefore / parentHeight) * total;
|
|
127
163
|
nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
|
|
128
164
|
}
|
|
129
|
-
else if ((nextSize * parentHeight) / total <
|
|
130
|
-
nextSize = (
|
|
165
|
+
else if ((nextSize * parentHeight) / total < headerHeightAfter) {
|
|
166
|
+
nextSize = (headerHeightAfter / parentHeight) * total;
|
|
131
167
|
size = savedSizes.current[idx] + (savedSizes.current[idx + 1] - nextSize);
|
|
132
168
|
}
|
|
133
169
|
}
|
|
@@ -148,9 +184,10 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
148
184
|
};
|
|
149
185
|
const panelContentRef = useRef(null);
|
|
150
186
|
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") }))] }));
|
|
187
|
+
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
188
|
});
|
|
153
189
|
const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
190
|
+
const [, set] = useState();
|
|
154
191
|
const closestRef = useRef(null);
|
|
155
192
|
const lastItem = useRef(null);
|
|
156
193
|
let lastPlaceholder = useRef(null);
|
|
@@ -208,14 +245,19 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
208
245
|
zones.push({ rect, config, index, element });
|
|
209
246
|
};
|
|
210
247
|
if (config.kind === "leaf") {
|
|
211
|
-
if (!config.tabs.
|
|
248
|
+
if (!(config.tabs.length == 1 && config.tabs[0] == name)) {
|
|
212
249
|
pushZone("TOP", left, top, width, height / 2);
|
|
213
250
|
pushZone("BOTTOM", left, top + height / 2, width, height / 2);
|
|
214
251
|
pushZone("LEFT", left, top, width / 2, height);
|
|
215
252
|
pushZone("RIGHT", left + width / 2, top, width / 2, height);
|
|
216
253
|
}
|
|
217
|
-
|
|
218
|
-
|
|
254
|
+
// Only allow center zone if it's for the panel to stay at the same spot.
|
|
255
|
+
// Indeed, it was confusing since it appears like it's going to create
|
|
256
|
+
// a new panel, when in reality it just creates a new tab in the target panel.
|
|
257
|
+
else {
|
|
258
|
+
pushZone("CENTER", left, top, width, height);
|
|
259
|
+
}
|
|
260
|
+
pushZone("TAB", left, top, width, element.children[0].offsetHeight);
|
|
219
261
|
}
|
|
220
262
|
else {
|
|
221
263
|
const firstTabs = config.children?.[0]?.tabs || [null];
|
|
@@ -340,9 +382,34 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
340
382
|
const overlayRef = useRef(null);
|
|
341
383
|
return _jsx("div", { className: "tab-handle-overlay", ref: overlayRef });
|
|
342
384
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
385
|
+
/**
|
|
386
|
+
* A Panel component.
|
|
387
|
+
*
|
|
388
|
+
* This component represents a Panel within the layout.
|
|
389
|
+
*
|
|
390
|
+
* @param props.name The unique identifier of the panel.
|
|
391
|
+
* @param props.header The content to render in the panel header.
|
|
392
|
+
* @param props.children The content to render within the panel.
|
|
393
|
+
*/
|
|
394
|
+
export const Panel = (props) => {
|
|
395
|
+
return null;
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Main layout component that organizes panels and handles drag and drop.
|
|
399
|
+
*
|
|
400
|
+
* The Layout component takes child panel components, constructs an initial layout configuration,
|
|
401
|
+
* and renders the panel structure using NestedPanel. It also wraps the layout in a DndProvider
|
|
402
|
+
* if drag and drop support is enabled.
|
|
403
|
+
*
|
|
404
|
+
* @param children The children `Panel` components to render within the layout.
|
|
405
|
+
* @param defaultConfig The default layout configuration to use.
|
|
406
|
+
* @param wrapDnd A boolean flag to enable or disable drag and drop support (default: true).
|
|
407
|
+
* @param collapseTabsOnMobile If true, auto-detect mobile devices and collapse the whole layout into a single tabbed panel.
|
|
408
|
+
* @returns A React element representing the complete panel layout.
|
|
409
|
+
*/
|
|
410
|
+
export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOnMobile = true, }) {
|
|
411
|
+
const children_array = React.Children.toArray(children);
|
|
412
|
+
const namedChildren = Object.fromEntries(children_array.map((c, i) => [
|
|
346
413
|
c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`),
|
|
347
414
|
{
|
|
348
415
|
element: c.props.children,
|
|
@@ -350,15 +417,38 @@ export function Layout(props) {
|
|
|
350
417
|
},
|
|
351
418
|
]));
|
|
352
419
|
const panelElements = useRef(new Map());
|
|
353
|
-
const [rootConfig, setRootConfig] = useState(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
420
|
+
const [rootConfig, setRootConfig] = useState(() => {
|
|
421
|
+
const base = defaultConfig
|
|
422
|
+
? normalizeConfig(defaultConfig, children_array.length)
|
|
423
|
+
: {
|
|
424
|
+
kind: "row",
|
|
425
|
+
size: 1,
|
|
426
|
+
children: children_array.map((c, i) => ({
|
|
427
|
+
kind: "leaf",
|
|
428
|
+
tabs: [c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`)],
|
|
429
|
+
tabIndex: 0,
|
|
430
|
+
size: 100 / children_array.length,
|
|
431
|
+
})),
|
|
432
|
+
};
|
|
433
|
+
if (collapseTabsOnMobile && isMobileDevice()) {
|
|
434
|
+
// If collapseTabsOnMobile is a list, use the names in the list as the first tabs
|
|
435
|
+
// then complete with the rest of the named children
|
|
436
|
+
const actualTabs = Object.keys(namedChildren);
|
|
437
|
+
const tabs = [
|
|
438
|
+
...(Array.isArray(collapseTabsOnMobile)
|
|
439
|
+
? collapseTabsOnMobile.filter((name) => actualTabs.includes(name))
|
|
440
|
+
: []),
|
|
441
|
+
...actualTabs.filter((name) => !Array.isArray(collapseTabsOnMobile) ||
|
|
442
|
+
!collapseTabsOnMobile.includes(name)),
|
|
443
|
+
];
|
|
444
|
+
return {
|
|
445
|
+
kind: "leaf",
|
|
446
|
+
tabs: tabs,
|
|
447
|
+
tabIndex: 0,
|
|
448
|
+
size: 100,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return base;
|
|
362
452
|
});
|
|
363
453
|
let config = rootConfig;
|
|
364
454
|
if (rootConfig.kind !== "leaf" || rootConfig.tabs.length > 0) {
|
|
@@ -373,11 +463,8 @@ export function Layout(props) {
|
|
|
373
463
|
setRootConfig(newConfig);
|
|
374
464
|
};
|
|
375
465
|
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 (
|
|
466
|
+
if (wrapDnd) {
|
|
377
467
|
return _jsx(DndProvider, { backend: HTML5Backend, children: container });
|
|
378
468
|
}
|
|
379
469
|
return container;
|
|
380
470
|
}
|
|
381
|
-
Layout.defaultProps = {
|
|
382
|
-
wrapDnd: true,
|
|
383
|
-
};
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -28,6 +28,23 @@ export type ContainerLayoutConfig = {
|
|
|
28
28
|
* A union type that represents either a leaf panel (with tabs) or a container panel (row/column with children).
|
|
29
29
|
*/
|
|
30
30
|
export type LayoutConfig = LeafLayoutConfig | ContainerLayoutConfig;
|
|
31
|
+
/**
|
|
32
|
+
* User-provided config can omit kind, size, and tabIndex for inference
|
|
33
|
+
*/
|
|
34
|
+
export type DefaultLeafConfig = {
|
|
35
|
+
kind?: "leaf";
|
|
36
|
+
tabs: string[];
|
|
37
|
+
tabIndex?: number;
|
|
38
|
+
size?: number;
|
|
39
|
+
nesting?: number;
|
|
40
|
+
};
|
|
41
|
+
export type DefaultContainerConfig = {
|
|
42
|
+
kind?: "row" | "column";
|
|
43
|
+
children: DefaultLayoutConfig[];
|
|
44
|
+
size?: number;
|
|
45
|
+
nesting?: number;
|
|
46
|
+
};
|
|
47
|
+
export type DefaultLayoutConfig = string | DefaultLeafConfig | DefaultContainerConfig | DefaultLayoutConfig[];
|
|
31
48
|
/**
|
|
32
49
|
* Properties for a panel component.
|
|
33
50
|
*
|