@linzjs/windows 4.0.2 → 4.1.1
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/dist/index.ts +1 -0
- package/dist/panel/OpenPanelButton.tsx +2 -2
- package/dist/panel/OpenPanelIcon.tsx +4 -4
- package/dist/panel/Panel.tsx +32 -145
- package/dist/panel/PanelContext.ts +1 -1
- package/dist/panel/PanelInstanceContext.ts +0 -5
- package/dist/panel/PanelInstanceContextProvider.tsx +3 -1
- package/dist/panel/PanelsContext.tsx +3 -5
- package/dist/panel/PanelsContextProvider.tsx +6 -1
- package/dist/panel/PopinWIndow.tsx +143 -0
- package/dist/panel/PopoutWindow.tsx +33 -5
- package/dist/panel/index.ts +2 -0
- package/dist/panel/types/PanelPosition.ts +4 -0
- package/dist/panel/types/PanelProps.ts +18 -0
- package/dist/panel/types/PanelRestoredState.ts +8 -0
- package/dist/panel/types/PanelSize.ts +4 -0
- package/dist/panel/types/PanelState.ts +15 -0
- package/dist/panel/types/PanelStateOptions.ts +11 -0
- package/dist/panel/types/index.ts +6 -0
- package/dist/panel/usePanelStateHandler.tsx +101 -0
- package/package.json +1 -1
package/dist/index.ts
CHANGED
|
@@ -7,11 +7,11 @@ interface OpenPanelButtonProps extends OpenPanelOptions {
|
|
|
7
7
|
buttonText: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const OpenPanelButton = ({ buttonText, ...openPanelOptions }: OpenPanelButtonProps) => {
|
|
10
|
+
export const OpenPanelButton = ({ buttonText, uniqueId = buttonText, ...openPanelOptions }: OpenPanelButtonProps) => {
|
|
11
11
|
const { openPanel, openPanels } = useContext(PanelsContext);
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<button onClick={() => openPanel(openPanelOptions)}>
|
|
14
|
+
<button onClick={() => openPanel({ uniqueId, ...openPanelOptions })}>
|
|
15
15
|
Show {buttonText} {openPanels.has(buttonText) ? "(Open)" : ""}
|
|
16
16
|
</button>
|
|
17
17
|
);
|
|
@@ -2,7 +2,7 @@ import "./OpenPanelIcon.scss";
|
|
|
2
2
|
|
|
3
3
|
import { OpenPanelOptions, PanelsContext } from "./PanelsContext";
|
|
4
4
|
import clsx from "clsx";
|
|
5
|
-
import { ReactNode, useContext
|
|
5
|
+
import { ReactNode, useContext } from "react";
|
|
6
6
|
|
|
7
7
|
import { LuiIcon } from "@linzjs/lui";
|
|
8
8
|
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
@@ -35,10 +35,10 @@ export const OpenPanelIcon = ({
|
|
|
35
35
|
className,
|
|
36
36
|
disabled,
|
|
37
37
|
testId,
|
|
38
|
+
uniqueId = iconTitle,
|
|
38
39
|
...openPanelOptions
|
|
39
40
|
}: OpenPanelIconProps) => {
|
|
40
41
|
const { openPanel, openPanels } = useContext(PanelsContext);
|
|
41
|
-
const id = useRef(openPanelOptions.uniqueId ?? crypto.randomUUID());
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<button
|
|
@@ -46,11 +46,11 @@ export const OpenPanelIcon = ({
|
|
|
46
46
|
className={clsx(
|
|
47
47
|
className,
|
|
48
48
|
"lui-button lui-button-secondary lui-button-toolbar panel-button",
|
|
49
|
-
openPanels.has(
|
|
49
|
+
openPanels.has(uniqueId) && "OpenPanelIcon-selected",
|
|
50
50
|
disabled && "OpenPanelIcon-disabled",
|
|
51
51
|
)}
|
|
52
52
|
title={iconTitle}
|
|
53
|
-
onClick={() => openPanel(openPanelOptions)}
|
|
53
|
+
onClick={() => openPanel({ uniqueId, ...openPanelOptions })}
|
|
54
54
|
disabled={disabled}
|
|
55
55
|
data-testid={testId}
|
|
56
56
|
>
|
package/dist/panel/Panel.tsx
CHANGED
|
@@ -2,59 +2,21 @@ import "./Panel.scss";
|
|
|
2
2
|
import "./PanelBlue.scss";
|
|
3
3
|
import "@linzjs/lui/dist/scss/base.scss";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { PanelPosition, PanelsContext } from "./PanelsContext";
|
|
5
|
+
import { PanelInstanceContext } from "./PanelInstanceContext";
|
|
6
|
+
import { PanelsContext } from "./PanelsContext";
|
|
8
7
|
import { PopoutWindow } from "./PopoutWindow";
|
|
9
8
|
import clsx from "clsx";
|
|
10
|
-
import
|
|
11
|
-
PropsWithChildren,
|
|
12
|
-
ReactElement,
|
|
13
|
-
ReactNode,
|
|
14
|
-
useCallback,
|
|
15
|
-
useContext,
|
|
16
|
-
useEffect,
|
|
17
|
-
useRef,
|
|
18
|
-
useState,
|
|
19
|
-
} from "react";
|
|
9
|
+
import { PropsWithChildren, ReactElement, useContext, useEffect, useMemo } from "react";
|
|
20
10
|
import { createPortal } from "react-dom";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
11
|
+
import { PopinWindow } from "./PopinWIndow";
|
|
12
|
+
import { PanelProps } from "./types/PanelProps";
|
|
13
|
+
import { useConstFunction } from "../common";
|
|
23
14
|
|
|
24
|
-
export
|
|
25
|
-
title:
|
|
26
|
-
position?: PanelPosition | "tile" | "center";
|
|
27
|
-
size?: PanelSize;
|
|
28
|
-
className?: string;
|
|
29
|
-
children: ReactNode;
|
|
30
|
-
maxHeight?: number | string;
|
|
31
|
-
maxWidth?: number | string;
|
|
32
|
-
minHeight?: number | string;
|
|
33
|
-
minWidth?: number | string;
|
|
34
|
-
modal?: boolean;
|
|
35
|
-
resizeable?: boolean;
|
|
36
|
-
onClose?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const Panel = ({
|
|
40
|
-
title,
|
|
41
|
-
onClose,
|
|
42
|
-
size = { width: 320, height: 200 },
|
|
43
|
-
maxHeight,
|
|
44
|
-
maxWidth,
|
|
45
|
-
minHeight = 100,
|
|
46
|
-
minWidth = 100,
|
|
47
|
-
modal,
|
|
48
|
-
className,
|
|
49
|
-
position = modal ? "center" : "tile",
|
|
50
|
-
resizeable = modal !== true,
|
|
51
|
-
children,
|
|
52
|
-
}: PanelProps): ReactElement => {
|
|
53
|
-
const panelRef = useRef<Rnd>(null);
|
|
15
|
+
export const Panel = (props: PanelProps): ReactElement => {
|
|
16
|
+
const { title, size = { width: 320, height: 200 }, className, children, onClose } = props;
|
|
54
17
|
|
|
55
|
-
const {
|
|
56
|
-
const { panelPoppedOut,
|
|
57
|
-
useContext(PanelInstanceContext);
|
|
18
|
+
const { dockElements, nextStackPosition } = useContext(PanelsContext);
|
|
19
|
+
const { panelPoppedOut, uniqueId, setTitle, dockId, docked } = useContext(PanelInstanceContext);
|
|
58
20
|
|
|
59
21
|
const onCloseConstFn = useConstFunction(onClose ?? (() => {}));
|
|
60
22
|
|
|
@@ -64,60 +26,34 @@ export const Panel = ({
|
|
|
64
26
|
};
|
|
65
27
|
}, [onCloseConstFn]);
|
|
66
28
|
|
|
67
|
-
const [panelPosition, setPanelPosition] = useState(() => {
|
|
68
|
-
switch (position) {
|
|
69
|
-
case "center":
|
|
70
|
-
return {
|
|
71
|
-
x: Math.max((window.innerWidth - size.width) / 2, 0),
|
|
72
|
-
y: Math.max((window.innerHeight - size.height) / 2, 0),
|
|
73
|
-
};
|
|
74
|
-
case "tile":
|
|
75
|
-
return nextStackPosition();
|
|
76
|
-
default:
|
|
77
|
-
return position ?? nextStackPosition();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const centerWindow = useCallback(() => {
|
|
82
|
-
const b = panelRef.current?.getSelfElement()?.getBoundingClientRect();
|
|
83
|
-
setPanelPosition({
|
|
84
|
-
x: Math.max((window.innerWidth - (b?.width ?? size.width)) / 2, 0),
|
|
85
|
-
y: Math.max((window.innerHeight - (b?.height ?? size.height)) / 2, 0),
|
|
86
|
-
});
|
|
87
|
-
}, [size]);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
if (!panelPoppedOut && position === "center" && !resizeable) {
|
|
91
|
-
centerWindow();
|
|
92
|
-
|
|
93
|
-
window.addEventListener("resize", centerWindow);
|
|
94
|
-
return () => window.removeEventListener("resize", centerWindow);
|
|
95
|
-
}
|
|
96
|
-
return;
|
|
97
|
-
}, [centerWindow, panelPoppedOut, position, resizeable]);
|
|
98
|
-
|
|
99
|
-
const [panelSize, setPanelSize] = useState(size ?? { width: 320, height: 200 });
|
|
100
|
-
|
|
101
|
-
const resizePanel = (newPanelSize: Partial<PanelSize>) => {
|
|
102
|
-
if (panelPoppedOut) return;
|
|
103
|
-
const newSize = { ...panelSize, ...newPanelSize };
|
|
104
|
-
if (newSize.width !== panelSize.width || newSize.height !== panelSize.height) {
|
|
105
|
-
setPanelSize(newSize);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
29
|
useEffect(() => {
|
|
110
30
|
setTitle(title);
|
|
111
31
|
}, [setTitle, title]);
|
|
112
32
|
|
|
113
33
|
const dockElement = docked && dockId && dockElements[dockId];
|
|
114
34
|
|
|
35
|
+
/**
|
|
36
|
+
* This needs to be here, otherwise, every time PopinWindow is rendered, nextStackPosition is recalculated.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const memoizeNextStackPosition = useMemo(
|
|
40
|
+
() => {
|
|
41
|
+
if (props.position === undefined || props.position === "tile") {
|
|
42
|
+
return nextStackPosition();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { x: 0, y: 0 };
|
|
46
|
+
},
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
[],
|
|
49
|
+
);
|
|
50
|
+
|
|
115
51
|
return (
|
|
116
|
-
|
|
52
|
+
<>
|
|
117
53
|
{dockElement && dockElement.isConnected ? (
|
|
118
54
|
createPortal(children, dockElement)
|
|
119
55
|
) : panelPoppedOut ? (
|
|
120
|
-
<PopoutWindow name={uniqueId} title={title} size={
|
|
56
|
+
<PopoutWindow name={uniqueId} title={title} size={size}>
|
|
121
57
|
<div
|
|
122
58
|
style={{
|
|
123
59
|
display: "flex",
|
|
@@ -131,60 +67,11 @@ export const Panel = ({
|
|
|
131
67
|
</div>
|
|
132
68
|
</PopoutWindow>
|
|
133
69
|
) : (
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
style={{
|
|
138
|
-
position: "absolute",
|
|
139
|
-
top: 0,
|
|
140
|
-
left: 0,
|
|
141
|
-
bottom: 0,
|
|
142
|
-
right: 0,
|
|
143
|
-
backgroundColor: "rgb(0,0,0,0.1)",
|
|
144
|
-
zIndex,
|
|
145
|
-
}}
|
|
146
|
-
/>
|
|
147
|
-
)}
|
|
148
|
-
<Rnd
|
|
149
|
-
ref={panelRef}
|
|
150
|
-
className={clsx("WindowPanel", className)}
|
|
151
|
-
dragHandleClassName={"draggable-handle"}
|
|
152
|
-
maxHeight={maxHeight}
|
|
153
|
-
maxWidth={maxWidth}
|
|
154
|
-
minWidth={minWidth}
|
|
155
|
-
minHeight={minHeight}
|
|
156
|
-
position={panelPosition}
|
|
157
|
-
size={panelSize}
|
|
158
|
-
style={{ zIndex }}
|
|
159
|
-
disableDragging={!resizeable}
|
|
160
|
-
enableResizing={resizeable}
|
|
161
|
-
bounds={bounds ?? document.body}
|
|
162
|
-
onDragStop={(_evt, data) => {
|
|
163
|
-
setPanelPosition({ x: data.x, y: data.y });
|
|
164
|
-
}}
|
|
165
|
-
onResizeStop={(_evt, _dir, ref, _delta, position) => {
|
|
166
|
-
setPanelPosition(position);
|
|
167
|
-
setPanelSize({
|
|
168
|
-
width: parseInt(ref.style.width),
|
|
169
|
-
height: parseInt(ref.style.height),
|
|
170
|
-
});
|
|
171
|
-
}}
|
|
172
|
-
onMouseDown={bringPanelToFront}
|
|
173
|
-
>
|
|
174
|
-
<div
|
|
175
|
-
style={{
|
|
176
|
-
display: "flex",
|
|
177
|
-
flexDirection: "column",
|
|
178
|
-
width: "100%",
|
|
179
|
-
height: "100%",
|
|
180
|
-
}}
|
|
181
|
-
>
|
|
182
|
-
{children}
|
|
183
|
-
</div>
|
|
184
|
-
</Rnd>
|
|
185
|
-
</>
|
|
70
|
+
<PopinWindow {...props} nextStackPosition={() => memoizeNextStackPosition}>
|
|
71
|
+
{children}
|
|
72
|
+
</PopinWindow>
|
|
186
73
|
)}
|
|
187
|
-
|
|
74
|
+
</>
|
|
188
75
|
);
|
|
189
76
|
};
|
|
190
77
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PanelInstanceContext } from "./PanelInstanceContext";
|
|
2
2
|
import { PanelInstance, PanelsContext } from "./PanelsContext";
|
|
3
3
|
import { ReactElement, ReactNode, useCallback, useContext, useState } from "react";
|
|
4
|
+
import { useRestoreStateFrom } from "./usePanelStateHandler";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Provides access to closing/popping panels
|
|
@@ -18,7 +19,8 @@ export const PanelInstanceContextProvider = ({
|
|
|
18
19
|
const [title, setTitle] = useState("");
|
|
19
20
|
const [dockId, setDockId] = useState<string>();
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
+
const savedState = useRestoreStateFrom({ uniqueId: panelInstance.uniqueId });
|
|
23
|
+
const [poppedOut, setPoppedOut] = useState(savedState?.panelPoppedOut ?? panelInstance.poppedOut);
|
|
22
24
|
|
|
23
25
|
const togglePopOut = useCallback(() => {
|
|
24
26
|
panelInstance.window = null;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { ReactElement, createContext } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
x: number;
|
|
5
|
-
y: number;
|
|
6
|
-
}
|
|
2
|
+
import { PanelStateOptions } from "./types/PanelStateOptions";
|
|
3
|
+
import { PanelPosition } from "./types";
|
|
7
4
|
|
|
8
5
|
export interface PanelInstance {
|
|
9
6
|
uniqueId: string;
|
|
@@ -27,6 +24,7 @@ export interface PanelsContextType {
|
|
|
27
24
|
nextStackPosition: () => PanelPosition;
|
|
28
25
|
dockElements: Record<string, HTMLDivElement>;
|
|
29
26
|
setDockElement: (id: string, element: HTMLDivElement) => void;
|
|
27
|
+
panelStateOptions?: PanelStateOptions;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
const NoContext = () => {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { PanelInstanceContextProvider } from "./PanelInstanceContextProvider";
|
|
2
|
-
import { OpenPanelOptions, PanelInstance,
|
|
2
|
+
import { OpenPanelOptions, PanelInstance, PanelsContext } from "./PanelsContext";
|
|
3
3
|
import { castArray, maxBy, sortBy } from "lodash-es";
|
|
4
4
|
import React, { Fragment, PropsWithChildren, ReactElement, useCallback, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { useInterval } from "usehooks-ts";
|
|
6
|
+
import { PanelStateOptions } from "./types/PanelStateOptions";
|
|
7
|
+
import { PanelPosition } from "./types";
|
|
6
8
|
|
|
7
9
|
export interface PanelsContextProviderProps {
|
|
8
10
|
baseZIndex?: number;
|
|
9
11
|
bounds?: string | Element;
|
|
12
|
+
panelStateOptions?: PanelStateOptions;
|
|
10
13
|
tilingStart?: PanelPosition;
|
|
11
14
|
tilingOffset?: PanelPosition;
|
|
12
15
|
tilingMax?: PanelPosition;
|
|
@@ -16,6 +19,7 @@ export const PanelsContextProvider = ({
|
|
|
16
19
|
bounds,
|
|
17
20
|
baseZIndex = 500,
|
|
18
21
|
children,
|
|
22
|
+
panelStateOptions,
|
|
19
23
|
tilingStart = { x: 30, y: 30 },
|
|
20
24
|
tilingOffset = { x: 30, y: 50 },
|
|
21
25
|
tilingMax = { x: 200, y: 300 },
|
|
@@ -126,6 +130,7 @@ export const PanelsContextProvider = ({
|
|
|
126
130
|
nextStackPosition,
|
|
127
131
|
dockElements,
|
|
128
132
|
setDockElement,
|
|
133
|
+
panelStateOptions,
|
|
129
134
|
}}
|
|
130
135
|
>
|
|
131
136
|
<Fragment key={"panels"}>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Rnd } from "react-rnd";
|
|
2
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { PanelInstanceContext } from "./PanelInstanceContext";
|
|
5
|
+
import { PanelProps } from "./types/PanelProps";
|
|
6
|
+
import { PanelPosition, PanelSize } from "./types";
|
|
7
|
+
import { PanelContext } from "./PanelContext";
|
|
8
|
+
import { useRestoreStateFrom, useSaveStateIn } from "./usePanelStateHandler";
|
|
9
|
+
|
|
10
|
+
export function PopinWindow({
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
maxHeight,
|
|
14
|
+
maxWidth,
|
|
15
|
+
minHeight,
|
|
16
|
+
minWidth,
|
|
17
|
+
modal,
|
|
18
|
+
nextStackPosition,
|
|
19
|
+
position = modal ? "center" : "tile",
|
|
20
|
+
resizeable = modal !== true,
|
|
21
|
+
size = { height: 200, width: 320 },
|
|
22
|
+
}: PanelProps & { nextStackPosition: () => PanelPosition }): JSX.Element {
|
|
23
|
+
const panelRef = useRef<Rnd>(null);
|
|
24
|
+
|
|
25
|
+
const { bounds, zIndex, bringPanelToFront, uniqueId } = useContext(PanelInstanceContext);
|
|
26
|
+
|
|
27
|
+
const savedState = useRestoreStateFrom({ uniqueId, panelPoppedOut: false });
|
|
28
|
+
|
|
29
|
+
const [panelPosition, setPanelPosition] = useState(() => {
|
|
30
|
+
if (savedState?.panelPosition) {
|
|
31
|
+
return savedState.panelPosition;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
switch (position) {
|
|
35
|
+
case "center":
|
|
36
|
+
return {
|
|
37
|
+
x: Math.max((window.innerWidth - size.width) / 2, 0),
|
|
38
|
+
y: Math.max((window.innerHeight - size.height) / 2, 0),
|
|
39
|
+
};
|
|
40
|
+
case "tile":
|
|
41
|
+
return nextStackPosition();
|
|
42
|
+
default:
|
|
43
|
+
return position ?? nextStackPosition();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const [panelSize, setPanelSize] = useState(() => {
|
|
48
|
+
if (savedState?.panelSize) {
|
|
49
|
+
return savedState.panelSize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return size ?? { width: 320, height: 200 };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const centerWindow = useCallback(() => {
|
|
56
|
+
const b = panelRef.current?.getSelfElement()?.getBoundingClientRect();
|
|
57
|
+
setPanelPosition({
|
|
58
|
+
x: Math.max((window.innerWidth - (b?.width ?? size.width)) / 2, 0),
|
|
59
|
+
y: Math.max((window.innerHeight - (b?.height ?? size.height)) / 2, 0),
|
|
60
|
+
});
|
|
61
|
+
}, [size]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (position === "center" && !resizeable) {
|
|
65
|
+
centerWindow();
|
|
66
|
+
|
|
67
|
+
window.addEventListener("resize", centerWindow);
|
|
68
|
+
return () => window.removeEventListener("resize", centerWindow);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}, [centerWindow, position, resizeable]);
|
|
72
|
+
|
|
73
|
+
const resizePanel = (newPanelSize: Partial<PanelSize>) => {
|
|
74
|
+
const newSize = { ...panelSize, ...newPanelSize };
|
|
75
|
+
if (newSize.width !== panelSize.width || newSize.height !== panelSize.height) {
|
|
76
|
+
setPanelSize(newSize);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const saveStateIn = useSaveStateIn({ uniqueId });
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
saveStateIn({
|
|
84
|
+
panelPosition,
|
|
85
|
+
panelSize,
|
|
86
|
+
});
|
|
87
|
+
}, [panelPosition, panelSize, saveStateIn]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<PanelContext.Provider value={{ resizePanel, resizeable }}>
|
|
91
|
+
{modal && (
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
position: "absolute",
|
|
95
|
+
top: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
bottom: 0,
|
|
98
|
+
right: 0,
|
|
99
|
+
backgroundColor: "rgb(0,0,0,0.1)",
|
|
100
|
+
zIndex,
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
<Rnd
|
|
105
|
+
ref={panelRef}
|
|
106
|
+
className={clsx("WindowPanel", className)}
|
|
107
|
+
dragHandleClassName={"draggable-handle"}
|
|
108
|
+
maxHeight={maxHeight}
|
|
109
|
+
maxWidth={maxWidth}
|
|
110
|
+
minWidth={minWidth}
|
|
111
|
+
minHeight={minHeight}
|
|
112
|
+
position={panelPosition}
|
|
113
|
+
size={panelSize}
|
|
114
|
+
style={{ zIndex }}
|
|
115
|
+
disableDragging={!resizeable}
|
|
116
|
+
enableResizing={resizeable}
|
|
117
|
+
bounds={bounds ?? document.body}
|
|
118
|
+
onDragStop={(_evt, data) => {
|
|
119
|
+
setPanelPosition({ x: data.x, y: data.y });
|
|
120
|
+
}}
|
|
121
|
+
onResizeStop={(_evt, _dir, ref, _delta, position) => {
|
|
122
|
+
setPanelPosition(position);
|
|
123
|
+
setPanelSize({
|
|
124
|
+
width: parseInt(ref.style.width),
|
|
125
|
+
height: parseInt(ref.style.height),
|
|
126
|
+
});
|
|
127
|
+
}}
|
|
128
|
+
onMouseDown={bringPanelToFront}
|
|
129
|
+
>
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
display: "flex",
|
|
133
|
+
flexDirection: "column",
|
|
134
|
+
width: "100%",
|
|
135
|
+
height: "100%",
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
</Rnd>
|
|
141
|
+
</PanelContext.Provider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -5,14 +5,14 @@ import createCache from "@emotion/cache";
|
|
|
5
5
|
import { CacheProvider } from "@emotion/react";
|
|
6
6
|
import { Dispatch, ReactElement, ReactNode, SetStateAction, useContext, useEffect, useRef, useState } from "react";
|
|
7
7
|
import ReactDOM from "react-dom";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { useRestoreStateFrom, useSaveStateIn } from "./usePanelStateHandler";
|
|
9
|
+
import { PanelPosition, PanelSize } from "./types";
|
|
10
10
|
|
|
11
11
|
interface PopoutWindowProps {
|
|
12
12
|
name: string;
|
|
13
13
|
title?: string; // The title of the popout window
|
|
14
14
|
children: ReactNode; // what to render inside the window
|
|
15
|
-
size:
|
|
15
|
+
size: PanelSize;
|
|
16
16
|
|
|
17
17
|
//says if we want to watch for any dynamic style changes
|
|
18
18
|
//only needed when using UI libraries like MUI
|
|
@@ -43,6 +43,7 @@ const openExternalWindow = ({
|
|
|
43
43
|
setHeadElement,
|
|
44
44
|
observeStyleChanges = false,
|
|
45
45
|
className,
|
|
46
|
+
position,
|
|
46
47
|
}: {
|
|
47
48
|
title: string;
|
|
48
49
|
size: { width: number; height: number };
|
|
@@ -50,6 +51,7 @@ const openExternalWindow = ({
|
|
|
50
51
|
setHeadElement: Dispatch<SetStateAction<HTMLElement | undefined>>;
|
|
51
52
|
observeStyleChanges?: boolean;
|
|
52
53
|
className?: string;
|
|
54
|
+
position: PanelPosition;
|
|
53
55
|
}): OpenExternalWindowResponse => {
|
|
54
56
|
const response: OpenExternalWindowResponse = { externalWindow: null, onCloseExternalWindow: null };
|
|
55
57
|
|
|
@@ -59,7 +61,7 @@ const openExternalWindow = ({
|
|
|
59
61
|
// if you open the browser in a window with a scaling and drag it to a window with a different scaling
|
|
60
62
|
const features =
|
|
61
63
|
`scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,` +
|
|
62
|
-
`width=${size.width},height=${size.height},left
|
|
64
|
+
`width=${size.width},height=${size.height},left=${position.x},top=${position.y}`;
|
|
63
65
|
response.externalWindow = window.open("", makePopoutId(title), features);
|
|
64
66
|
|
|
65
67
|
if (response.externalWindow) {
|
|
@@ -138,17 +140,25 @@ export const PopoutWindow = ({
|
|
|
138
140
|
}
|
|
139
141
|
}, [title]);
|
|
140
142
|
|
|
143
|
+
const savedState = useRestoreStateFrom({ panelPoppedOut: true, uniqueId: name });
|
|
144
|
+
|
|
145
|
+
const panelPosition = savedState?.panelPosition ?? { x: 300, y: 200 };
|
|
146
|
+
const panelSize = savedState?.panelSize ?? size;
|
|
147
|
+
|
|
148
|
+
const saveStateIn = useSaveStateIn({ panelPoppedOut: true, uniqueId: name });
|
|
149
|
+
|
|
141
150
|
// When we create this component, open a new window
|
|
142
151
|
useEffect(
|
|
143
152
|
() => {
|
|
144
153
|
const oew = openExternalWindowFn ?? openExternalWindow;
|
|
145
154
|
const { externalWindow, onCloseExternalWindow } = oew({
|
|
146
155
|
title,
|
|
147
|
-
size,
|
|
156
|
+
size: panelSize,
|
|
148
157
|
setContainerElement,
|
|
149
158
|
setHeadElement,
|
|
150
159
|
observeStyleChanges,
|
|
151
160
|
className,
|
|
161
|
+
position: panelPosition,
|
|
152
162
|
});
|
|
153
163
|
|
|
154
164
|
if (!externalWindow) return;
|
|
@@ -159,6 +169,24 @@ export const PopoutWindow = ({
|
|
|
159
169
|
externalWindow.close();
|
|
160
170
|
};
|
|
161
171
|
|
|
172
|
+
const savePanelState = ({ currentTarget }: Event) => {
|
|
173
|
+
const extWindow = currentTarget as Window;
|
|
174
|
+
|
|
175
|
+
saveStateIn({
|
|
176
|
+
panelPosition: {
|
|
177
|
+
x: extWindow.screenX,
|
|
178
|
+
y: extWindow.screenY,
|
|
179
|
+
},
|
|
180
|
+
panelSize: {
|
|
181
|
+
height: extWindow.innerHeight,
|
|
182
|
+
width: extWindow.innerWidth,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
externalWindow.addEventListener("beforeunload", savePanelState);
|
|
188
|
+
externalWindow.addEventListener("resize", savePanelState);
|
|
189
|
+
|
|
162
190
|
// When the main window closes or reloads, close the popout
|
|
163
191
|
window.addEventListener("beforeunload", closeExternalWindow, { once: true });
|
|
164
192
|
|
package/dist/panel/index.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { PanelPosition } from "./PanelPosition";
|
|
3
|
+
import { PanelSize } from "./PanelSize";
|
|
4
|
+
|
|
5
|
+
export interface PanelProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
maxHeight?: number | string;
|
|
9
|
+
maxWidth?: number | string;
|
|
10
|
+
minHeight?: number | string;
|
|
11
|
+
minWidth?: number | string;
|
|
12
|
+
modal?: boolean;
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
position?: PanelPosition | "tile" | "center";
|
|
15
|
+
resizeable?: boolean;
|
|
16
|
+
size?: PanelSize;
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PanelPosition } from "./PanelPosition";
|
|
2
|
+
import { PanelSize } from "./PanelSize";
|
|
3
|
+
|
|
4
|
+
export type PanelMode = "poppedIn" | "poppedOut";
|
|
5
|
+
|
|
6
|
+
export interface PanelLayout {
|
|
7
|
+
panelPosition?: PanelPosition;
|
|
8
|
+
panelSize?: PanelSize;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PanelState {
|
|
12
|
+
mode: PanelMode;
|
|
13
|
+
poppedIn?: PanelLayout;
|
|
14
|
+
poppedOut?: PanelLayout;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PanelRestoredState } from "./PanelRestoredState";
|
|
2
|
+
import { PanelState } from "./PanelState";
|
|
3
|
+
|
|
4
|
+
export type SaveStateInOption = "external" | "localStorage";
|
|
5
|
+
|
|
6
|
+
export type PanelStateOptions = {
|
|
7
|
+
onRestoreState?: () => PanelRestoredState;
|
|
8
|
+
onSaveState?: (panelState: PanelState) => void;
|
|
9
|
+
saveStateIn: SaveStateInOption;
|
|
10
|
+
saveStateKey: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
|
+
import { PanelsContext } from "./PanelsContext";
|
|
3
|
+
import { PanelLayout, PanelMode, PanelRestoredState, PanelState } from "./types";
|
|
4
|
+
|
|
5
|
+
type HookArgs = {
|
|
6
|
+
panelPoppedOut?: boolean;
|
|
7
|
+
uniqueId: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const useRestoreStateFrom = ({ panelPoppedOut, uniqueId }: HookArgs) => {
|
|
11
|
+
const { panelStateOptions } = useContext(PanelsContext);
|
|
12
|
+
|
|
13
|
+
return useMemo((): PanelRestoredState | null | undefined => {
|
|
14
|
+
if (!panelStateOptions) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (panelStateOptions.saveStateIn === "external") {
|
|
19
|
+
return panelStateOptions.onRestoreState?.();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const uniqueKey = createUniqueKey({
|
|
23
|
+
saveStateKey: panelStateOptions.saveStateKey,
|
|
24
|
+
uniqueId,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const storedState = localStorage.getItem(uniqueKey);
|
|
29
|
+
if (storedState) {
|
|
30
|
+
const { mode, ...state } = JSON.parse(storedState) as PanelState;
|
|
31
|
+
|
|
32
|
+
const currentMode: PanelMode = panelPoppedOut === undefined ? mode : panelPoppedOut ? "poppedOut" : "poppedIn";
|
|
33
|
+
const panelLayout = state[currentMode];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
panelPoppedOut: currentMode === "poppedOut",
|
|
37
|
+
...panelLayout,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error("Failed to read stored panel state!", e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}, [panelPoppedOut, panelStateOptions, uniqueId]);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const useSaveStateIn = ({ panelPoppedOut = false, uniqueId }: HookArgs) => {
|
|
49
|
+
const { panelStateOptions } = useContext(PanelsContext);
|
|
50
|
+
return (panelLayout: PanelLayout) => {
|
|
51
|
+
if (!panelStateOptions) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uniqueKey = createUniqueKey({
|
|
56
|
+
saveStateKey: panelStateOptions.saveStateKey,
|
|
57
|
+
uniqueId,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const mode: PanelMode = panelPoppedOut ? "poppedOut" : "poppedIn";
|
|
61
|
+
|
|
62
|
+
if (panelStateOptions.saveStateIn === "external") {
|
|
63
|
+
panelStateOptions.onSaveState?.({
|
|
64
|
+
mode,
|
|
65
|
+
[mode]: panelLayout,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const storedState = localStorage.getItem(uniqueKey);
|
|
71
|
+
|
|
72
|
+
let panelState: PanelState = {
|
|
73
|
+
mode,
|
|
74
|
+
[mode]: panelLayout,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (storedState) {
|
|
78
|
+
try {
|
|
79
|
+
panelState = JSON.parse(storedState) as PanelState;
|
|
80
|
+
|
|
81
|
+
panelState.mode = mode;
|
|
82
|
+
panelState[mode] = {
|
|
83
|
+
...panelState[mode],
|
|
84
|
+
...panelLayout,
|
|
85
|
+
};
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error("Failed to read existing panel state!", e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
localStorage.setItem(uniqueKey, JSON.stringify(panelState));
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error("Failed to save panel state!", e);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function createUniqueKey({ saveStateKey, uniqueId }: { saveStateKey: string; uniqueId: string }): string {
|
|
100
|
+
return `panel-${saveStateKey}-${uniqueId}`;
|
|
101
|
+
}
|