├── .github
├── FUNDING.yml
└── workflows
│ └── build_ghpages.yml
├── .gitignore
├── .gitignore.ghpages
├── README.md
├── dist
├── Container.d.ts
├── Container.jsx
├── Container.jsx.map
├── Panel.d.ts
├── Panel.jsx
├── Panel.jsx.map
├── global.d.ts
├── global.js
├── global.js.map
├── index.d.ts
├── index.js
├── index.js.map
├── rect.d.ts
├── rect.js
├── rect.js.map
├── refState.d.ts
├── refState.js
├── refState.js.map
├── state.d.ts
├── state.js
└── state.js.map
├── example.gif
├── example
├── Clock.tsx
├── Counter.tsx
├── TextEditor.tsx
└── main.tsx
├── index.html
├── package-lock.json
├── package.json
├── src
├── Container.tsx
├── Panel.tsx
├── global.ts
├── index.ts
├── rect.ts
├── refState.ts
└── state.ts
├── tsconfig.json
└── webpack.config.cjs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: hlorenzi
2 | ko_fi: hlorenzi
--------------------------------------------------------------------------------
/.github/workflows/build_ghpages.yml:
--------------------------------------------------------------------------------
1 | name: Build GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 |
7 | jobs:
8 | build:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@master
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Use Node.js 15.x
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 15.x
22 |
23 | - run: git branch -D ghpages
24 | continue-on-error: true
25 |
26 | - run: git checkout -b ghpages
27 | - run: rm .gitignore
28 | - run: mv .gitignore.ghpages .gitignore
29 | - run: npm ci
30 | - run: npm run build2
31 | - run: git config user.name github-actions
32 | - run: git config user.email github-actions@github.com
33 | - run: git add -A
34 | - run: git commit -m "build GitHub Pages"
35 | - run: git push -f origin ghpages
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /build/
--------------------------------------------------------------------------------
/.gitignore.ghpages:
--------------------------------------------------------------------------------
1 | /node_modules/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-dockable
2 |
3 | 
4 |
5 | An easy-to-use dockable window manager for React,
6 | fully embracing hooks!
7 |
8 | [Try it right now!](https://hlorenzi.github.io/react-dockable/)
9 |
10 | Your custom content's lifecycle hooks are respected,
11 | so `useState`, `useEffect`, etc. work out of the box,
12 | and state carries over even if the user rearranges
13 | their panels.
14 |
15 | [![npm][badge-npm-img]][badge-npm-url]
16 |
17 | [![Discord][badge-discord-img]][badge-discord-url]
18 |
19 | [badge-discord-img]: https://img.shields.io/discord/394999035540275222?label=Join%20the%20Discord%20server!&logo=discord
20 | [badge-discord-url]: https://discord.com/invite/pXeDXGD
21 |
22 | [badge-npm-img]: https://img.shields.io/npm/v/@hlorenzi/react-dockable
23 | [badge-npm-url]: https://www.npmjs.com/package/@hlorenzi/react-dockable
24 |
25 | ## Installation
26 |
27 | ```
28 | npm install @hlorenzi/react-dockable
29 | ```
30 |
31 | ## Example
32 |
33 | ```tsx
34 | import * as React from "react"
35 | import * as Dockable from "@hlorenzi/react-dockable"
36 |
37 | function App()
38 | {
39 | // Create the base state,
40 | // and set up initial content
41 | const state = Dockable.useDockable((state) =>
42 | {
43 | Dockable.createDockedPanel(
44 | state, state.rootPanel, Dockable.DockMode.Full,
45 | )
46 | })
47 |
48 | // Render the root Container element,
49 | // which handles all interactions on your behalf
50 | return
54 |
55 |
56 |
57 |
58 | }
59 |
60 | // Your custom element!
61 | function Counter()
62 | {
63 | const [value, setValue] = React.useState(0)
64 | const countUp = () => setValue(value + 1)
65 |
66 | const ctx = Dockable.useContentContext()
67 | ctx.setTitle(`Count: ${ value }`)
68 | ctx.setPreferredSize(300, 250)
69 |
70 | return
71 | { value }
72 |
73 |
74 | }
75 | ```
76 |
--------------------------------------------------------------------------------
/dist/Container.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as Dockable from "./index.js";
3 | export declare function Container(props: {
4 | state: Dockable.RefState;
5 | anchorSize?: number;
6 | resizeHandleSize?: number;
7 | dividerSize?: number;
8 | tabHeight?: number;
9 | }): JSX.Element;
10 |
--------------------------------------------------------------------------------
/dist/Container.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Dockable from "./index.js";
3 | import styled from "styled-components";
4 | const StyledContainer = styled.div `
5 | --dockable-voidBkg: #252525;
6 | --dockable-panelBkg: #1e1e1e;
7 | --dockable-panelInactiveBorder: #393939;
8 | --dockable-panelActiveBorder: #777777;
9 | --dockable-panelTabBkg: #2d2d2d;
10 | --dockable-panelTabTextColor: #ffffff;
11 | --dockable-overlayColor: #00aaff44;
12 | --dockable-anchorColor: #00aaff;
13 | --dockable-buttonHoverBkg: #323232;
14 | --dockable-scrollbarColor: #777777;
15 |
16 | width: 100%;
17 | height: 100%;
18 | background-color: var(--dockable-voidBkg);
19 | `;
20 | const StyledContentRoot = styled.div `
21 | display: ${props => props.isCurrentTab ? "grid" : "none"};
22 | grid-template: 100% / 100%;
23 |
24 | position: absolute;
25 | box-sizing: border-box;
26 | contain: strict;
27 |
28 | color: #fff;
29 | text-align: left;
30 |
31 | background-color: transparent;
32 | overflow: hidden;
33 | `;
34 | const StyledContentInner = styled.div `
35 | grid-row: 1;
36 | grid-column: 1;
37 | width: 100%;
38 | height: 100%;
39 | `;
40 | const StyledBottomRightResizeHandle = styled.div `
41 | width: ${props => props.size}px;
42 | height: ${props => props.size}px;
43 |
44 | grid-row: 1;
45 | grid-column: 1;
46 | align-self: end;
47 | justify-self: end;
48 |
49 | cursor: nwse-resize;
50 | z-index: 1;
51 |
52 | &:hover
53 | {
54 | background-color: var(--dockable-overlayColor);
55 | }
56 | `;
57 | const StyledDivider = styled.div `
58 | &:hover
59 | {
60 | background-color: var(--dockable-overlayColor);
61 | }
62 | `;
63 | export function Container(props) {
64 | const [rect, setRect] = React.useState(new Dockable.Rect(0, 0, 0, 0));
65 | const rootRef = React.useRef(null);
66 | const anchorSize = props.anchorSize ?? 5;
67 | const resizeHandleSize = props.anchorSize ?? 10;
68 | const dividerSize = props.anchorSize ?? 6;
69 | const tabHeight = props.anchorSize ?? 25;
70 | React.useLayoutEffect(() => {
71 | const onResize = () => {
72 | if (!rootRef.current)
73 | return;
74 | const elemRect = rootRef.current.getBoundingClientRect();
75 | setRect(new Dockable.Rect(elemRect.x, elemRect.y, elemRect.width, elemRect.height));
76 | };
77 | onResize();
78 | window.addEventListener("resize", onResize);
79 | return () => window.removeEventListener("resize", onResize);
80 | }, []);
81 | const rectRef = React.useRef(null);
82 | rectRef.current = rect;
83 | const layoutRef = React.useRef(null);
84 | layoutRef.current = React.useMemo(() => {
85 | return Dockable.getLayout(props.state.ref.current, new Dockable.Rect(rect.x, rect.y, rect.w - 1, rect.h - 1));
86 | }, [rect, props.state.updateToken]);
87 | const setTitle = (layoutContent, title) => {
88 | if (layoutContent.content.title != title) {
89 | window.requestAnimationFrame(() => {
90 | layoutContent.content.title = title;
91 | props.state.commit();
92 | });
93 | }
94 | };
95 | const setPreferredSize = (layoutContent, width, height) => {
96 | if (layoutContent.tabIndex == layoutContent.panel.currentTabIndex &&
97 | (width != layoutContent.panel.preferredWidth ||
98 | height != layoutContent.panel.preferredHeight)) {
99 | window.requestAnimationFrame(() => {
100 | layoutContent.panel.preferredWidth = width;
101 | layoutContent.panel.preferredHeight = height;
102 | layoutContent.panel.rect = new Dockable.Rect(layoutContent.panel.rect.x, layoutContent.panel.rect.y, width, height);
103 | props.state.commit();
104 | });
105 | }
106 | };
107 | return
108 |
109 | {layoutRef.current.panelRects.map(panelRect => handleClickedPanel(props.state, panelRect.panel, null)} onClickTab={(tabNumber) => handleClickedPanel(props.state, panelRect.panel, tabNumber)} onCloseTab={(ev, tabNumber) => handleClosedTab(ev, props.state, panelRect.panel, tabNumber)} onDragHeader={(ev, tabNumber) => handleDraggedHeader(ev, props.state, layoutRef, rectRef, panelRect.panel, tabNumber)}/>)}
110 |
111 | {layoutRef.current.content.map(layoutContent => handleClickedPanel(props.state, layoutContent.panel, null)} style={{
112 | left: (layoutContent.layoutPanel.rect.x) + "px",
113 | top: (layoutContent.layoutPanel.rect.y + tabHeight) + "px",
114 | width: (layoutContent.layoutPanel.rect.w) + "px",
115 | height: (layoutContent.layoutPanel.rect.h - tabHeight) + "px",
116 | zIndex: layoutContent.layoutPanel.zIndex * 3 + 1,
117 | }}>
118 | setTitle(layoutContent, title),
121 | setPreferredSize: (w, h) => setPreferredSize(layoutContent, w, h),
122 | }}>
123 |
124 | {layoutContent.content.element}
125 |
126 |
127 |
128 | {layoutContent.panel.floating &&
129 | {
130 | handleClickedPanel(props.state, layoutContent.panel, null);
131 | handleDraggedEdge(ev, props.state, layoutRef, layoutContent.panel);
132 | }}/>}
133 | )}
134 |
135 | {layoutRef.current.dividers.map((divider, i) => handleDraggedDivider(ev, props.state, divider)} style={{
136 | width: (divider.rect.w || dividerSize) + "px",
137 | height: (divider.rect.h || dividerSize) + "px",
138 | position: "absolute",
139 | left: (divider.rect.x - (!divider.vertical ? dividerSize / 2 : 0)) + "px",
140 | top: (divider.rect.y - (divider.vertical ? dividerSize / 2 : 0)) + "px",
141 | cursor: !divider.vertical ?
142 | "ew-resize" :
143 | "ns-resize",
144 | zIndex: 1,
145 | userSelect: "none",
146 | }}/>)}
147 |
148 | {props.state.ref.current.previewAnchor &&
149 | }
158 |
159 | {props.state.ref.current.showAnchors &&
160 | layoutRef.current.anchors.map((anchor, i) => props.state.ref.current.draggedPanel !== anchor.panel &&
161 | )}
173 |
174 | ;
175 | }
176 | function handleDraggedDivider(ev, state, divider) {
177 | ev.preventDefault();
178 | const onMouseMove = (ev) => {
179 | const mouseX = ev.pageX;
180 | const mouseY = ev.pageY;
181 | divider.panel.splitSize =
182 | Math.max(0.05, Math.min(0.95, ((divider.vertical ? mouseY : mouseX) - divider.resizeMin) /
183 | (divider.resizeMax - divider.resizeMin)));
184 | state.commit();
185 | };
186 | const onMouseUp = () => {
187 | window.removeEventListener("mousemove", onMouseMove);
188 | window.removeEventListener("mouseup", onMouseUp);
189 | };
190 | window.addEventListener("mousemove", onMouseMove);
191 | window.addEventListener("mouseup", onMouseUp);
192 | }
193 | function handleDraggedEdge(ev, state, layout, panel) {
194 | ev.preventDefault();
195 | ev.stopPropagation();
196 | const startMouseX = ev.pageX;
197 | const startMouseY = ev.pageY;
198 | const layoutPanel = layout.current.panelRects.find(p => p.panel === panel);
199 | const startPanelRect = layoutPanel.rect;
200 | const onMouseMove = (ev) => {
201 | const mouseX = ev.pageX;
202 | const mouseY = ev.pageY;
203 | panel.rect = new Dockable.Rect(startPanelRect.x, startPanelRect.y, Math.max(150, startPanelRect.w + mouseX - startMouseX), Math.max(50, startPanelRect.h + mouseY - startMouseY));
204 | state.commit();
205 | };
206 | const onMouseUp = () => {
207 | window.removeEventListener("mousemove", onMouseMove);
208 | window.removeEventListener("mouseup", onMouseUp);
209 | };
210 | window.addEventListener("mousemove", onMouseMove);
211 | window.addEventListener("mouseup", onMouseUp);
212 | }
213 | function handleDraggedHeader(ev, state, layout, containerRect, draggedPanel, draggedTabIndex) {
214 | ev.preventDefault();
215 | ev.stopPropagation();
216 | const startMouseX = ev.pageX;
217 | const startMouseY = ev.pageY;
218 | const layoutPanel = layout.current.panelRects.find(p => p.panel === draggedPanel);
219 | let startPanelRect = layoutPanel.rect;
220 | let dragLocked = true;
221 | const onMouseMove = (ev) => {
222 | const mouseX = ev.pageX;
223 | const mouseY = ev.pageY;
224 | // Start dragging only when mouse moves far enough, and
225 | // undock panel at this moment if originally docked
226 | if (Math.abs(mouseX - startMouseX) > 10 ||
227 | Math.abs(mouseY - startMouseY) > 10) {
228 | dragLocked = false;
229 | const floatingRect = new Dockable.Rect(mouseX - Math.min(draggedPanel.preferredWidth / 2, mouseX - startPanelRect.x), mouseY - (mouseY - startPanelRect.y), draggedPanel.preferredWidth, draggedPanel.preferredHeight);
230 | if (draggedTabIndex !== null && draggedPanel.contentList.length > 1) {
231 | // Remove single tab content from original panel and
232 | // transfer it to a new floating panel
233 | const content = draggedPanel.contentList[draggedTabIndex];
234 | Dockable.removeContent(state.ref.current, draggedPanel, content.contentId);
235 | draggedPanel = Dockable.makePanel(state.ref.current);
236 | Dockable.addContent(state.ref.current, draggedPanel, content);
237 | Dockable.coallesceEmptyPanels(state.ref.current);
238 | draggedPanel.rect = startPanelRect = floatingRect;
239 | }
240 | else if (!draggedPanel.floating) {
241 | // Remove original docked panel and
242 | // transfer all content to a new floating panel
243 | const contents = [...draggedPanel.contentList];
244 | const originalTabIndex = draggedPanel.currentTabIndex;
245 | for (const content of contents)
246 | Dockable.removeContent(state.ref.current, draggedPanel, content.contentId);
247 | draggedPanel = Dockable.makePanel(state.ref.current);
248 | for (const content of contents)
249 | Dockable.addContent(state.ref.current, draggedPanel, content);
250 | draggedPanel.currentTabIndex = originalTabIndex;
251 | Dockable.coallesceEmptyPanels(state.ref.current);
252 | draggedPanel.rect = startPanelRect = floatingRect;
253 | }
254 | state.ref.current.draggedPanel = draggedPanel;
255 | state.ref.current.showAnchors = true;
256 | state.commit();
257 | }
258 | // Handle actual dragging
259 | if (!dragLocked) {
260 | // Move panel rect
261 | draggedPanel.rect = startPanelRect.displace(mouseX - startMouseX, mouseY - startMouseY);
262 | // Find nearest anchor
263 | let nearestDistSqr = 50 * 50;
264 | state.ref.current.previewAnchor = null;
265 | for (const anchor of layout.current.anchors) {
266 | if (anchor.panel === draggedPanel)
267 | continue;
268 | const xx = anchor.x - mouseX;
269 | const yy = anchor.y - mouseY;
270 | const distSqr = xx * xx + yy * yy;
271 | if (distSqr < nearestDistSqr) {
272 | nearestDistSqr = distSqr;
273 | state.ref.current.previewAnchor = anchor;
274 | }
275 | }
276 | state.commit();
277 | }
278 | };
279 | const onMouseUp = () => {
280 | window.removeEventListener("mousemove", onMouseMove);
281 | window.removeEventListener("mouseup", onMouseUp);
282 | // Dock dragged panel if near an anchor
283 | if (state.ref.current.previewAnchor) {
284 | Dockable.dock(state.ref.current, draggedPanel, state.ref.current.previewAnchor.panel, state.ref.current.previewAnchor.mode);
285 | }
286 | Dockable.clampFloatingPanels(state.ref.current, containerRect.current);
287 | state.ref.current.draggedPanel = null;
288 | state.ref.current.showAnchors = false;
289 | state.ref.current.previewAnchor = null;
290 | state.commit();
291 | };
292 | window.addEventListener("mousemove", onMouseMove);
293 | window.addEventListener("mouseup", onMouseUp);
294 | }
295 | function handleClickedPanel(state, clickedPanel, tabNumber) {
296 | if (tabNumber !== null) {
297 | clickedPanel.currentTabIndex = tabNumber;
298 | }
299 | Dockable.setPanelActiveAndBringToFront(state.ref.current, clickedPanel);
300 | state.commit();
301 | }
302 | function handleClosedTab(ev, state, panel, tabNumber) {
303 | ev.preventDefault();
304 | const content = panel.contentList[tabNumber];
305 | Dockable.removeContent(state.ref.current, panel, content.contentId);
306 | Dockable.coallesceEmptyPanels(state.ref.current);
307 | state.commit();
308 | }
309 | //# sourceMappingURL=Container.jsx.map
--------------------------------------------------------------------------------
/dist/Container.jsx.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"Container.jsx","sourceRoot":"","sources":["../src/Container.tsx"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/Panel.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Dockable from "./index.js";
3 | export declare function ContainerPanel(props: {
4 | state: Dockable.RefState;
5 | panelRect: Dockable.LayoutPanel;
6 | tabHeight: number;
7 | onClickPanel: () => void;
8 | onClickTab: (tabNumber: number) => void;
9 | onCloseTab: (ev: React.MouseEvent, tabNumber: number) => void;
10 | onDragHeader: (ev: React.MouseEvent, tabNumber: number | null) => void;
11 | }): JSX.Element;
12 |
--------------------------------------------------------------------------------
/dist/Panel.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 | const StyledPanelRoot = styled.div `
4 | position: absolute;
5 | box-sizing: border-box;
6 | contain: strict;
7 | `;
8 | const StyledTabRow = styled.div `
9 | background-color: var(--dockable-panelBkg);
10 | box-sizing: border-box;
11 | width: 100%;
12 | height: 100%;
13 |
14 | display: grid;
15 | grid-template: auto 1fr / 1fr;
16 | overflow: hidden;
17 |
18 | border: 1px solid var(--dockable-panelInactiveBorder);
19 |
20 | &.active
21 | {
22 | border: 1px solid var(--dockable-panelActiveBorder);
23 | }
24 | `;
25 | const StyledTabRowInner = styled.div `
26 | background-color: var(--dockable-voidBkg);
27 | text-align: left;
28 | grid-row: 1;
29 | grid-column: 1;
30 |
31 | display: grid;
32 | grid-template: ${props => props.tabHeight}px / repeat(${props => props.tabCount}, auto) 1fr;
33 | grid-auto-flow: column;
34 |
35 | height: ${props => props.tabHeight}px;
36 |
37 | overflow-x: auto;
38 | overflow-y: hidden;
39 | user-select: none;
40 |
41 | &::-webkit-scrollbar
42 | {
43 | width: 4px;
44 | height: 4px;
45 | }
46 |
47 | &::-webkit-scrollbar-track
48 | {
49 | background: var(--dockable-panelBkg);
50 | }
51 |
52 | &::-webkit-scrollbar-thumb
53 | {
54 | background-color: var(--dockable-scrollbarColor);
55 | border-radius: 0;
56 | border: 0;
57 | }
58 | `;
59 | const StyledTab = styled.div `
60 | grid-row: 1;
61 | grid-column: ${props => props.tabNumber + 1};
62 |
63 | display: grid;
64 | grid-template: auto / auto auto;
65 | justify-items: start;
66 | align-items: center;
67 |
68 | min-width: max-content;
69 | height: 100%;
70 | box-sizing: border-box;
71 | margin-right: 1px;
72 | padding-left: 0.75em;
73 | padding-right: 0.5em;
74 | user-select: none;
75 |
76 | color: var(--dockable-panelTabTextColor);
77 | background-color: ${props => props.isCurrentTab ?
78 | "var(--dockable-panelBkg)" :
79 | "var(--dockable-panelTabBkg)"};
80 | `;
81 | const StyledCloseButton = styled.button `
82 | pointer-events: auto;
83 | border: 0;
84 | border-radius: 0.25em;
85 | background-color: transparent;
86 | padding: 0.1em 0.3em;
87 | cursor: pointer;
88 | margin-left: 0.25em;
89 | width: 1.5em;
90 | height: 1.5em;
91 |
92 | color: ${props => props.isCurrentTab ? "var(--dockable-panelTabTextColor)" : "transparent"};
93 |
94 | &:hover
95 | {
96 | background-color: var(--dockable-buttonHoverBkg);
97 | color: var(--dockable-panelTabTextColor);
98 | }
99 |
100 | &:active
101 | {
102 | background-color: var(--dockable-buttonHoverBkg);
103 | color: var(--dockable-panelTabTextColor);
104 | }
105 | `;
106 | const StyledTabRowEmptySpace = styled.div `
107 | min-width: 2em;
108 | pointer-events: none;
109 | `;
110 | export function ContainerPanel(props) {
111 | const panelRect = props.panelRect;
112 | const isActivePanel = props.state.ref.current.activePanel === panelRect.panel;
113 | return
120 |
121 | {
122 | props.onClickPanel();
123 | props.onDragHeader(ev, null);
124 | }}>
125 |
126 | {panelRect.panel.contentList.map((content, tabNumber) => {
127 | props.onClickTab(tabNumber);
128 | props.onDragHeader(ev, tabNumber);
129 | }}>
130 | {content.title || `Content ${content.contentId}`}
131 | {
132 | props.onClickTab(tabNumber);
133 | props.onCloseTab(ev, tabNumber);
134 | }}>
135 | ×
136 |
137 | )}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | ;
146 | }
147 | //# sourceMappingURL=Panel.jsx.map
--------------------------------------------------------------------------------
/dist/Panel.jsx.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"Panel.jsx","sourceRoot":"","sources":["../src/Panel.tsx"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/global.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Dockable from "./index.js";
3 | export declare function useDockable(init?: (state: Dockable.State) => void): Dockable.RefState;
4 | interface MousePos {
5 | x: number;
6 | y: number;
7 | }
8 | export declare const mousePos: MousePos;
9 | export declare function spawnFloating(state: Dockable.RefState, elem: JSX.Element): Dockable.Panel;
10 | export declare function spawnFloatingEphemeral(state: Dockable.RefState, elem: JSX.Element): Dockable.Panel;
11 | export interface ContentContextProps {
12 | layoutContent: Dockable.LayoutContent;
13 | setTitle: (title: string) => void;
14 | setPreferredSize: (w: number, h: number) => void;
15 | }
16 | export declare const ContentContext: React.Context;
17 | export declare function useContentContext(): ContentContextProps;
18 | export {};
19 |
--------------------------------------------------------------------------------
/dist/global.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Dockable from "./index.js";
3 | export function useDockable(init) {
4 | return Dockable.useRefState(() => {
5 | const state = Dockable.makeState();
6 | if (init)
7 | init(state);
8 | return state;
9 | });
10 | }
11 | export const mousePos = {
12 | x: 0,
13 | y: 0,
14 | };
15 | window.addEventListener("mousemove", (ev) => {
16 | mousePos.x = ev.pageX;
17 | mousePos.y = ev.pageY;
18 | });
19 | export function spawnFloating(state, elem) {
20 | const panel = Dockable.makePanel(state.ref.current);
21 | Dockable.addNewContent(state.ref.current, panel, elem);
22 | panel.rect = new Dockable.Rect(mousePos.x, mousePos.y, 500, 300);
23 | state.ref.current.activePanel = panel;
24 | state.commit();
25 | return panel;
26 | }
27 | export function spawnFloatingEphemeral(state, elem) {
28 | const panel = spawnFloating(state, elem);
29 | Dockable.removeEphemerals(state.ref.current);
30 | panel.ephemeral = true;
31 | state.commit();
32 | return panel;
33 | }
34 | export const ContentContext = React.createContext(null);
35 | export function useContentContext() {
36 | return React.useContext(ContentContext);
37 | }
38 | //# sourceMappingURL=global.js.map
--------------------------------------------------------------------------------
/dist/global.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"global.js","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAGtC,MAAM,UAAU,WAAW,CACvB,IAAsC;IAGtC,OAAO,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE;QAE7B,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAA;QAElC,IAAI,IAAI;YACJ,IAAI,CAAC,KAAK,CAAC,CAAA;QAEf,OAAO,KAAK,CAAA;IAChB,CAAC,CAAC,CAAA;AACN,CAAC;AAUD,MAAM,CAAC,MAAM,QAAQ,GACrB;IACI,CAAC,EAAE,CAAC;IACJ,CAAC,EAAE,CAAC;CACP,CAAA;AAGD,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,EAAc,EAAE,EAAE;IAEpD,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAA;IACrB,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAA;AACzB,CAAC,CAAC,CAAA;AAGF,MAAM,UAAU,aAAa,CACzB,KAAwC,EACxC,IAAiB;IAGjB,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACnD,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;IACtD,KAAK,CAAC,IAAI,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IAEhE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAA;IACrC,KAAK,CAAC,MAAM,EAAE,CAAA;IACd,OAAO,KAAK,CAAA;AAChB,CAAC;AAGD,MAAM,UAAU,sBAAsB,CAClC,KAAwC,EACxC,IAAiB;IAGjB,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACxC,QAAQ,CAAC,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5C,KAAK,CAAC,SAAS,GAAG,IAAI,CAAA;IACtB,KAAK,CAAC,MAAM,EAAE,CAAA;IACd,OAAO,KAAK,CAAA;AAChB,CAAC;AAYD,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,aAAa,CAAsB,IAAK,CAAC,CAAA;AAG7E,MAAM,UAAU,iBAAiB;IAE7B,OAAO,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,CAAA;AAC3C,CAAC"}
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./global.js";
2 | export * from "./state.js";
3 | export * from "./Container.jsx";
4 | export * from "./Panel.jsx";
5 | export * from "./rect.js";
6 | export * from "./refState.js";
7 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | export * from "./global.js";
2 | export * from "./state.js";
3 | export * from "./Container.jsx";
4 | export * from "./Panel.jsx";
5 | export * from "./rect.js";
6 | export * from "./refState.js";
7 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
--------------------------------------------------------------------------------
/dist/rect.d.ts:
--------------------------------------------------------------------------------
1 | export declare class Rect {
2 | x: number;
3 | y: number;
4 | w: number;
5 | h: number;
6 | constructor(x: number, y: number, w: number, h: number);
7 | static fromVertices(x1: number, y1: number, x2: number, y2: number): Rect;
8 | static fromElement(elem: HTMLElement): Rect;
9 | clone(): Rect;
10 | get x1(): number;
11 | get y1(): number;
12 | get x2(): number;
13 | get y2(): number;
14 | get xCenter(): number;
15 | get yCenter(): number;
16 | withX(value: number): Rect;
17 | withY(value: number): Rect;
18 | withW(value: number): Rect;
19 | withH(value: number): Rect;
20 | withX1(value: number): Rect;
21 | withY1(value: number): Rect;
22 | withX2(value: number): Rect;
23 | withY2(value: number): Rect;
24 | displace(x: number, y: number): Rect;
25 | expand(amount: number): Rect;
26 | expandW(amount: number): Rect;
27 | contains(p: {
28 | x: number;
29 | y: number;
30 | }): boolean;
31 | overlaps(other: Rect): boolean;
32 | }
33 |
--------------------------------------------------------------------------------
/dist/rect.js:
--------------------------------------------------------------------------------
1 | export class Rect {
2 | x;
3 | y;
4 | w;
5 | h;
6 | constructor(x, y, w, h) {
7 | this.x = x;
8 | this.y = y;
9 | this.w = w;
10 | this.h = h;
11 | }
12 | static fromVertices(x1, y1, x2, y2) {
13 | return new Rect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
14 | }
15 | static fromElement(elem) {
16 | const clientRect = elem.getBoundingClientRect();
17 | return new Rect(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
18 | }
19 | clone() {
20 | return new Rect(this.x, this.y, this.w, this.h);
21 | }
22 | get x1() {
23 | return this.x;
24 | }
25 | get y1() {
26 | return this.y;
27 | }
28 | get x2() {
29 | return this.x + this.w;
30 | }
31 | get y2() {
32 | return this.y + this.h;
33 | }
34 | get xCenter() {
35 | return (this.x1 + this.x2) / 2;
36 | }
37 | get yCenter() {
38 | return (this.y1 + this.y2) / 2;
39 | }
40 | withX(value) {
41 | return new Rect(value, this.y, this.w, this.h);
42 | }
43 | withY(value) {
44 | return new Rect(this.x, value, this.w, this.h);
45 | }
46 | withW(value) {
47 | return new Rect(this.x, this.y, value, this.h);
48 | }
49 | withH(value) {
50 | return new Rect(this.x, this.y, this.w, value);
51 | }
52 | withX1(value) {
53 | return Rect.fromVertices(value, this.y1, this.x2, this.y2);
54 | }
55 | withY1(value) {
56 | return Rect.fromVertices(this.x1, value, this.x2, this.y2);
57 | }
58 | withX2(value) {
59 | return Rect.fromVertices(this.x1, this.y1, value, this.y2);
60 | }
61 | withY2(value) {
62 | return Rect.fromVertices(this.x1, this.y1, this.x2, value);
63 | }
64 | displace(x, y) {
65 | return new Rect(this.x + x, this.y + y, this.w, this.h);
66 | }
67 | expand(amount) {
68 | return Rect.fromVertices(this.x1 - amount, this.y1 - amount, this.x2 + amount, this.y2 + amount);
69 | }
70 | expandW(amount) {
71 | return Rect.fromVertices(this.x1 - amount, this.y1, this.x2 + amount, this.y2);
72 | }
73 | contains(p) {
74 | return p.x >= this.x &&
75 | p.x < this.x2 &&
76 | p.y >= this.y &&
77 | p.y < this.y2;
78 | }
79 | overlaps(other) {
80 | return this.x2 >= other.x &&
81 | this.x < other.x2 &&
82 | this.y2 >= other.y &&
83 | this.y < other.y2;
84 | }
85 | }
86 | //# sourceMappingURL=rect.js.map
--------------------------------------------------------------------------------
/dist/rect.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"rect.js","sourceRoot":"","sources":["../src/rect.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/refState.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | export interface RefState {
3 | ref: React.MutableRefObject;
4 | updateToken: number;
5 | commit: () => void;
6 | }
7 | export declare function useRefState(initializer: () => T): RefState;
8 |
--------------------------------------------------------------------------------
/dist/refState.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | export function useRefState(initializer) {
3 | const [updateToken, setUpdateToken] = React.useState(0);
4 | const ref = React.useRef(null);
5 | if (ref.current === null)
6 | ref.current = initializer();
7 | return {
8 | ref,
9 | updateToken,
10 | commit: () => setUpdateToken(n => (n + 1) % 1000000)
11 | };
12 | }
13 | //# sourceMappingURL=refState.js.map
--------------------------------------------------------------------------------
/dist/refState.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"refState.js","sourceRoot":"","sources":["../src/refState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAW9B,MAAM,UAAU,WAAW,CAAI,WAAoB;IAE/C,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEvD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAI,IAAK,CAAC,CAAA;IAClC,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI;QACpB,GAAG,CAAC,OAAO,GAAG,WAAW,EAAE,CAAA;IAE/B,OAAO;QACH,GAAG;QACH,WAAW;QACX,MAAM,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC;KACvD,CAAA;AACL,CAAC"}
--------------------------------------------------------------------------------
/dist/state.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Rect } from "./rect.js";
3 | export declare type PanelId = number;
4 | export declare type ContentId = number;
5 | export declare type ContentElement = JSX.Element;
6 | export interface State {
7 | idNext: number;
8 | rootPanel: Panel;
9 | floatingPanels: Panel[];
10 | activePanel: Panel | null;
11 | draggedPanel: Panel | null;
12 | showAnchors: boolean;
13 | previewAnchor: Anchor | null;
14 | }
15 | export declare enum SplitMode {
16 | LeftRight = 0,
17 | TopBottom = 1
18 | }
19 | export declare enum DockMode {
20 | Full = 0,
21 | Left = 1,
22 | Right = 2,
23 | Top = 3,
24 | Bottom = 4
25 | }
26 | export interface Panel {
27 | id: PanelId;
28 | floating: boolean;
29 | rect: Rect;
30 | contentList: Content[];
31 | currentTabIndex: number;
32 | splitPanels: Panel[];
33 | splitMode: SplitMode;
34 | splitSize: number;
35 | preferredWidth: number;
36 | preferredHeight: number;
37 | ephemeral: boolean;
38 | }
39 | export interface Content {
40 | contentId: ContentId;
41 | title: string;
42 | element: JSX.Element;
43 | }
44 | export interface Divider {
45 | panel: Panel;
46 | vertical: boolean;
47 | rect: Rect;
48 | resizeMin: number;
49 | resizeMax: number;
50 | }
51 | export interface Anchor {
52 | panel: Panel;
53 | x: number;
54 | y: number;
55 | mode: DockMode;
56 | previewRect: Rect;
57 | }
58 | export interface Layout {
59 | panelRects: LayoutPanel[];
60 | content: LayoutContent[];
61 | dividers: Divider[];
62 | anchors: Anchor[];
63 | }
64 | export interface LayoutPanel {
65 | panel: Panel;
66 | rect: Rect;
67 | floating: boolean;
68 | zIndex: number;
69 | }
70 | export interface LayoutContent {
71 | content: Content;
72 | tabIndex: number;
73 | panel: Panel;
74 | layoutPanel: LayoutPanel;
75 | }
76 | export declare function makeState(): State;
77 | export declare function makePanel(state: State): Panel;
78 | export declare function createDockedPanel(state: State, dockIntoPanel: Panel, mode: DockMode, content: ContentElement): Panel;
79 | export declare function detachPanel(state: State, panel: Panel): void;
80 | export declare function addNewContent(state: State, toPanel: Panel, element: ContentElement): void;
81 | export declare function addContent(state: State, toPanel: Panel, content: Content): void;
82 | export declare function removeContent(state: State, fromPanel: Panel, contentId: ContentId): void;
83 | export declare function removeEphemerals(state: State): void;
84 | export declare function removeEphemeralsRecursive(state: State, fromPanel: Panel): void;
85 | export declare function coallesceEmptyPanels(state: State): void;
86 | export declare function coallesceEmptyPanelsRecursive(state: State, fromPanel: Panel): void;
87 | export declare function dock(state: State, panel: Panel, dockIntoPanel: Panel, mode: DockMode): void;
88 | export declare function setPanelActiveAndBringToFront(state: State, panel: Panel): void;
89 | export declare function clampFloatingPanels(state: State, rect: Rect): void;
90 | export declare function clampFloatingPanelStrictly(state: State, panel: Panel, rect: Rect): void;
91 | export declare function traverseLayout(panel: Panel, rect: Rect, layout: Layout): void;
92 | export declare function getLayout(state: State, rect: Rect): Layout;
93 | export declare function getContentRect(state: State, rect: Rect, contentId: ContentId): Rect | undefined;
94 |
--------------------------------------------------------------------------------
/dist/state.js:
--------------------------------------------------------------------------------
1 | import { Rect } from "./rect.js";
2 | export var SplitMode;
3 | (function (SplitMode) {
4 | SplitMode[SplitMode["LeftRight"] = 0] = "LeftRight";
5 | SplitMode[SplitMode["TopBottom"] = 1] = "TopBottom";
6 | })(SplitMode || (SplitMode = {}));
7 | export var DockMode;
8 | (function (DockMode) {
9 | DockMode[DockMode["Full"] = 0] = "Full";
10 | DockMode[DockMode["Left"] = 1] = "Left";
11 | DockMode[DockMode["Right"] = 2] = "Right";
12 | DockMode[DockMode["Top"] = 3] = "Top";
13 | DockMode[DockMode["Bottom"] = 4] = "Bottom";
14 | })(DockMode || (DockMode = {}));
15 | export function makeState() {
16 | return {
17 | idNext: 2,
18 | rootPanel: {
19 | id: 1,
20 | floating: false,
21 | rect: new Rect(0, 0, 0, 0),
22 | contentList: [],
23 | currentTabIndex: 0,
24 | splitPanels: [],
25 | splitMode: SplitMode.LeftRight,
26 | splitSize: 0.5,
27 | preferredWidth: 300,
28 | preferredHeight: 250,
29 | ephemeral: false,
30 | },
31 | floatingPanels: [],
32 | activePanel: null,
33 | draggedPanel: null,
34 | showAnchors: false,
35 | previewAnchor: null,
36 | };
37 | }
38 | export function makePanel(state) {
39 | const id = state.idNext++;
40 | const panel = {
41 | id,
42 | floating: true,
43 | rect: new Rect(0, 0, 0, 0),
44 | contentList: [],
45 | currentTabIndex: 0,
46 | splitPanels: [],
47 | splitMode: SplitMode.LeftRight,
48 | splitSize: 0.5,
49 | preferredWidth: 300,
50 | preferredHeight: 250,
51 | ephemeral: false,
52 | };
53 | state.floatingPanels.push(panel);
54 | return panel;
55 | }
56 | export function createDockedPanel(state, dockIntoPanel, mode, content) {
57 | const panel = makePanel(state);
58 | addNewContent(state, panel, content);
59 | dock(state, panel, dockIntoPanel, mode);
60 | return panel;
61 | }
62 | export function detachPanel(state, panel) {
63 | if (!panel.floating) {
64 | panel.floating = true;
65 | state.activePanel = panel;
66 | state.floatingPanels.push(panel);
67 | }
68 | }
69 | export function addNewContent(state, toPanel, element) {
70 | const id = state.idNext++;
71 | toPanel.contentList.push({
72 | contentId: id,
73 | title: "",
74 | element,
75 | });
76 | toPanel.currentTabIndex = toPanel.contentList.length - 1;
77 | toPanel.ephemeral = false;
78 | }
79 | export function addContent(state, toPanel, content) {
80 | const id = state.idNext++;
81 | toPanel.contentList.push(content);
82 | toPanel.currentTabIndex = toPanel.contentList.length - 1;
83 | toPanel.ephemeral = false;
84 | }
85 | export function removeContent(state, fromPanel, contentId) {
86 | const index = fromPanel.contentList.findIndex(w => w.contentId === contentId);
87 | if (index < 0)
88 | return;
89 | fromPanel.contentList.splice(index, 1);
90 | fromPanel.currentTabIndex =
91 | Math.max(0, Math.min(fromPanel.contentList.length - 1, fromPanel.currentTabIndex));
92 | }
93 | export function removeEphemerals(state) {
94 | for (var i = 0; i < state.floatingPanels.length; i++)
95 | removeEphemeralsRecursive(state, state.floatingPanels[i]);
96 | coallesceEmptyPanels(state);
97 | }
98 | export function removeEphemeralsRecursive(state, fromPanel) {
99 | for (var i = 0; i < fromPanel.splitPanels.length; i++)
100 | coallesceEmptyPanelsRecursive(state, fromPanel.splitPanels[i]);
101 | if (fromPanel.ephemeral) {
102 | fromPanel.contentList = [];
103 | fromPanel.currentTabIndex = 0;
104 | }
105 | }
106 | export function coallesceEmptyPanels(state) {
107 | coallesceEmptyPanelsRecursive(state, state.rootPanel);
108 | for (var i = 0; i < state.floatingPanels.length; i++)
109 | coallesceEmptyPanelsRecursive(state, state.floatingPanels[i]);
110 | state.floatingPanels = state.floatingPanels.filter(p => p.contentList.length != 0 || p.splitPanels.length != 0);
111 | }
112 | export function coallesceEmptyPanelsRecursive(state, fromPanel) {
113 | for (var i = 0; i < fromPanel.splitPanels.length; i++)
114 | coallesceEmptyPanelsRecursive(state, fromPanel.splitPanels[i]);
115 | fromPanel.splitPanels = fromPanel.splitPanels.filter(p => p.contentList.length != 0 || p.splitPanels.length != 0);
116 | if (fromPanel.splitPanels.length == 1)
117 | Object.assign(fromPanel, fromPanel.splitPanels[0]);
118 | }
119 | export function dock(state, panel, dockIntoPanel, mode) {
120 | if (mode == DockMode.Full ||
121 | (dockIntoPanel.contentList.length == 0 && dockIntoPanel.splitPanels.length == 0)) {
122 | if (dockIntoPanel.splitPanels.length > 0)
123 | throw "invalid full docking into subdivided panel";
124 | for (const window of panel.contentList)
125 | addContent(state, dockIntoPanel, window);
126 | detachPanel(state, panel);
127 | panel.contentList = [];
128 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel);
129 | state.activePanel = dockIntoPanel;
130 | }
131 | else if (mode == DockMode.Right ||
132 | mode == DockMode.Left ||
133 | mode == DockMode.Top ||
134 | mode == DockMode.Bottom) {
135 | const subdivMode = (mode == DockMode.Right || mode == DockMode.Left) ?
136 | SplitMode.LeftRight :
137 | SplitMode.TopBottom;
138 | const subdivOriginalFirst = (mode == DockMode.Bottom || mode == DockMode.Right);
139 | const newSubpanels = [panel];
140 | const newSubpanel = makePanel(state);
141 | newSubpanel.contentList = dockIntoPanel.contentList;
142 | newSubpanel.currentTabIndex = dockIntoPanel.currentTabIndex;
143 | newSubpanel.splitMode = dockIntoPanel.splitMode;
144 | newSubpanel.splitPanels = dockIntoPanel.splitPanels;
145 | newSubpanel.splitSize = dockIntoPanel.splitSize;
146 | dockIntoPanel.contentList = [];
147 | dockIntoPanel.splitPanels = newSubpanels;
148 | dockIntoPanel.splitMode = subdivMode;
149 | dockIntoPanel.splitSize = subdivOriginalFirst ? 0.75 : 0.25;
150 | if (subdivOriginalFirst)
151 | newSubpanels.unshift(newSubpanel);
152 | else
153 | newSubpanels.push(newSubpanel);
154 | panel.floating = false;
155 | dockIntoPanel.floating = false;
156 | newSubpanel.floating = false;
157 | state.activePanel = panel;
158 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel && p !== newSubpanel);
159 | }
160 | else {
161 | throw "invalid docking";
162 | }
163 | }
164 | export function setPanelActiveAndBringToFront(state, panel) {
165 | if (panel.contentList.length != 0)
166 | state.activePanel = panel;
167 | if (!panel.floating)
168 | return;
169 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel);
170 | state.floatingPanels.push(panel);
171 | if (!panel.ephemeral)
172 | removeEphemerals(state);
173 | }
174 | export function clampFloatingPanels(state, rect) {
175 | const margin = 10;
176 | for (const panel of state.floatingPanels) {
177 | panel.rect.x =
178 | Math.max(rect.x + margin - panel.rect.w / 2, Math.min(rect.x2 - margin - panel.rect.w / 2, panel.rect.x));
179 | panel.rect.y =
180 | Math.max(rect.y + margin, Math.min(rect.y2 - margin - panel.rect.h / 2, panel.rect.y));
181 | }
182 | }
183 | export function clampFloatingPanelStrictly(state, panel, rect) {
184 | const margin = 10;
185 | panel.rect.x =
186 | Math.max(rect.x + margin, Math.min(rect.x2 - margin - panel.rect.w, panel.rect.x));
187 | panel.rect.y =
188 | Math.max(rect.y + margin, Math.min(rect.y2 - margin - panel.rect.h, panel.rect.y));
189 | }
190 | export function traverseLayout(panel, rect, layout) {
191 | const xMid = (rect.x1 + rect.x2) / 2;
192 | const yMid = (rect.y1 + rect.y2) / 2;
193 | if (panel.splitPanels.length == 2) {
194 | if (panel.splitMode == SplitMode.LeftRight) {
195 | const xSplit = rect.x1 + Math.round((rect.x2 - rect.x1) * panel.splitSize);
196 | const rect1 = rect.withX2(xSplit);
197 | const rect2 = rect.withX1(xSplit);
198 | const rectDivider = rect.withX1(xSplit).withX2(xSplit);
199 | traverseLayout(panel.splitPanels[0], rect1, layout);
200 | traverseLayout(panel.splitPanels[1], rect2, layout);
201 | layout.dividers.push({
202 | panel,
203 | vertical: false,
204 | rect: rectDivider,
205 | resizeMin: rect.x1,
206 | resizeMax: rect.x2,
207 | });
208 | }
209 | else if (panel.splitMode == SplitMode.TopBottom) {
210 | const ySplit = rect.y1 + Math.round((rect.y2 - rect.y1) * panel.splitSize);
211 | const rect1 = rect.withY2(ySplit);
212 | const rect2 = rect.withY1(ySplit);
213 | const rectDivider = rect.withY1(ySplit).withY2(ySplit);
214 | traverseLayout(panel.splitPanels[0], rect1, layout);
215 | traverseLayout(panel.splitPanels[1], rect2, layout);
216 | layout.dividers.push({
217 | panel,
218 | vertical: true,
219 | rect: rectDivider,
220 | resizeMin: rect.y1,
221 | resizeMax: rect.y2,
222 | });
223 | }
224 | }
225 | else {
226 | const panelRect = {
227 | panel,
228 | rect,
229 | floating: false,
230 | zIndex: 0,
231 | };
232 | for (let w = 0; w < panel.contentList.length; w++) {
233 | layout.content.push({
234 | content: panel.contentList[w],
235 | tabIndex: w,
236 | panel,
237 | layoutPanel: panelRect,
238 | });
239 | }
240 | layout.panelRects.push(panelRect);
241 | layout.anchors.push({
242 | panel,
243 | x: xMid,
244 | y: yMid,
245 | mode: DockMode.Full,
246 | previewRect: rect,
247 | });
248 | }
249 | layout.anchors.push({
250 | panel,
251 | x: rect.x2 - 10,
252 | y: yMid,
253 | mode: DockMode.Right,
254 | previewRect: rect.withX1(rect.x1 + (rect.x2 - rect.x1) * 3 / 4),
255 | });
256 | layout.anchors.push({
257 | panel,
258 | x: rect.x1 + 10,
259 | y: yMid,
260 | mode: DockMode.Left,
261 | previewRect: rect.withX2(rect.x1 + (rect.x2 - rect.x1) / 4),
262 | });
263 | layout.anchors.push({
264 | panel,
265 | x: xMid,
266 | y: rect.y2 - 10,
267 | mode: DockMode.Bottom,
268 | previewRect: rect.withY1(rect.y1 + (rect.y2 - rect.y1) * 3 / 4),
269 | });
270 | layout.anchors.push({
271 | panel,
272 | x: xMid,
273 | y: rect.y1 + 10,
274 | mode: DockMode.Top,
275 | previewRect: rect.withY2(rect.y1 + (rect.y2 - rect.y1) / 4),
276 | });
277 | }
278 | export function getLayout(state, rect) {
279 | const layout = {
280 | panelRects: [],
281 | content: [],
282 | dividers: [],
283 | anchors: [],
284 | };
285 | traverseLayout(state.rootPanel, rect, layout);
286 | for (let fp = 0; fp < state.floatingPanels.length; fp++) {
287 | const floatingPanel = state.floatingPanels[fp];
288 | const panelRect = {
289 | panel: floatingPanel,
290 | rect: floatingPanel.rect,
291 | floating: true,
292 | zIndex: fp + 1,
293 | };
294 | for (let w = 0; w < floatingPanel.contentList.length; w++) {
295 | layout.content.push({
296 | content: floatingPanel.contentList[w],
297 | tabIndex: w,
298 | panel: floatingPanel,
299 | layoutPanel: panelRect,
300 | });
301 | }
302 | layout.panelRects.push(panelRect);
303 | layout.anchors.push({
304 | panel: floatingPanel,
305 | x: floatingPanel.rect.xCenter,
306 | y: floatingPanel.rect.yCenter,
307 | mode: DockMode.Full,
308 | previewRect: floatingPanel.rect,
309 | });
310 | }
311 | return layout;
312 | }
313 | export function getContentRect(state, rect, contentId) {
314 | const layout = getLayout(state, rect);
315 | return layout.panelRects.find(p => p.panel.contentList.some(c => c.contentId === contentId)).rect;
316 | }
317 | //# sourceMappingURL=state.js.map
--------------------------------------------------------------------------------
/dist/state.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hlorenzi/react-dockable/0d0ac9503ca7e8dbb1ac0f667267d73f1fce6d84/example.gif
--------------------------------------------------------------------------------
/example/Clock.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "@hlorenzi/react-dockable"
3 |
4 |
5 | export function Clock()
6 | {
7 | const [value, setValue] = React.useState(Date.now())
8 |
9 | React.useEffect(() =>
10 | {
11 | const interval = setInterval(() => setValue(Date.now()), 1000)
12 | return () => clearInterval(interval)
13 | })
14 |
15 |
16 | const ctx = Dockable.useContentContext()
17 | ctx.setTitle(`Clock`)
18 | ctx.setPreferredSize(450, 250)
19 |
20 |
21 | return
33 | { new Date(value).toLocaleString() }
34 |
35 | }
--------------------------------------------------------------------------------
/example/Counter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "@hlorenzi/react-dockable"
3 |
4 |
5 | export function Counter()
6 | {
7 | const [value, setValue] = React.useState(0)
8 | const countUp = () => setValue(value + 1)
9 |
10 |
11 | const ctx = Dockable.useContentContext()
12 | ctx.setTitle(`Count: ${ value }`)
13 | ctx.setPreferredSize(300, 250)
14 |
15 |
16 | return
28 | { value }
29 |
30 |
36 |
37 | }
--------------------------------------------------------------------------------
/example/TextEditor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "@hlorenzi/react-dockable"
3 |
4 |
5 | export function TextEditor(props: {
6 | initialValue: string,
7 | })
8 | {
9 | const [value, setValue] = React.useState(props.initialValue)
10 | const [edited, setEdited] = React.useState(false)
11 |
12 | const onEdit = (newValue: string) =>
13 | {
14 | setEdited(true)
15 | setValue(newValue)
16 | }
17 |
18 |
19 | const ctx = Dockable.useContentContext()
20 | ctx.setTitle(`Text Editor${ edited ? " [edited]" : "" }`)
21 | ctx.setPreferredSize(300, 250)
22 |
23 |
24 | return
38 |
43 |
44 |
55 | }
--------------------------------------------------------------------------------
/example/main.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ReactDOM from "react-dom"
3 | import * as Dockable from "@hlorenzi/react-dockable"
4 | import { Counter } from "./Counter"
5 | import { Clock } from "./Clock"
6 | import { TextEditor } from "./TextEditor"
7 |
8 |
9 | const fillerText =
10 | [
11 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
12 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
13 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
14 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
15 | ]
16 |
17 |
18 | function App()
19 | {
20 | const state = Dockable.useDockable((state) =>
21 | {
22 | // Set up initial panels and content
23 |
24 | // Dock three Counters in the same panel
25 | // (which will show up as tabs)...
26 | Dockable.createDockedPanel(
27 | state, state.rootPanel, Dockable.DockMode.Full,
28 | )
29 |
30 | Dockable.createDockedPanel(
31 | state, state.rootPanel, Dockable.DockMode.Full,
32 | )
33 |
34 | Dockable.createDockedPanel(
35 | state, state.rootPanel, Dockable.DockMode.Full,
36 | )
37 |
38 | // ...then dock one TextEditor to the right...
39 | Dockable.createDockedPanel(
40 | state, state.rootPanel, Dockable.DockMode.Right,
41 | )
42 |
43 | // ...and finally dock one Clock at the bottom
44 | Dockable.createDockedPanel(
45 | state, state.rootPanel, Dockable.DockMode.Bottom,
46 | )
47 | })
48 |
49 |
50 | const spawnCounter = () =>
51 | {
52 | Dockable.spawnFloating(
53 | state,
54 | )
55 | }
56 |
57 | const spawnClock = () =>
58 | {
59 | Dockable.spawnFloating(
60 | state,
61 | )
62 | }
63 |
64 | const spawnTextEditor = () =>
65 | {
66 | Dockable.spawnFloating(
67 | state,
68 | )
71 | }
72 |
73 |
74 | return
80 |
81 |
87 |
90 |
91 |
94 |
95 |
98 |
99 |
100 |
101 |
102 |
103 | }
104 |
105 |
106 | document.body.onload = function()
107 | {
108 | ReactDOM.render(, document.getElementById("divApp"))
109 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | @hlorenzi/react-dockable
9 |
10 |
11 |
12 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hlorenzi/react-dockable",
3 | "version": "0.1.0",
4 | "description": "An easy-to-use dockable window manager for React, fully embracing hooks!",
5 | "repository": "github:hlorenzi/react-dockable",
6 | "homepage": "https://github.com/hlorenzi/react-dockable#readme",
7 | "bugs": "https://github.com/hlorenzi/react-dockable/issues",
8 | "keywords": [
9 | "react",
10 | "hook",
11 | "layout",
12 | "dock",
13 | "window",
14 | "popout"
15 | ],
16 | "author": "hlorenzi",
17 | "type": "module",
18 | "license": "APACHE-2.0",
19 | "main": "./dist/index.js",
20 | "types": "./dist/index.d.ts",
21 | "files": [
22 | "/dist"
23 | ],
24 | "scripts": {
25 | "dev": "tsc -w --project ./",
26 | "dev2": "webpack --config webpack.config.cjs --watch --mode development",
27 | "build2": "webpack --config webpack.config.cjs",
28 | "start": "http-server -p 80"
29 | },
30 | "browserslist": [
31 | "> 2%",
32 | "not dead",
33 | "not ie 11"
34 | ],
35 | "devDependencies": {
36 | "@babel/core": "^7.16.0",
37 | "@babel/preset-env": "^7.16.0",
38 | "@babel/preset-react": "^7.16.0",
39 | "@babel/preset-typescript": "^7.16.0",
40 | "@types/node": "^16.11.6",
41 | "@types/react": "^17.0.34",
42 | "@types/react-dom": "^17.0.11",
43 | "@types/styled-components": "^5.1.15",
44 | "babel-loader": "^8.2.3",
45 | "react": "^17.0.2",
46 | "react-dom": "^17.0.2",
47 | "styled-components": "^5.3.3",
48 | "typescript": "^4.4.4",
49 | "webpack": "^5.62.1",
50 | "webpack-cli": "^4.9.1"
51 | },
52 | "peerDependencies": {
53 | "react": "^17.0.2",
54 | "styled-components": "^5.3.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "./index.js"
3 | import styled from "styled-components"
4 |
5 |
6 | const StyledContainer = styled.div<{
7 | }>`
8 | --dockable-voidBkg: #252525;
9 | --dockable-panelBkg: #1e1e1e;
10 | --dockable-panelInactiveBorder: #393939;
11 | --dockable-panelActiveBorder: #777777;
12 | --dockable-panelTabBkg: #2d2d2d;
13 | --dockable-panelTabTextColor: #ffffff;
14 | --dockable-overlayColor: #00aaff44;
15 | --dockable-anchorColor: #00aaff;
16 | --dockable-buttonHoverBkg: #323232;
17 | --dockable-scrollbarColor: #777777;
18 |
19 | width: 100%;
20 | height: 100%;
21 | background-color: var(--dockable-voidBkg);
22 | `
23 |
24 |
25 | const StyledContentRoot = styled.div<{
26 | isCurrentTab: boolean
27 | }>`
28 | display: ${ props => props.isCurrentTab ? "grid" : "none" };
29 | grid-template: 100% / 100%;
30 |
31 | position: absolute;
32 | box-sizing: border-box;
33 | contain: strict;
34 |
35 | color: #fff;
36 | text-align: left;
37 |
38 | background-color: transparent;
39 | overflow: hidden;
40 | `
41 |
42 |
43 | const StyledContentInner = styled.div`
44 | grid-row: 1;
45 | grid-column: 1;
46 | width: 100%;
47 | height: 100%;
48 | `
49 |
50 |
51 | const StyledBottomRightResizeHandle = styled.div<{
52 | size: number,
53 | }>`
54 | width: ${ props => props.size }px;
55 | height: ${ props => props.size }px;
56 |
57 | grid-row: 1;
58 | grid-column: 1;
59 | align-self: end;
60 | justify-self: end;
61 |
62 | cursor: nwse-resize;
63 | z-index: 1;
64 |
65 | &:hover
66 | {
67 | background-color: var(--dockable-overlayColor);
68 | }
69 | `
70 |
71 |
72 | const StyledDivider = styled.div`
73 | &:hover
74 | {
75 | background-color: var(--dockable-overlayColor);
76 | }
77 | `
78 |
79 |
80 | export function Container(props: {
81 | state: Dockable.RefState,
82 |
83 | anchorSize?: number,
84 | resizeHandleSize?: number,
85 | dividerSize?: number,
86 | tabHeight?: number,
87 | })
88 | {
89 | const [rect, setRect] = React.useState(new Dockable.Rect(0, 0, 0, 0))
90 | const rootRef = React.useRef(null)
91 |
92 |
93 | const anchorSize = props.anchorSize ?? 5
94 | const resizeHandleSize = props.anchorSize ?? 10
95 | const dividerSize = props.anchorSize ?? 6
96 | const tabHeight = props.anchorSize ?? 25
97 |
98 |
99 | React.useLayoutEffect(() =>
100 | {
101 | const onResize = () =>
102 | {
103 | if (!rootRef.current)
104 | return
105 |
106 | const elemRect = rootRef.current!.getBoundingClientRect()
107 |
108 | setRect(new Dockable.Rect(
109 | elemRect.x,
110 | elemRect.y,
111 | elemRect.width,
112 | elemRect.height))
113 | }
114 |
115 | onResize()
116 |
117 | window.addEventListener("resize", onResize)
118 | return () => window.removeEventListener("resize", onResize)
119 |
120 | }, [])
121 |
122 |
123 | const rectRef = React.useRef(null!)
124 | rectRef.current = rect
125 |
126 |
127 | const layoutRef = React.useRef(null!)
128 | layoutRef.current = React.useMemo(() =>
129 | {
130 | return Dockable.getLayout(props.state.ref.current, new Dockable.Rect(rect.x, rect.y, rect.w - 1, rect.h - 1))
131 |
132 | }, [rect, props.state.updateToken])
133 |
134 |
135 | const setTitle = (layoutContent: Dockable.LayoutContent, title: string) =>
136 | {
137 | if (layoutContent.content.title != title)
138 | {
139 | window.requestAnimationFrame(() =>
140 | {
141 | layoutContent.content.title = title
142 | props.state.commit()
143 | })
144 | }
145 | }
146 |
147 | const setPreferredSize = (layoutContent: Dockable.LayoutContent, width: number, height: number) =>
148 | {
149 | if (layoutContent.tabIndex == layoutContent.panel.currentTabIndex &&
150 | (width != layoutContent.panel.preferredWidth ||
151 | height != layoutContent.panel.preferredHeight))
152 | {
153 | window.requestAnimationFrame(() =>
154 | {
155 | layoutContent.panel.preferredWidth = width
156 | layoutContent.panel.preferredHeight = height
157 |
158 | layoutContent.panel.rect = new Dockable.Rect(
159 | layoutContent.panel.rect.x, layoutContent.panel.rect.y,
160 | width, height)
161 |
162 | props.state.commit()
163 | })
164 | }
165 | }
166 |
167 | return
170 |
171 | { layoutRef.current.panelRects.map(panelRect =>
172 | handleClickedPanel(props.state, panelRect.panel, null) }
178 | onClickTab={ (tabNumber) => handleClickedPanel(props.state, panelRect.panel, tabNumber) }
179 | onCloseTab={ (ev, tabNumber) => handleClosedTab(ev, props.state, panelRect.panel, tabNumber) }
180 | onDragHeader={ (ev, tabNumber) => handleDraggedHeader(ev, props.state, layoutRef, rectRef, panelRect.panel, tabNumber) }
181 | />
182 | )}
183 |
184 | { layoutRef.current.content.map(layoutContent =>
185 | handleClickedPanel(props.state, layoutContent.panel, null) }
189 | style={{
190 | left: (layoutContent.layoutPanel.rect.x) + "px",
191 | top: (layoutContent.layoutPanel.rect.y + tabHeight) + "px",
192 | width: (layoutContent.layoutPanel.rect.w) + "px",
193 | height: (layoutContent.layoutPanel.rect.h - tabHeight) + "px",
194 | zIndex: layoutContent.layoutPanel.zIndex * 3 + 1,
195 | }}>
196 | setTitle(layoutContent, title),
200 | setPreferredSize: (w, h) => setPreferredSize(layoutContent, w, h),
201 | }}>
202 |
203 | { layoutContent.content.element }
204 |
205 |
206 |
207 | { layoutContent.panel.floating &&
208 | {
211 | handleClickedPanel(props.state, layoutContent.panel, null)
212 | handleDraggedEdge(ev, props.state, layoutRef, layoutContent.panel)
213 | }}
214 | />
215 | }
216 |
217 | )}
218 |
219 | { layoutRef.current.dividers.map((divider, i) =>
220 | handleDraggedDivider(ev, props.state, divider) }
223 | style={{
224 | width: (divider.rect.w || dividerSize) + "px",
225 | height: (divider.rect.h || dividerSize) + "px",
226 |
227 | position: "absolute",
228 | left: (divider.rect.x - (!divider.vertical ? dividerSize / 2 : 0)) + "px",
229 | top: (divider.rect.y - (divider.vertical ? dividerSize / 2 : 0)) + "px",
230 |
231 | cursor: !divider.vertical ?
232 | "ew-resize" :
233 | "ns-resize",
234 |
235 | zIndex: 1,
236 | userSelect: "none",
237 | }}/>
238 | )}
239 |
240 | { props.state.ref.current.previewAnchor &&
241 |
251 | }
252 |
253 | { props.state.ref.current.showAnchors &&
254 | layoutRef.current.anchors.map((anchor, i) =>
255 | props.state.ref.current.draggedPanel !== anchor.panel &&
256 | )
271 | }
272 |
273 |
274 | }
275 |
276 |
277 | function handleDraggedDivider(
278 | ev: React.MouseEvent,
279 | state: Dockable.RefState,
280 | divider: Dockable.Divider)
281 | {
282 | ev.preventDefault()
283 |
284 | const onMouseMove = (ev: MouseEvent) =>
285 | {
286 | const mouseX = ev.pageX
287 | const mouseY = ev.pageY
288 |
289 | divider.panel.splitSize =
290 | Math.max(0.05,
291 | Math.min(0.95,
292 | ((divider.vertical ? mouseY : mouseX) - divider.resizeMin) /
293 | (divider.resizeMax - divider.resizeMin)))
294 |
295 | state.commit()
296 | }
297 |
298 | const onMouseUp = () =>
299 | {
300 | window.removeEventListener("mousemove", onMouseMove)
301 | window.removeEventListener("mouseup", onMouseUp)
302 | }
303 |
304 | window.addEventListener("mousemove", onMouseMove)
305 | window.addEventListener("mouseup", onMouseUp)
306 | }
307 |
308 |
309 | function handleDraggedEdge(
310 | ev: React.MouseEvent,
311 | state: Dockable.RefState,
312 | layout: React.MutableRefObject,
313 | panel: Dockable.Panel)
314 | {
315 | ev.preventDefault()
316 | ev.stopPropagation()
317 |
318 | const startMouseX = ev.pageX
319 | const startMouseY = ev.pageY
320 |
321 | const layoutPanel = layout.current.panelRects.find(p => p.panel === panel)!
322 | const startPanelRect = layoutPanel.rect
323 |
324 | const onMouseMove = (ev: MouseEvent) =>
325 | {
326 | const mouseX = ev.pageX
327 | const mouseY = ev.pageY
328 |
329 | panel.rect = new Dockable.Rect(
330 | startPanelRect.x,
331 | startPanelRect.y,
332 | Math.max(150, startPanelRect.w + mouseX - startMouseX),
333 | Math.max(50, startPanelRect.h + mouseY - startMouseY))
334 |
335 | state.commit()
336 | }
337 |
338 | const onMouseUp = () =>
339 | {
340 | window.removeEventListener("mousemove", onMouseMove)
341 | window.removeEventListener("mouseup", onMouseUp)
342 | }
343 |
344 | window.addEventListener("mousemove", onMouseMove)
345 | window.addEventListener("mouseup", onMouseUp)
346 | }
347 |
348 |
349 | function handleDraggedHeader(
350 | ev: React.MouseEvent,
351 | state: Dockable.RefState,
352 | layout: React.MutableRefObject,
353 | containerRect: React.MutableRefObject,
354 | draggedPanel: Dockable.Panel,
355 | draggedTabIndex: number | null)
356 | {
357 | ev.preventDefault()
358 | ev.stopPropagation()
359 |
360 | const startMouseX = ev.pageX
361 | const startMouseY = ev.pageY
362 |
363 | const layoutPanel = layout.current.panelRects.find(p => p.panel === draggedPanel)!
364 | let startPanelRect = layoutPanel.rect
365 |
366 | let dragLocked = true
367 |
368 |
369 | const onMouseMove = (ev: MouseEvent) =>
370 | {
371 | const mouseX = ev.pageX
372 | const mouseY = ev.pageY
373 |
374 | // Start dragging only when mouse moves far enough, and
375 | // undock panel at this moment if originally docked
376 | if (Math.abs(mouseX - startMouseX) > 10 ||
377 | Math.abs(mouseY - startMouseY) > 10)
378 | {
379 | dragLocked = false
380 |
381 | const floatingRect = new Dockable.Rect(
382 | mouseX - Math.min(draggedPanel.preferredWidth / 2, mouseX - startPanelRect.x),
383 | mouseY - (mouseY - startPanelRect.y),
384 | draggedPanel.preferredWidth,
385 | draggedPanel.preferredHeight)
386 |
387 | if (draggedTabIndex !== null && draggedPanel.contentList.length > 1)
388 | {
389 | // Remove single tab content from original panel and
390 | // transfer it to a new floating panel
391 | const content = draggedPanel.contentList[draggedTabIndex]
392 | Dockable.removeContent(state.ref.current, draggedPanel, content.contentId)
393 |
394 | draggedPanel = Dockable.makePanel(state.ref.current)
395 | Dockable.addContent(state.ref.current, draggedPanel, content)
396 |
397 | Dockable.coallesceEmptyPanels(state.ref.current)
398 |
399 | draggedPanel.rect = startPanelRect = floatingRect
400 | }
401 |
402 | else if (!draggedPanel.floating)
403 | {
404 | // Remove original docked panel and
405 | // transfer all content to a new floating panel
406 | const contents = [...draggedPanel.contentList]
407 | const originalTabIndex = draggedPanel.currentTabIndex
408 | for (const content of contents)
409 | Dockable.removeContent(state.ref.current, draggedPanel, content.contentId)
410 |
411 | draggedPanel = Dockable.makePanel(state.ref.current)
412 | for (const content of contents)
413 | Dockable.addContent(state.ref.current, draggedPanel, content)
414 |
415 | draggedPanel.currentTabIndex = originalTabIndex
416 | Dockable.coallesceEmptyPanels(state.ref.current)
417 |
418 | draggedPanel.rect = startPanelRect = floatingRect
419 | }
420 |
421 | state.ref.current.draggedPanel = draggedPanel
422 | state.ref.current.showAnchors = true
423 | state.commit()
424 | }
425 |
426 | // Handle actual dragging
427 | if (!dragLocked)
428 | {
429 | // Move panel rect
430 | draggedPanel.rect = startPanelRect.displace(mouseX - startMouseX, mouseY - startMouseY)
431 |
432 | // Find nearest anchor
433 | let nearestDistSqr = 50 * 50
434 | state.ref.current.previewAnchor = null
435 |
436 | for (const anchor of layout.current.anchors)
437 | {
438 | if (anchor.panel === draggedPanel)
439 | continue
440 |
441 | const xx = anchor.x - mouseX
442 | const yy = anchor.y - mouseY
443 | const distSqr = xx * xx + yy * yy
444 | if (distSqr < nearestDistSqr)
445 | {
446 | nearestDistSqr = distSqr
447 | state.ref.current.previewAnchor = anchor
448 | }
449 | }
450 |
451 | state.commit()
452 | }
453 | }
454 |
455 | const onMouseUp = () =>
456 | {
457 | window.removeEventListener("mousemove", onMouseMove)
458 | window.removeEventListener("mouseup", onMouseUp)
459 |
460 | // Dock dragged panel if near an anchor
461 | if (state.ref.current.previewAnchor)
462 | {
463 | Dockable.dock(
464 | state.ref.current,
465 | draggedPanel,
466 | state.ref.current.previewAnchor.panel,
467 | state.ref.current.previewAnchor.mode)
468 | }
469 |
470 | Dockable.clampFloatingPanels(state.ref.current, containerRect.current)
471 | state.ref.current.draggedPanel = null
472 | state.ref.current.showAnchors = false
473 | state.ref.current.previewAnchor = null
474 | state.commit()
475 | }
476 |
477 | window.addEventListener("mousemove", onMouseMove)
478 | window.addEventListener("mouseup", onMouseUp)
479 | }
480 |
481 |
482 | function handleClickedPanel(
483 | state: Dockable.RefState,
484 | clickedPanel: Dockable.Panel,
485 | tabNumber: number | null)
486 | {
487 | if (tabNumber !== null)
488 | {
489 | clickedPanel.currentTabIndex = tabNumber
490 | }
491 |
492 | Dockable.setPanelActiveAndBringToFront(state.ref.current, clickedPanel)
493 | state.commit()
494 | }
495 |
496 |
497 | function handleClosedTab(
498 | ev: React.MouseEvent,
499 | state: Dockable.RefState,
500 | panel: Dockable.Panel,
501 | tabNumber: number)
502 | {
503 | ev.preventDefault()
504 |
505 | const content = panel.contentList[tabNumber]
506 | Dockable.removeContent(state.ref.current, panel, content.contentId)
507 | Dockable.coallesceEmptyPanels(state.ref.current)
508 | state.commit()
509 | }
--------------------------------------------------------------------------------
/src/Panel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "./index.js"
3 | import styled from "styled-components"
4 |
5 |
6 | const StyledPanelRoot = styled.div`
7 | position: absolute;
8 | box-sizing: border-box;
9 | contain: strict;
10 | `
11 |
12 |
13 | const StyledTabRow = styled.div`
14 | background-color: var(--dockable-panelBkg);
15 | box-sizing: border-box;
16 | width: 100%;
17 | height: 100%;
18 |
19 | display: grid;
20 | grid-template: auto 1fr / 1fr;
21 | overflow: hidden;
22 |
23 | border: 1px solid var(--dockable-panelInactiveBorder);
24 |
25 | &.active
26 | {
27 | border: 1px solid var(--dockable-panelActiveBorder);
28 | }
29 | `
30 |
31 |
32 | const StyledTabRowInner = styled.div<{
33 | tabHeight: number,
34 | tabCount: number,
35 | }>`
36 | background-color: var(--dockable-voidBkg);
37 | text-align: left;
38 | grid-row: 1;
39 | grid-column: 1;
40 |
41 | display: grid;
42 | grid-template: ${ props => props.tabHeight }px / repeat(${ props => props.tabCount }, auto) 1fr;
43 | grid-auto-flow: column;
44 |
45 | height: ${ props => props.tabHeight }px;
46 |
47 | overflow-x: auto;
48 | overflow-y: hidden;
49 | user-select: none;
50 |
51 | &::-webkit-scrollbar
52 | {
53 | width: 4px;
54 | height: 4px;
55 | }
56 |
57 | &::-webkit-scrollbar-track
58 | {
59 | background: var(--dockable-panelBkg);
60 | }
61 |
62 | &::-webkit-scrollbar-thumb
63 | {
64 | background-color: var(--dockable-scrollbarColor);
65 | border-radius: 0;
66 | border: 0;
67 | }
68 | `
69 |
70 |
71 | const StyledTab = styled.div<{
72 | tabNumber: number,
73 | isCurrentTab: boolean,
74 | }>`
75 | grid-row: 1;
76 | grid-column: ${ props => props.tabNumber + 1 };
77 |
78 | display: grid;
79 | grid-template: auto / auto auto;
80 | justify-items: start;
81 | align-items: center;
82 |
83 | min-width: max-content;
84 | height: 100%;
85 | box-sizing: border-box;
86 | margin-right: 1px;
87 | padding-left: 0.75em;
88 | padding-right: 0.5em;
89 | user-select: none;
90 |
91 | color: var(--dockable-panelTabTextColor);
92 | background-color: ${ props => props.isCurrentTab ?
93 | "var(--dockable-panelBkg)" :
94 | "var(--dockable-panelTabBkg)" };
95 | `
96 |
97 |
98 | const StyledCloseButton = styled.button<{
99 | isCurrentTab: boolean,
100 | }>`
101 | pointer-events: auto;
102 | border: 0;
103 | border-radius: 0.25em;
104 | background-color: transparent;
105 | padding: 0.1em 0.3em;
106 | cursor: pointer;
107 | margin-left: 0.25em;
108 | width: 1.5em;
109 | height: 1.5em;
110 |
111 | color: ${ props => props.isCurrentTab ? "var(--dockable-panelTabTextColor)" : "transparent" };
112 |
113 | &:hover
114 | {
115 | background-color: var(--dockable-buttonHoverBkg);
116 | color: var(--dockable-panelTabTextColor);
117 | }
118 |
119 | &:active
120 | {
121 | background-color: var(--dockable-buttonHoverBkg);
122 | color: var(--dockable-panelTabTextColor);
123 | }
124 | `
125 |
126 |
127 | const StyledTabRowEmptySpace = styled.div`
128 | min-width: 2em;
129 | pointer-events: none;
130 | `
131 |
132 |
133 | export function ContainerPanel(props: {
134 | state: Dockable.RefState,
135 | panelRect: Dockable.LayoutPanel,
136 | tabHeight: number,
137 | onClickPanel: () => void,
138 | onClickTab: (tabNumber: number) => void,
139 | onCloseTab: (ev: React.MouseEvent, tabNumber: number) => void,
140 | onDragHeader: (ev: React.MouseEvent, tabNumber: number | null) => void,
141 | })
142 | {
143 | const panelRect: Dockable.LayoutPanel = props.panelRect
144 |
145 | const isActivePanel = props.state.ref.current.activePanel === panelRect.panel
146 |
147 | return
155 |
158 | {
163 | props.onClickPanel()
164 | props.onDragHeader(ev, null)
165 | }}
166 | >
167 |
168 | { panelRect.panel.contentList.map((content, tabNumber) =>
169 | {
174 | props.onClickTab(tabNumber)
175 | props.onDragHeader(ev, tabNumber)
176 | }}
177 | >
178 | { content.title || `Content ${ content.contentId }` }
179 | {
182 | props.onClickTab(tabNumber)
183 | props.onCloseTab(ev, tabNumber)
184 | }}
185 | >
186 | ×
187 |
188 |
189 | )}
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | }
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as Dockable from "./index.js"
3 |
4 |
5 | export function useDockable(
6 | init?: (state: Dockable.State) => void)
7 | : Dockable.RefState
8 | {
9 | return Dockable.useRefState(() =>
10 | {
11 | const state = Dockable.makeState()
12 |
13 | if (init)
14 | init(state)
15 |
16 | return state
17 | })
18 | }
19 |
20 |
21 | interface MousePos
22 | {
23 | x: number
24 | y: number
25 | }
26 |
27 |
28 | export const mousePos: MousePos =
29 | {
30 | x: 0,
31 | y: 0,
32 | }
33 |
34 |
35 | window.addEventListener("mousemove", (ev: MouseEvent) =>
36 | {
37 | mousePos.x = ev.pageX
38 | mousePos.y = ev.pageY
39 | })
40 |
41 |
42 | export function spawnFloating(
43 | state: Dockable.RefState,
44 | elem: JSX.Element)
45 | : Dockable.Panel
46 | {
47 | const panel = Dockable.makePanel(state.ref.current)
48 | Dockable.addNewContent(state.ref.current, panel, elem)
49 | panel.rect = new Dockable.Rect(mousePos.x, mousePos.y, 500, 300)
50 |
51 | state.ref.current.activePanel = panel
52 | state.commit()
53 | return panel
54 | }
55 |
56 |
57 | export function spawnFloatingEphemeral(
58 | state: Dockable.RefState,
59 | elem: JSX.Element)
60 | : Dockable.Panel
61 | {
62 | const panel = spawnFloating(state, elem)
63 | Dockable.removeEphemerals(state.ref.current)
64 | panel.ephemeral = true
65 | state.commit()
66 | return panel
67 | }
68 |
69 |
70 | export interface ContentContextProps
71 | {
72 | layoutContent: Dockable.LayoutContent
73 |
74 | setTitle: (title: string) => void
75 | setPreferredSize: (w: number, h: number) => void
76 | }
77 |
78 |
79 | export const ContentContext = React.createContext(null!)
80 |
81 |
82 | export function useContentContext(): ContentContextProps
83 | {
84 | return React.useContext(ContentContext)
85 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./global.js"
2 | export * from "./state.js"
3 | export * from "./Container.jsx"
4 | export * from "./Panel.jsx"
5 | export * from "./rect.js"
6 | export * from "./refState.js"
--------------------------------------------------------------------------------
/src/rect.ts:
--------------------------------------------------------------------------------
1 | export class Rect
2 | {
3 | x: number
4 | y: number
5 | w: number
6 | h: number
7 |
8 |
9 | constructor(x: number, y: number, w: number, h: number)
10 | {
11 | this.x = x
12 | this.y = y
13 | this.w = w
14 | this.h = h
15 | }
16 |
17 |
18 | static fromVertices(x1: number, y1: number, x2: number, y2: number): Rect
19 | {
20 | return new Rect(
21 | Math.min(x1, x2),
22 | Math.min(y1, y2),
23 | Math.abs(x2 - x1),
24 | Math.abs(y2 - y1))
25 | }
26 |
27 |
28 | static fromElement(elem: HTMLElement)
29 | {
30 | const clientRect = elem.getBoundingClientRect()
31 | return new Rect(
32 | clientRect.left,
33 | clientRect.top,
34 | clientRect.width,
35 | clientRect.height)
36 | }
37 |
38 |
39 | clone(): Rect
40 | {
41 | return new Rect(this.x, this.y, this.w, this.h)
42 | }
43 |
44 |
45 | get x1(): number
46 | {
47 | return this.x
48 | }
49 |
50 |
51 | get y1(): number
52 | {
53 | return this.y
54 | }
55 |
56 |
57 | get x2(): number
58 | {
59 | return this.x + this.w
60 | }
61 |
62 |
63 | get y2(): number
64 | {
65 | return this.y + this.h
66 | }
67 |
68 |
69 | get xCenter(): number
70 | {
71 | return (this.x1 + this.x2) / 2
72 | }
73 |
74 |
75 | get yCenter(): number
76 | {
77 | return (this.y1 + this.y2) / 2
78 | }
79 |
80 |
81 | withX(value: number): Rect
82 | {
83 | return new Rect(value, this.y, this.w, this.h)
84 | }
85 |
86 |
87 | withY(value: number): Rect
88 | {
89 | return new Rect(this.x, value, this.w, this.h)
90 | }
91 |
92 |
93 | withW(value: number): Rect
94 | {
95 | return new Rect(this.x, this.y, value, this.h)
96 | }
97 |
98 |
99 | withH(value: number): Rect
100 | {
101 | return new Rect(this.x, this.y, this.w, value)
102 | }
103 |
104 |
105 | withX1(value: number): Rect
106 | {
107 | return Rect.fromVertices(value, this.y1, this.x2, this.y2)
108 | }
109 |
110 |
111 | withY1(value: number): Rect
112 | {
113 | return Rect.fromVertices(this.x1, value, this.x2, this.y2)
114 | }
115 |
116 |
117 | withX2(value: number): Rect
118 | {
119 | return Rect.fromVertices(this.x1, this.y1, value, this.y2)
120 | }
121 |
122 |
123 | withY2(value: number): Rect
124 | {
125 | return Rect.fromVertices(this.x1, this.y1, this.x2, value)
126 | }
127 |
128 |
129 | displace(x: number, y: number): Rect
130 | {
131 | return new Rect(this.x + x, this.y + y, this.w, this.h)
132 | }
133 |
134 |
135 | expand(amount: number): Rect
136 | {
137 | return Rect.fromVertices(
138 | this.x1 - amount,
139 | this.y1 - amount,
140 | this.x2 + amount,
141 | this.y2 + amount)
142 | }
143 |
144 |
145 | expandW(amount: number): Rect
146 | {
147 | return Rect.fromVertices(
148 | this.x1 - amount,
149 | this.y1,
150 | this.x2 + amount,
151 | this.y2)
152 | }
153 |
154 |
155 | contains(p: { x: number, y: number }): boolean
156 | {
157 | return p.x >= this.x &&
158 | p.x < this.x2 &&
159 | p.y >= this.y &&
160 | p.y < this.y2
161 | }
162 |
163 |
164 | overlaps(other: Rect): boolean
165 | {
166 | return this.x2 >= other.x &&
167 | this.x < other.x2 &&
168 | this.y2 >= other.y &&
169 | this.y < other.y2
170 | }
171 | }
--------------------------------------------------------------------------------
/src/refState.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 |
4 | export interface RefState
5 | {
6 | ref: React.MutableRefObject
7 | updateToken: number
8 | commit: () => void
9 | }
10 |
11 |
12 | export function useRefState(initializer: () => T): RefState
13 | {
14 | const [updateToken, setUpdateToken] = React.useState(0)
15 |
16 | const ref = React.useRef(null!)
17 | if (ref.current === null)
18 | ref.current = initializer()
19 |
20 | return {
21 | ref,
22 | updateToken,
23 | commit: () => setUpdateToken(n => (n + 1) % 1000000)
24 | }
25 | }
--------------------------------------------------------------------------------
/src/state.ts:
--------------------------------------------------------------------------------
1 | import { Rect } from "./rect.js"
2 |
3 |
4 | export type PanelId = number
5 | export type ContentId = number
6 | export type ContentElement = JSX.Element
7 |
8 |
9 | export interface State
10 | {
11 | idNext: number
12 | rootPanel: Panel
13 | floatingPanels: Panel[]
14 | activePanel: Panel | null
15 |
16 | draggedPanel: Panel | null
17 | showAnchors: boolean
18 | previewAnchor: Anchor | null
19 | }
20 |
21 |
22 | export enum SplitMode
23 | {
24 | LeftRight,
25 | TopBottom,
26 | }
27 |
28 |
29 | export enum DockMode
30 | {
31 | Full,
32 | Left,
33 | Right,
34 | Top,
35 | Bottom,
36 | }
37 |
38 |
39 | export interface Panel
40 | {
41 | id: PanelId
42 | floating: boolean
43 | rect: Rect
44 |
45 | contentList: Content[]
46 | currentTabIndex: number
47 |
48 | splitPanels: Panel[]
49 | splitMode: SplitMode
50 | splitSize: number
51 |
52 | preferredWidth: number
53 | preferredHeight: number
54 |
55 | ephemeral: boolean
56 | }
57 |
58 |
59 | export interface Content
60 | {
61 | contentId: ContentId
62 | title: string
63 | element: JSX.Element
64 | }
65 |
66 |
67 | export interface Divider
68 | {
69 | panel: Panel
70 | vertical: boolean
71 | rect: Rect
72 | resizeMin: number
73 | resizeMax: number
74 | }
75 |
76 |
77 | export interface Anchor
78 | {
79 | panel: Panel
80 | x: number
81 | y: number
82 | mode: DockMode
83 | previewRect: Rect
84 | }
85 |
86 |
87 | export interface Layout
88 | {
89 | panelRects: LayoutPanel[]
90 | content: LayoutContent[]
91 | dividers: Divider[]
92 | anchors: Anchor[]
93 | }
94 |
95 |
96 | export interface LayoutPanel
97 | {
98 | panel: Panel
99 | rect: Rect
100 | floating: boolean
101 | zIndex: number
102 | }
103 |
104 |
105 | export interface LayoutContent
106 | {
107 | content: Content
108 | tabIndex: number
109 | panel: Panel
110 | layoutPanel: LayoutPanel
111 | }
112 |
113 |
114 | export function makeState(): State
115 | {
116 | return {
117 | idNext: 2,
118 | rootPanel: {
119 | id: 1,
120 | floating: false,
121 | rect: new Rect(0, 0, 0, 0),
122 | contentList: [],
123 | currentTabIndex: 0,
124 | splitPanels: [],
125 | splitMode: SplitMode.LeftRight,
126 | splitSize: 0.5,
127 |
128 | preferredWidth: 300,
129 | preferredHeight: 250,
130 |
131 | ephemeral: false,
132 | },
133 | floatingPanels: [],
134 | activePanel: null,
135 |
136 | draggedPanel: null,
137 | showAnchors: false,
138 | previewAnchor: null,
139 | }
140 | }
141 |
142 |
143 | export function makePanel(state: State): Panel
144 | {
145 | const id = state.idNext++
146 | const panel: Panel = {
147 | id,
148 | floating: true,
149 | rect: new Rect(0, 0, 0, 0),
150 |
151 | contentList: [],
152 | currentTabIndex: 0,
153 |
154 | splitPanels: [],
155 | splitMode: SplitMode.LeftRight,
156 | splitSize: 0.5,
157 |
158 | preferredWidth: 300,
159 | preferredHeight: 250,
160 |
161 | ephemeral: false,
162 | }
163 | state.floatingPanels.push(panel)
164 | return panel
165 | }
166 |
167 |
168 | export function createDockedPanel(
169 | state: State,
170 | dockIntoPanel: Panel,
171 | mode: DockMode,
172 | content: ContentElement)
173 | : Panel
174 | {
175 | const panel = makePanel(state)
176 | addNewContent(state, panel, content)
177 | dock(state, panel, dockIntoPanel, mode)
178 | return panel
179 | }
180 |
181 |
182 | export function detachPanel(state: State, panel: Panel)
183 | {
184 | if (!panel.floating)
185 | {
186 | panel.floating = true
187 | state.activePanel = panel
188 | state.floatingPanels.push(panel)
189 | }
190 | }
191 |
192 |
193 | export function addNewContent(state: State, toPanel: Panel, element: ContentElement)
194 | {
195 | const id = state.idNext++
196 | toPanel.contentList.push({
197 | contentId: id,
198 | title: "",
199 | element,
200 | })
201 | toPanel.currentTabIndex = toPanel.contentList.length - 1
202 | toPanel.ephemeral = false
203 | }
204 |
205 |
206 | export function addContent(state: State, toPanel: Panel, content: Content)
207 | {
208 | const id = state.idNext++
209 | toPanel.contentList.push(content)
210 | toPanel.currentTabIndex = toPanel.contentList.length - 1
211 | toPanel.ephemeral = false
212 | }
213 |
214 |
215 | export function removeContent(state: State, fromPanel: Panel, contentId: ContentId)
216 | {
217 | const index = fromPanel.contentList.findIndex(w => w.contentId === contentId)
218 | if (index < 0)
219 | return
220 |
221 | fromPanel.contentList.splice(index, 1)
222 | fromPanel.currentTabIndex =
223 | Math.max(0,
224 | Math.min(fromPanel.contentList.length - 1,
225 | fromPanel.currentTabIndex))
226 | }
227 |
228 |
229 | export function removeEphemerals(state: State)
230 | {
231 | for (var i = 0; i < state.floatingPanels.length; i++)
232 | removeEphemeralsRecursive(state, state.floatingPanels[i])
233 |
234 | coallesceEmptyPanels(state)
235 | }
236 |
237 |
238 | export function removeEphemeralsRecursive(state: State, fromPanel: Panel)
239 | {
240 | for (var i = 0; i < fromPanel.splitPanels.length; i++)
241 | coallesceEmptyPanelsRecursive(state, fromPanel.splitPanels[i])
242 |
243 | if (fromPanel.ephemeral)
244 | {
245 | fromPanel.contentList = []
246 | fromPanel.currentTabIndex = 0
247 | }
248 | }
249 |
250 |
251 | export function coallesceEmptyPanels(state: State)
252 | {
253 | coallesceEmptyPanelsRecursive(state, state.rootPanel)
254 | for (var i = 0; i < state.floatingPanels.length; i++)
255 | coallesceEmptyPanelsRecursive(state, state.floatingPanels[i])
256 |
257 | state.floatingPanels = state.floatingPanels.filter(p =>
258 | p.contentList.length != 0 || p.splitPanels.length != 0)
259 | }
260 |
261 |
262 | export function coallesceEmptyPanelsRecursive(state: State, fromPanel: Panel)
263 | {
264 | for (var i = 0; i < fromPanel.splitPanels.length; i++)
265 | coallesceEmptyPanelsRecursive(state, fromPanel.splitPanels[i])
266 |
267 | fromPanel.splitPanels = fromPanel.splitPanels.filter(p =>
268 | p.contentList.length != 0 || p.splitPanels.length != 0)
269 |
270 | if (fromPanel.splitPanels.length == 1)
271 | Object.assign(fromPanel, fromPanel.splitPanels[0])
272 | }
273 |
274 |
275 | export function dock(state: State, panel: Panel, dockIntoPanel: Panel, mode: DockMode)
276 | {
277 | if (mode == DockMode.Full ||
278 | (dockIntoPanel.contentList.length == 0 && dockIntoPanel.splitPanels.length == 0))
279 | {
280 | if (dockIntoPanel.splitPanels.length > 0)
281 | throw "invalid full docking into subdivided panel"
282 |
283 | for (const window of panel.contentList)
284 | addContent(state, dockIntoPanel, window)
285 |
286 | detachPanel(state, panel)
287 | panel.contentList = []
288 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel)
289 | state.activePanel = dockIntoPanel
290 | }
291 | else if (mode == DockMode.Right ||
292 | mode == DockMode.Left ||
293 | mode == DockMode.Top ||
294 | mode == DockMode.Bottom)
295 | {
296 | const subdivMode =
297 | (mode == DockMode.Right || mode == DockMode.Left) ?
298 | SplitMode.LeftRight :
299 | SplitMode.TopBottom
300 |
301 | const subdivOriginalFirst =
302 | (mode == DockMode.Bottom || mode == DockMode.Right)
303 |
304 | const newSubpanels = [panel]
305 |
306 | const newSubpanel = makePanel(state)
307 | newSubpanel.contentList = dockIntoPanel.contentList
308 | newSubpanel.currentTabIndex = dockIntoPanel.currentTabIndex
309 | newSubpanel.splitMode = dockIntoPanel.splitMode
310 | newSubpanel.splitPanels = dockIntoPanel.splitPanels
311 | newSubpanel.splitSize = dockIntoPanel.splitSize
312 |
313 | dockIntoPanel.contentList = []
314 | dockIntoPanel.splitPanels = newSubpanels
315 | dockIntoPanel.splitMode = subdivMode
316 | dockIntoPanel.splitSize = subdivOriginalFirst ? 0.75 : 0.25
317 |
318 | if (subdivOriginalFirst)
319 | newSubpanels.unshift(newSubpanel)
320 | else
321 | newSubpanels.push(newSubpanel)
322 |
323 | panel.floating = false
324 | dockIntoPanel.floating = false
325 | newSubpanel.floating = false
326 | state.activePanel = panel
327 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel && p !== newSubpanel)
328 | }
329 | else
330 | {
331 | throw "invalid docking"
332 | }
333 | }
334 |
335 | export function setPanelActiveAndBringToFront(state: State, panel: Panel)
336 | {
337 | if (panel.contentList.length != 0)
338 | state.activePanel = panel
339 |
340 | if (!panel.floating)
341 | return
342 |
343 | state.floatingPanels = state.floatingPanels.filter(p => p !== panel)
344 | state.floatingPanels.push(panel)
345 |
346 | if (!panel.ephemeral)
347 | removeEphemerals(state)
348 | }
349 |
350 |
351 | export function clampFloatingPanels(state: State, rect: Rect)
352 | {
353 | const margin = 10
354 |
355 | for (const panel of state.floatingPanels)
356 | {
357 | panel.rect.x =
358 | Math.max(rect.x + margin - panel.rect.w / 2,
359 | Math.min(rect.x2 - margin - panel.rect.w / 2,
360 | panel.rect.x))
361 |
362 | panel.rect.y =
363 | Math.max(rect.y + margin,
364 | Math.min(rect.y2 - margin - panel.rect.h / 2,
365 | panel.rect.y))
366 | }
367 | }
368 |
369 |
370 | export function clampFloatingPanelStrictly(state: State, panel: Panel, rect: Rect)
371 | {
372 | const margin = 10
373 |
374 | panel.rect.x =
375 | Math.max(rect.x + margin,
376 | Math.min(rect.x2 - margin - panel.rect.w,
377 | panel.rect.x))
378 |
379 | panel.rect.y =
380 | Math.max(rect.y + margin,
381 | Math.min(rect.y2 - margin - panel.rect.h,
382 | panel.rect.y))
383 | }
384 |
385 |
386 | export function traverseLayout(panel: Panel, rect: Rect, layout: Layout)
387 | {
388 | const xMid = (rect.x1 + rect.x2) / 2
389 | const yMid = (rect.y1 + rect.y2) / 2
390 |
391 | if (panel.splitPanels.length == 2)
392 | {
393 | if (panel.splitMode == SplitMode.LeftRight)
394 | {
395 | const xSplit = rect.x1 + Math.round((rect.x2 - rect.x1) * panel.splitSize)
396 |
397 | const rect1 = rect.withX2(xSplit)
398 | const rect2 = rect.withX1(xSplit)
399 | const rectDivider = rect.withX1(xSplit).withX2(xSplit)
400 |
401 | traverseLayout(panel.splitPanels[0], rect1, layout)
402 | traverseLayout(panel.splitPanels[1], rect2, layout)
403 |
404 | layout.dividers.push({
405 | panel,
406 | vertical: false,
407 | rect: rectDivider,
408 | resizeMin: rect.x1,
409 | resizeMax: rect.x2,
410 | })
411 | }
412 | else if (panel.splitMode == SplitMode.TopBottom)
413 | {
414 | const ySplit = rect.y1 + Math.round((rect.y2 - rect.y1) * panel.splitSize)
415 |
416 | const rect1 = rect.withY2(ySplit)
417 | const rect2 = rect.withY1(ySplit)
418 | const rectDivider = rect.withY1(ySplit).withY2(ySplit)
419 |
420 | traverseLayout(panel.splitPanels[0], rect1, layout)
421 | traverseLayout(panel.splitPanels[1], rect2, layout)
422 |
423 | layout.dividers.push({
424 | panel,
425 | vertical: true,
426 | rect: rectDivider,
427 | resizeMin: rect.y1,
428 | resizeMax: rect.y2,
429 | })
430 | }
431 | }
432 | else
433 | {
434 | const panelRect = {
435 | panel,
436 | rect,
437 | floating: false,
438 | zIndex: 0,
439 | }
440 |
441 | for (let w = 0; w < panel.contentList.length; w++)
442 | {
443 | layout.content.push(
444 | {
445 | content: panel.contentList[w],
446 | tabIndex: w,
447 | panel,
448 | layoutPanel: panelRect,
449 | })
450 | }
451 |
452 | layout.panelRects.push(panelRect)
453 |
454 | layout.anchors.push({
455 | panel,
456 | x: xMid,
457 | y: yMid,
458 | mode: DockMode.Full,
459 | previewRect: rect,
460 | })
461 | }
462 |
463 | layout.anchors.push({
464 | panel,
465 | x: rect.x2 - 10,
466 | y: yMid,
467 | mode: DockMode.Right,
468 | previewRect: rect.withX1(rect.x1 + (rect.x2 - rect.x1) * 3 / 4),
469 | })
470 |
471 | layout.anchors.push({
472 | panel,
473 | x: rect.x1 + 10,
474 | y: yMid,
475 | mode: DockMode.Left,
476 | previewRect: rect.withX2(rect.x1 + (rect.x2 - rect.x1) / 4),
477 | })
478 |
479 | layout.anchors.push({
480 | panel,
481 | x: xMid,
482 | y: rect.y2 - 10,
483 | mode: DockMode.Bottom,
484 | previewRect: rect.withY1(rect.y1 + (rect.y2 - rect.y1) * 3 / 4),
485 | })
486 |
487 | layout.anchors.push({
488 | panel,
489 | x: xMid,
490 | y: rect.y1 + 10,
491 | mode: DockMode.Top,
492 | previewRect: rect.withY2(rect.y1 + (rect.y2 - rect.y1) / 4),
493 | })
494 | }
495 |
496 |
497 | export function getLayout(state: State, rect: Rect): Layout
498 | {
499 | const layout: Layout =
500 | {
501 | panelRects: [],
502 | content: [],
503 | dividers: [],
504 | anchors: [],
505 | }
506 |
507 | traverseLayout(state.rootPanel, rect, layout)
508 |
509 | for (let fp = 0; fp < state.floatingPanels.length; fp++)
510 | {
511 | const floatingPanel = state.floatingPanels[fp]
512 |
513 | const panelRect =
514 | {
515 | panel: floatingPanel,
516 | rect: floatingPanel.rect,
517 | floating: true,
518 | zIndex: fp + 1,
519 | }
520 |
521 | for (let w = 0; w < floatingPanel.contentList.length; w++)
522 | {
523 | layout.content.push(
524 | {
525 | content: floatingPanel.contentList[w],
526 | tabIndex: w,
527 | panel: floatingPanel,
528 | layoutPanel: panelRect,
529 | })
530 | }
531 |
532 | layout.panelRects.push(panelRect)
533 |
534 | layout.anchors.push({
535 | panel: floatingPanel,
536 | x: floatingPanel.rect.xCenter,
537 | y: floatingPanel.rect.yCenter,
538 | mode: DockMode.Full,
539 | previewRect: floatingPanel.rect,
540 | })
541 | }
542 |
543 | return layout
544 | }
545 |
546 |
547 | export function getContentRect(state: State, rect: Rect, contentId: ContentId): Rect | undefined
548 | {
549 | const layout = getLayout(state, rect)
550 | return layout.panelRects.find(p => p.panel.contentList.some(c => c.contentId === contentId))!.rect
551 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "target": "esnext",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "baseUrl": "./src",
8 | "rootDir": "./src",
9 | "outDir": "./dist",
10 | "strict": true,
11 | "jsx": "preserve",
12 | "declaration": true,
13 | "paths": {
14 | "@hlorenzi/react-dockable": ["../dist"]
15 | }
16 | },
17 | "include": [
18 | "./src/index.ts"
19 | ],
20 | "exclude": [
21 | "node_modules"
22 | ]
23 | }
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 |
4 | module.exports =
5 | {
6 | mode: "production",
7 | entry:
8 | {
9 | main: path.resolve(__dirname, "example/main.tsx"),
10 | },
11 |
12 | output:
13 | {
14 | filename: "[name].js",
15 | path: path.resolve(__dirname, "build")
16 | },
17 |
18 | resolve: {
19 | extensions: [".ts", ".tsx", ".js", ".json"],
20 | fallback: {
21 | "@hlorenzi/react-dockable": path.resolve(__dirname, "dist"),
22 | },
23 | },
24 |
25 | module:
26 | {
27 | rules:
28 | [
29 | {
30 | test: /\.(js|jsx|ts|tsx)$/,
31 | exclude: /node_modules/,
32 | use:
33 | {
34 | loader: "babel-loader",
35 | options: {
36 | presets: [
37 | "@babel/preset-typescript",
38 | "@babel/preset-env",
39 | "@babel/preset-react",
40 | ]
41 | }
42 | }
43 | }
44 | ]
45 | }
46 | }
--------------------------------------------------------------------------------