Add support for resizing panes in the playground (#2581)
fix #2422 ![Kapture 2023-10-17 at 15 12 37](https://github.com/microsoft/typespec/assets/1031227/e2eea00b-3d2f-454a-9c89-76c678c6dedb)
This commit is contained in:
Родитель
f39f3a7a99
Коммит
9d2cf852e7
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/playground",
|
||||
"comment": "Add resizable panes for the editor and output",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/playground"
|
||||
}
|
|
@ -882,6 +882,9 @@ importers:
|
|||
c8:
|
||||
specifier: ~8.0.1
|
||||
version: 8.0.1
|
||||
copyfiles:
|
||||
specifier: ~2.4.1
|
||||
version: 2.4.1
|
||||
cross-env:
|
||||
specifier: ~7.0.3
|
||||
version: 7.0.3
|
||||
|
@ -8710,6 +8713,19 @@ packages:
|
|||
webpack: 5.88.2(@swc/core@1.3.62)
|
||||
dev: false
|
||||
|
||||
/copyfiles@2.4.1:
|
||||
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
mkdirp: 1.0.4
|
||||
noms: 0.0.0
|
||||
through2: 2.0.5
|
||||
untildify: 4.0.0
|
||||
yargs: 16.2.0
|
||||
dev: true
|
||||
|
||||
/core-js-compat@3.32.2:
|
||||
resolution: {integrity: sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==}
|
||||
dependencies:
|
||||
|
@ -8728,7 +8744,6 @@ packages:
|
|||
|
||||
/core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
dev: false
|
||||
|
||||
/cose-base@1.0.3:
|
||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||
|
@ -11711,7 +11726,6 @@ packages:
|
|||
|
||||
/isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: false
|
||||
|
||||
/isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
@ -12900,7 +12914,6 @@ packages:
|
|||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
|
@ -13145,6 +13158,13 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/noms@0.0.0:
|
||||
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 1.0.34
|
||||
dev: true
|
||||
|
||||
/non-layered-tidy-tree-layout@2.0.2:
|
||||
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
|
||||
|
||||
|
@ -14144,7 +14164,6 @@ packages:
|
|||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: false
|
||||
|
||||
/process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
|
@ -14709,6 +14728,15 @@ packages:
|
|||
mute-stream: 0.0.8
|
||||
dev: true
|
||||
|
||||
/readable-stream@1.0.34:
|
||||
resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 0.0.1
|
||||
string_decoder: 0.10.31
|
||||
dev: true
|
||||
|
||||
/readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
dependencies:
|
||||
|
@ -14719,7 +14747,6 @@ packages:
|
|||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
dev: false
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
|
@ -15109,7 +15136,6 @@ packages:
|
|||
|
||||
/safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
dev: false
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
@ -15605,11 +15631,14 @@ packages:
|
|||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
/string_decoder@0.10.31:
|
||||
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
|
||||
dev: true
|
||||
|
||||
/string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
dev: false
|
||||
|
||||
/string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
@ -15941,6 +15970,13 @@ packages:
|
|||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
/through2@2.0.5:
|
||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
xtend: 4.0.2
|
||||
dev: true
|
||||
|
||||
/through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
requiresBuild: true
|
||||
|
@ -16429,7 +16465,6 @@ packages:
|
|||
/untildify@4.0.0:
|
||||
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/update-browserslist-db@1.0.12(browserslist@4.21.10):
|
||||
resolution: {integrity: sha512-tE1smlR58jxbFMtrMpFNRmsrOXlpNXss965T1CrpwuZUzUAg/TBQc94SpyhDLSzrqrJS9xTRBthnZAGcE1oaxg==}
|
||||
|
@ -17249,7 +17284,6 @@ packages:
|
|||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: false
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
declare module "*.module.css";
|
|
@ -30,8 +30,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./dist ./dist-dev ./temp ./typespecContents.json",
|
||||
"build": "tsc -p .",
|
||||
"watch": "tsc -p . --watch",
|
||||
"build": "tsc -p . && npm run copy-css",
|
||||
"watch": "npm run copy-css && tsc -p . --watch",
|
||||
"copy-css": "copyfiles -u 1 src/**/*.module.css dist/src",
|
||||
"preview": "npm run build && vite preview",
|
||||
"start": "vite",
|
||||
"e2e": "cross-env PW_EXPERIMENTAL_TS_ESM=1 playwright test -c e2e ",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"rimraf": "~5.0.1",
|
||||
"rollup-plugin-visualizer": "~5.9.2",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
"vite": "^4.4.9",
|
||||
"copyfiles": "~2.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ export { createBrowserHost } from "./browser-host.js";
|
|||
export { registerMonacoDefaultWorkers } from "./monaco-worker.js";
|
||||
export { registerMonacoLanguage } from "./services.js";
|
||||
export { createUrlStateStorage } from "./state-storage.js";
|
||||
export { PlaygroundSample } from "./types.js";
|
||||
export type { PlaygroundSample } from "./types.js";
|
||||
export { resolveLibraries as filterEmitters } from "./utils.js";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { Playground, PlaygroundProps } from "./playground.js";
|
||||
export { Playground } from "./playground.js";
|
||||
export type { PlaygroundProps } from "./playground.js";
|
||||
export { createReactPlayground, renderReactPlayground } from "./standalone.js";
|
||||
export * from "./types.js";
|
||||
|
|
|
@ -14,6 +14,8 @@ import { useMonacoModel } from "./editor.js";
|
|||
import { Footer } from "./footer.js";
|
||||
import { useAsyncMemo, useControllableValue } from "./hooks.js";
|
||||
import { OutputView } from "./output-view.js";
|
||||
import Pane from "./split-pane/pane.js";
|
||||
import { SplitPane } from "./split-pane/split-pane.js";
|
||||
import { CompilationState, FileOutputViewer } from "./types.js";
|
||||
import { TypeSpecEditor } from "./typespec-editor.js";
|
||||
|
||||
|
@ -208,45 +210,42 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
|
|||
<div
|
||||
css={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gridTemplateColumns: "1",
|
||||
gridTemplateRows: "1fr auto",
|
||||
gridTemplateAreas: '"typespeceditor output"\n "footer footer"',
|
||||
gridTemplateAreas: '"typespeceditor"\n "footer"',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
fontFamily: `"Segoe UI", Tahoma, Geneva, Verdana, sans-serif`,
|
||||
}}
|
||||
>
|
||||
<div css={{ gridArea: "typespeceditor", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<EditorCommandBar
|
||||
libraries={libraries}
|
||||
selectedEmitter={selectedEmitter}
|
||||
onSelectedEmitterChange={onSelectedEmitterChange}
|
||||
compilerOptions={compilerOptions}
|
||||
onCompilerOptionsChange={onCompilerOptionsChange}
|
||||
samples={props.samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
saveCode={saveCode}
|
||||
newIssue={props?.links?.githubIssueUrl ? newIssue : undefined}
|
||||
documentationUrl={props.links?.documentationUrl}
|
||||
/>
|
||||
<TypeSpecEditor model={typespecModel} actions={typespecEditorActions} />
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
gridArea: "output",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderLeft: "1px solid #c5c5c5",
|
||||
}}
|
||||
<SplitPane
|
||||
initialSizes={["50%", "50%"]}
|
||||
css={{ gridArea: "typespeceditor", width: "100%", height: "100%", overflow: "hidden" }}
|
||||
>
|
||||
<OutputView
|
||||
compilationState={compilationState}
|
||||
viewers={props.emitterViewers?.[selectedEmitter]}
|
||||
/>
|
||||
</div>
|
||||
<Pane>
|
||||
<EditorCommandBar
|
||||
libraries={libraries}
|
||||
selectedEmitter={selectedEmitter}
|
||||
onSelectedEmitterChange={onSelectedEmitterChange}
|
||||
compilerOptions={compilerOptions}
|
||||
onCompilerOptionsChange={onCompilerOptionsChange}
|
||||
samples={props.samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
saveCode={saveCode}
|
||||
newIssue={props?.links?.githubIssueUrl ? newIssue : undefined}
|
||||
documentationUrl={props.links?.documentationUrl}
|
||||
/>
|
||||
<TypeSpecEditor model={typespecModel} actions={typespecEditorActions} />
|
||||
</Pane>
|
||||
<Pane>
|
||||
<OutputView
|
||||
compilationState={compilationState}
|
||||
viewers={props.emitterViewers?.[selectedEmitter]}
|
||||
/>
|
||||
</Pane>
|
||||
</SplitPane>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { SplitPane, SplitPaneProps } from "./split-pane.js";
|
|
@ -0,0 +1,13 @@
|
|||
import { HTMLAttributes, PropsWithChildren } from "react";
|
||||
|
||||
export interface PaneProps {
|
||||
maxSize?: number | string;
|
||||
minSize?: number | string;
|
||||
}
|
||||
|
||||
export default function Pane({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<HTMLAttributes<HTMLDivElement> & PaneProps>) {
|
||||
return <div {...props}>{children}</div>;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import { ReactNode } from "react";
|
||||
import style from "./split-pane.module.css";
|
||||
|
||||
export interface SashContentProps {
|
||||
className?: string;
|
||||
dragging?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const SashContent: React.FunctionComponent<SashContentProps> = ({
|
||||
className,
|
||||
children,
|
||||
dragging,
|
||||
...others
|
||||
}: SashContentProps) => {
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(
|
||||
style["sash-content"],
|
||||
dragging && style["sash-content-dragging"],
|
||||
className
|
||||
)}
|
||||
{...others}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import { ReactNode, useState } from "react";
|
||||
import style from "./split-pane.module.css";
|
||||
|
||||
export interface SashProps {
|
||||
className?: string;
|
||||
style: React.CSSProperties;
|
||||
render: (dragging: boolean) => ReactNode;
|
||||
onReset: () => void;
|
||||
onDragStart: React.MouseEventHandler<HTMLDivElement>;
|
||||
onDragging: React.MouseEventHandler<HTMLDivElement>;
|
||||
onDragEnd: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const Sash = ({
|
||||
className,
|
||||
render,
|
||||
onDragStart,
|
||||
onDragging,
|
||||
onDragEnd,
|
||||
onReset,
|
||||
...others
|
||||
}: SashProps) => {
|
||||
const [draging, setDrag] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: any) => {
|
||||
onDragging(e);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: any) => {
|
||||
setDrag(false);
|
||||
onDragEnd(e);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(style["sash"], className)}
|
||||
onMouseDown={(e) => {
|
||||
setDrag(true);
|
||||
onDragStart(e);
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
}}
|
||||
onDoubleClick={onReset}
|
||||
{...others}
|
||||
>
|
||||
{render(draging)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
.split-pane {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.split-disabled {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.split-pane-dragging.react-split-vertical {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.split-pane-dragging.react-split-horizontal {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.sash {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: background-color 0.1s;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sash-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sash-vertical {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.sash-horizontal {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.sash-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.sash-content-dragging,
|
||||
.sash-content:hover {
|
||||
background-color: #c3c3c3;
|
||||
}
|
||||
|
||||
.pane {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import {
|
||||
FunctionComponent,
|
||||
JSX,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useControllableValue } from "../hooks.js";
|
||||
import Pane, { PaneProps } from "./pane.js";
|
||||
import { SashContent } from "./sash-content.js";
|
||||
import { Sash } from "./sash.js";
|
||||
import style from "./split-pane.module.css";
|
||||
|
||||
export interface SplitPaneProps {
|
||||
children: JSX.Element[];
|
||||
allowResize?: boolean;
|
||||
split?: "vertical" | "horizontal";
|
||||
initialSizes: (string | number)[];
|
||||
sizes?: (string | number)[];
|
||||
sashRender?: (index: number, active: boolean) => React.ReactNode;
|
||||
onChange?: (sizes: number[]) => void;
|
||||
onDragStart?: (e: MouseEvent) => void;
|
||||
onDragEnd?: (e: MouseEvent) => void;
|
||||
className?: string;
|
||||
sashClassName?: string;
|
||||
performanceMode?: boolean;
|
||||
resizerSize?: number;
|
||||
}
|
||||
|
||||
interface Axis {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface CacheSizes {
|
||||
sizes: (string | number)[];
|
||||
sashPosSizes: (string | number)[];
|
||||
}
|
||||
|
||||
export const SplitPane: FunctionComponent<SplitPaneProps> = ({
|
||||
children,
|
||||
sizes: propSizes,
|
||||
initialSizes: defaultSizes,
|
||||
allowResize = true,
|
||||
split = "vertical",
|
||||
className: wrapClassName,
|
||||
sashRender = (_, active) => <SashContent dragging={active} />,
|
||||
resizerSize = 4,
|
||||
performanceMode = false,
|
||||
onChange = () => null,
|
||||
onDragStart = () => null,
|
||||
onDragEnd = () => null,
|
||||
...others
|
||||
}: SplitPaneProps) => {
|
||||
const [resolvedPropSize, updateSizes] = useControllableValue<(string | number)[]>(
|
||||
propSizes,
|
||||
defaultSizes,
|
||||
onChange as any
|
||||
);
|
||||
const axis = useRef<Axis>({ x: 0, y: 0 });
|
||||
const wrapper = useRef<HTMLDivElement>(null);
|
||||
const cacheSizes = useRef<CacheSizes>({ sizes: [], sashPosSizes: [] });
|
||||
const [wrapperRect, setWrapperRect] = useState<Record<string, number>>({});
|
||||
const [isDragging, setDragging] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setWrapperRect(wrapper?.current?.getBoundingClientRect() ?? ({} as any));
|
||||
});
|
||||
resizeObserver.observe(wrapper.current!);
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { sizeName, splitPos, splitAxis } = useMemo(
|
||||
() =>
|
||||
({
|
||||
sizeName: split === "vertical" ? "width" : "height",
|
||||
splitPos: split === "vertical" ? "left" : "top",
|
||||
splitAxis: split === "vertical" ? "x" : "y",
|
||||
}) as const,
|
||||
[split]
|
||||
);
|
||||
|
||||
const wrapSize: number = wrapperRect[sizeName] ?? 0;
|
||||
|
||||
// Get limit sizes via children
|
||||
const paneLimitSizes = useMemo(
|
||||
() =>
|
||||
children.map((childNode) => {
|
||||
const limits = [0, Infinity];
|
||||
if (childNode?.type === Pane) {
|
||||
const { minSize, maxSize } = childNode.props as PaneProps;
|
||||
limits[0] = assertsSize(minSize, wrapSize, 0);
|
||||
limits[1] = assertsSize(maxSize, wrapSize);
|
||||
}
|
||||
return limits;
|
||||
}),
|
||||
[children, wrapSize]
|
||||
);
|
||||
|
||||
const sizes = useMemo(
|
||||
function () {
|
||||
let count = 0;
|
||||
let curSum = 0;
|
||||
const res = children.map((_, index) => {
|
||||
const size = assertsSize(resolvedPropSize[index], wrapSize);
|
||||
size === Infinity ? count++ : (curSum += size);
|
||||
return size;
|
||||
});
|
||||
|
||||
// resize or illegal size input,recalculate pane sizes
|
||||
if (curSum > wrapSize || (!count && curSum < wrapSize)) {
|
||||
const cacheNum = (curSum - wrapSize) / curSum;
|
||||
return res.map((size) => {
|
||||
return size === Infinity ? 0 : size - size * cacheNum;
|
||||
});
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
const average = (wrapSize - curSum) / count;
|
||||
return res.map((size) => {
|
||||
return size === Infinity ? average : size;
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
[...resolvedPropSize, children.length, wrapSize]
|
||||
);
|
||||
|
||||
const sashPosSizes = useMemo(
|
||||
() => sizes.reduce((a, b) => [...a, a[a.length - 1] + b], [0]),
|
||||
[...sizes]
|
||||
);
|
||||
|
||||
const dragStart = useCallback(
|
||||
(e: any) => {
|
||||
document?.body?.classList?.add(style["split-disabled"]);
|
||||
axis.current = { x: e.pageX, y: e.pageY };
|
||||
cacheSizes.current = { sizes, sashPosSizes };
|
||||
setDragging(true);
|
||||
onDragStart(e);
|
||||
},
|
||||
[onDragStart, sizes, sashPosSizes]
|
||||
);
|
||||
|
||||
const resetPosition = useCallback(() => {
|
||||
updateSizes(defaultSizes);
|
||||
}, [defaultSizes, updateSizes]);
|
||||
|
||||
const dragEnd = useCallback(
|
||||
(e: any) => {
|
||||
document?.body?.classList?.remove(style["split-disabled"]);
|
||||
axis.current = { x: e.pageX, y: e.pageY };
|
||||
cacheSizes.current = { sizes, sashPosSizes };
|
||||
setDragging(false);
|
||||
onDragEnd(e);
|
||||
},
|
||||
[onDragEnd, sizes, sashPosSizes]
|
||||
);
|
||||
|
||||
const onDragging = useCallback(
|
||||
function (e: MouseEvent<HTMLDivElement>, i: number) {
|
||||
const curAxis = { x: e.pageX, y: e.pageY };
|
||||
let distanceX = curAxis[splitAxis] - axis.current[splitAxis];
|
||||
|
||||
const leftBorder = -Math.min(
|
||||
sizes[i] - paneLimitSizes[i][0],
|
||||
paneLimitSizes[i + 1][1] - sizes[i + 1]
|
||||
);
|
||||
const rightBorder = Math.min(
|
||||
sizes[i + 1] - paneLimitSizes[i + 1][0],
|
||||
paneLimitSizes[i][1] - sizes[i]
|
||||
);
|
||||
|
||||
if (distanceX < leftBorder) {
|
||||
distanceX = leftBorder;
|
||||
}
|
||||
if (distanceX > rightBorder) {
|
||||
distanceX = rightBorder;
|
||||
}
|
||||
|
||||
const nextSizes = [...sizes];
|
||||
nextSizes[i] += distanceX;
|
||||
nextSizes[i + 1] -= distanceX;
|
||||
|
||||
updateSizes(nextSizes);
|
||||
},
|
||||
[paneLimitSizes, onChange]
|
||||
);
|
||||
|
||||
const paneFollow = !(performanceMode && isDragging);
|
||||
const paneSizes = paneFollow ? sizes : cacheSizes.current.sizes;
|
||||
const panePoses = paneFollow ? sashPosSizes : cacheSizes.current.sashPosSizes;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(
|
||||
style["split-pane"],
|
||||
split === "vertical" && style["split-pane-vertical"],
|
||||
split === "horizontal" && style["split-pane-horizontal"],
|
||||
isDragging && style["split-pane-dragging"],
|
||||
wrapClassName
|
||||
)}
|
||||
ref={wrapper}
|
||||
{...others}
|
||||
>
|
||||
{children.map((childNode, childIndex) => {
|
||||
const isPane = childNode.type === Pane;
|
||||
const paneProps = isPane ? childNode.props : {};
|
||||
|
||||
return (
|
||||
<Pane
|
||||
key={childIndex}
|
||||
className={mergeClasses(style["pane"], paneProps.className)}
|
||||
style={{
|
||||
...paneProps.style,
|
||||
[sizeName]: paneSizes[childIndex],
|
||||
[splitPos]: panePoses[childIndex],
|
||||
}}
|
||||
>
|
||||
{isPane ? paneProps.children : childNode}
|
||||
</Pane>
|
||||
);
|
||||
})}
|
||||
{sashPosSizes.slice(1, -1).map((posSize, index) => (
|
||||
<Sash
|
||||
key={index}
|
||||
className={mergeClasses(
|
||||
!allowResize && style["sash-disabled"],
|
||||
split === "vertical" ? style["sash-vertical"] : style["sash-horizontal"]
|
||||
)}
|
||||
style={{
|
||||
[sizeName]: resizerSize,
|
||||
[splitPos]: posSize - resizerSize / 2,
|
||||
}}
|
||||
render={sashRender.bind(null, index)}
|
||||
onReset={resetPosition}
|
||||
onDragStart={dragStart}
|
||||
onDragging={(e) => onDragging(e, index)}
|
||||
onDragEnd={dragEnd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert size to absolute number or Infinity
|
||||
* SplitPane allows sizes in string and number, but the state sizes only support number,
|
||||
* so convert string and number to number in here
|
||||
* 'auto' -> divide the remaining space equally
|
||||
* 'xxx px' -> xxx
|
||||
* 'xxx%' -> wrapper.size * xxx/100
|
||||
* xxx -> xxx
|
||||
*/
|
||||
function assertsSize(size: string | number | undefined, sum: number, defaultValue = Infinity) {
|
||||
if (typeof size === "undefined") return defaultValue;
|
||||
if (typeof size === "number") return size;
|
||||
if (size.endsWith("%")) return sum * (+size.replace("%", "") / 100);
|
||||
if (size.endsWith("px")) return +size.replace("px", "");
|
||||
return defaultValue;
|
||||
}
|
Загрузка…
Ссылка в новой задаче