@linzjs/windows 4.0.1 → 4.1.0

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.
@@ -0,0 +1 @@
1
+ export * from "./useConstFunction";
@@ -0,0 +1,16 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * When calling external non-react components, and you can't update the function when state changes,
5
+ * use this to proxy the dynamic function with a static function.
6
+ */
7
+ export const useConstFunction = <Args extends never[], R, TFn extends (...args: Args) => R>(fn: TFn): TFn => {
8
+ const functionRef = useRef<TFn>(fn);
9
+ const proxyRef = useRef<TFn>(((...args: Args) => functionRef.current(...args)) as TFn);
10
+
11
+ useEffect(() => {
12
+ functionRef.current = fn;
13
+ }, [fn]);
14
+
15
+ return proxyRef.current;
16
+ };
package/dist/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./common";
2
2
  export * from "./LuiModalAsync";
3
3
  export * from "./panel";
4
+ export * from "./panel/types";
@@ -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 { PanelContext } from "./PanelContext";
6
- import { PanelInstanceContext, PanelSize } from "./PanelInstanceContext";
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 React, {
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 { Rnd } from "react-rnd";
22
- import { useConstFunction } from "../common/useConstFunction";
11
+ import { PopinWindow } from "./PopinWIndow";
12
+ import { PanelProps } from "./types/PanelProps";
13
+ import { useConstFunction } from "../common";
23
14
 
24
- export interface PanelProps {
25
- title: string;
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 { nextStackPosition, dockElements } = useContext(PanelsContext);
56
- const { panelPoppedOut, bounds, zIndex, bringPanelToFront, uniqueId, setTitle, dockId, docked } =
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
- <PanelContext.Provider value={{ resizePanel, resizeable }}>
52
+ <>
117
53
  {dockElement && dockElement.isConnected ? (
118
54
  createPortal(children, dockElement)
119
55
  ) : panelPoppedOut ? (
120
- <PopoutWindow name={uniqueId} title={title} size={panelSize}>
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
- {modal && (
136
- <div
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
- </PanelContext.Provider>
74
+ </>
188
75
  );
189
76
  };
190
77
 
@@ -1,5 +1,5 @@
1
- import { PanelSize } from "./PanelInstanceContext";
2
1
  import { createContext } from "react";
2
+ import { PanelSize } from "./types";
3
3
 
4
4
  export interface PanelContextType {
5
5
  resizePanel: (size: Partial<PanelSize>) => void;
@@ -1,10 +1,5 @@
1
1
  import { createContext } from "react";
2
2
 
3
- export interface PanelSize {
4
- width: number;
5
- height: number;
6
- }
7
-
8
3
  export interface PanelInstanceContextType {
9
4
  title: string;
10
5
  setTitle: (title: string) => void;
@@ -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 [poppedOut, setPoppedOut] = useState(panelInstance.poppedOut);
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
- export interface PanelPosition {
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, PanelPosition, PanelsContext } from "./PanelsContext";
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
- export type FloatingWindowSize = { height: number; width: number };
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: FloatingWindowSize;
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=300,top=200`;
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
 
@@ -1,3 +1,5 @@
1
+ export * from "./types";
2
+
1
3
  export * from "./OpenPanelButton";
2
4
  export * from "./OpenPanelIcon";
3
5
  export * from "./Panel";
@@ -0,0 +1,4 @@
1
+ export interface PanelPosition {
2
+ x: number;
3
+ y: number;
4
+ }
@@ -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,8 @@
1
+ import { PanelPosition } from "./PanelPosition";
2
+ import { PanelSize } from "./PanelSize";
3
+
4
+ export interface PanelRestoredState {
5
+ panelPoppedOut: boolean;
6
+ panelPosition?: PanelPosition;
7
+ panelSize?: PanelSize;
8
+ }
@@ -0,0 +1,4 @@
1
+ export interface PanelSize {
2
+ height: number;
3
+ width: number;
4
+ }
@@ -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,6 @@
1
+ export * from "./PanelPosition";
2
+ export * from "./PanelProps";
3
+ export * from "./PanelRestoredState";
4
+ export * from "./PanelSize";
5
+ export * from "./PanelState";
6
+ export * from "./PanelStateOptions";
@@ -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
+ }
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "popout"
14
14
  ],
15
15
  "main": "./dist/index.ts",
16
- "version": "4.0.1",
16
+ "version": "4.1.0",
17
17
  "peerDependencies": {
18
18
  "@linzjs/lui": ">=21",
19
19
  "lodash-es": ">=4",