├── .gitignore
├── .storybook
├── main.js
├── preview-head.html
└── preview.js
├── LICENSE
├── README.md
├── build
└── gulpfile.js
├── docs
├── records.md
└── 中文文档.md
├── index.html
├── package.json
├── src
├── SashContent.tsx
├── SplitPane.tsx
├── base.ts
├── index.ts
├── pane.tsx
├── sash.tsx
├── themes
│ └── default.scss
└── types.ts
├── stories
├── 01Vertical.stories.tsx
├── 02Horizontal.stories.tsx
├── 03Complex.stories.tsx
├── 04Percentage.stories.tsx
├── 05LimitSize.stories.tsx
├── 06AllowResize.stories.tsx
├── 07ResizeStyle.stories.tsx
├── 08CustomSash.stories.tsx
└── 09PerformanceMode.stories.tsx
├── tsconfig.json
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | esm
12 | dist
13 | dist-ssr
14 | *.local
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../stories/*.stories.tsx",
4 | ],
5 | "addons": [
6 | "@storybook/addon-links",
7 | "@storybook/addon-essentials",
8 | "@storybook/addon-interactions"
9 | ],
10 | "framework": "@storybook/react",
11 | "core": {
12 | "builder": "@storybook/builder-vite"
13 | },
14 | "features": {
15 | "storyStoreV7": true
16 | },
17 | async viteFinal(config) {
18 | return Object.assign({}, config, {
19 | base: '/split-pane-react/'
20 | });
21 | },
22 | }
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: "^on[A-Z].*" },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/,
7 | },
8 | },
9 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 yyllff
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [中文](/docs/中文文档.md)
3 |
4 | # split-pane-react
5 | > Resizable split panes for [React.js](http://reactjs.org).Check out the [live demo](https://yyllff.github.io/split-pane-react/). Different [themes](https://codesandbox.io/s/split-pane-themes-xmsqtt).
6 |
7 | ## Features
8 |
9 | - 💪**Simple api** and support for multiple panels.
10 | - 🔥Supports **vertical & horizontal layouts** and **fluid pane**.
11 | - 🎉Use **controlled component** mode, flexible use.
12 | - 😎**React16.8** version at least, and **React18** version at the same time.
13 | - 👷♂️Support flexible and convenient **customization of sash**.
14 |
15 |
16 | ## Installing
17 |
18 | ````sh
19 | # use npm
20 | npm install split-pane-react
21 |
22 | # or if you use yarn
23 | yarn add split-pane-react
24 | ````
25 |
26 | ## Example Usage
27 |
28 | ```jsx
29 | import React, { useState } from 'react';
30 | import SplitPane, { Pane } from 'split-pane-react';
31 | import 'split-pane-react/esm/themes/default.css';
32 |
33 | function style (color) {
34 | return {
35 | height: '100%',
36 | display: 'flex',
37 | alignItems: 'center',
38 | justifyContent: 'center',
39 | backgroundColor: color
40 | };
41 | }
42 |
43 | function App () {
44 | const [sizes, setSizes] = useState([100, '30%', 'auto']);
45 |
46 | return (
47 |
48 |
53 |
54 |
55 | pane1
56 |
57 |
58 |
59 | pane2
60 |
61 |
62 | pane3
63 |
64 |
65 |
66 | );
67 | };
68 | ```
69 |
70 | ## props
71 |
72 | **SplitPane**
73 |
74 | | Property | Description | Type | Default |
75 | | -------------- | ---------------- | :--------: | :----------: |
76 | | split | Determine layout of panes. | 'vertical' \|'horizontal' |'vertical' |
77 | | sizes | Collection of different panel sizes,Only support controlled mode, so it's required | (string \| number)[] |[] |
78 | | resizerSize | Specify the size for resizer | number |4 |
79 | | allowResize | Should allowed to resized | boolean |true |
80 | | className | split pane custom class name | string |void |
81 | | sashRender | User defined sliding axis function | (index: number, active: boolean) => void |void |
82 | | performanceMode | High performance mode to avoid excessive pressure on the browser | boolean | false |
83 | | onChange | Callback of size change | (sizes: number[]) => void |void |
84 | | onDragStart | This callback is invoked when a drag starts | () => void |void |
85 | | onDragEnd | This callback is invoked when a drag ends | () => void |void |
86 |
87 | **Pane**
88 |
89 | | Property | Description | Type | Default |
90 | | ------------------ | ---------------- | :--------: | ------------------ |
91 | | className | pane class name | string | void |
92 | | minSize | Limit the minimum size of the panel | string \| number | void |
93 | | maxSize | Limit the maximum size of this panel | string\|number | void |
94 |
95 | ## themes
96 |
97 | You can use the sashRender parameter to configure the theme you need:
98 |
99 | - The default theme style refers to vscode style
100 | - At the same time, we have built in a theme similar to sublime
101 | - Other demo [themes](https://codesandbox.io/s/split-pane-themes-xmsqtt).
102 |
103 |
104 | ## License
105 |
106 | **[split-pane-react](https://github.com/yyllff/split-pane-react)** licensed under [MIT](LICENSE).
107 |
108 | > PS: I would love to know if you're using split-pane-react. If you have any use problems, you can raise the issue, and I will repair them as soon as possible. The project will always be maintained. If you have a good idea, you can propose a merge. **If this component helps you, please leave your star. Those who need it will be very grateful.**
109 |
--------------------------------------------------------------------------------
/build/gulpfile.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const gulp = require('gulp');
3 | const ts = require('gulp-typescript');
4 | const rimraf = require('rimraf');
5 | const sass = require('gulp-sass')(require('sass'));
6 | const tsProject = ts.createProject('../tsconfig.json');
7 |
8 | const output = '../esm';
9 |
10 | gulp.task('clean', function (cb) {
11 | rimraf(output, cb);
12 | });
13 |
14 | gulp.task('build:esm', function () {
15 | return tsProject
16 | .src()
17 | .pipe(tsProject())
18 | .on('error', function (error, callback) {
19 | console.error(error.stack);
20 | })
21 | .pipe(gulp.dest(output));
22 | });
23 |
24 | gulp.task('build:sass', function () {
25 | return gulp
26 | .src('../src/**/*.scss')
27 | .pipe(sass())
28 | .pipe(gulp.dest(output));
29 | });
30 |
31 | gulp.task(
32 | 'default',
33 | gulp.series(
34 | 'clean',
35 | 'build:esm',
36 | 'build:sass'
37 | )
38 | );
39 |
--------------------------------------------------------------------------------
/docs/records.md:
--------------------------------------------------------------------------------
1 |
2 | # 项目完善记录
3 |
4 | ## package.json中dependencies和peerDependencies问题
5 |
6 | 需要将react和react-dom作为peerDependencies
7 | 如果作为dependencies,用户安装的时候,可能会遇到React版本冲突的问题
8 | 从而导致重复下载React,在codesandbox下,导致页面崩溃
9 |
10 | ##
11 |
12 |
--------------------------------------------------------------------------------
/docs/中文文档.md:
--------------------------------------------------------------------------------
1 | # split-pane-react
2 | > 拆分面板组件,基于 [React.js](http://reactjs.org).查看 [实时demo](https://yyllff.github.io/split-pane-react/). 不同 [主题demo](https://codesandbox.io/s/split-pane-themes-xmsqtt).
3 |
4 | ## 特征
5 |
6 | - **简单的api** 并且支持多面板.
7 | - 支持 **横向&竖向布局** 和 **流体布局**.
8 | - 使用 **受控组件** 模式, 使用灵活方便.
9 | - 支持 **React16.8** 到 **React18**版本,因为使用了React Hooks.
10 | - 支持方便灵活的 **滑轴定制**.
11 |
12 |
13 | ## 安装
14 |
15 | ````sh
16 | # use npm
17 | npm install split-pane-react
18 |
19 | # or if you use yarn
20 | yarn add split-pane-react
21 | ````
22 |
23 | ## 使用样例
24 |
25 | ```js
26 | import React, { useState } from 'react';
27 | import SplitPane, { Pane } from 'split-pane-react';
28 | import 'split-pane-react/esm/themes/default.css';
29 |
30 | function style (color) {
31 | return {
32 | height: '100%',
33 | display: 'flex',
34 | alignItems: 'center',
35 | justifyContent: 'center',
36 | backgroundColor: color
37 | };
38 | }
39 |
40 | function App () {
41 | const [sizes, setSizes] = useState([100, '30%', 'auto']);
42 |
43 | return (
44 |
45 |
50 |
51 |
52 | pane1
53 |
54 |
55 |
56 | pane2
57 |
58 |
59 | pane3
60 |
61 |
62 |
63 | );
64 | };
65 | ```
66 |
67 | ## 属性
68 |
69 | ** SplitPane **
70 |
71 | | Property | Description | Type | Default |
72 | | -------------- | ---------------- | :--------: | :----------: |
73 | | split | 布局方式,支持水平和竖直两种. | 'vertical' \|'horizontal' |'vertical' |
74 | | sizes | 每个面板尺寸,因为是受控组件模式,所以该属性必传| (string \| number)[] |[] |
75 | | resizerSize | 指定滑轴的尺寸(宽度) | number |4 |
76 | | allowResize | 面板尺寸是否支持调整,设置为false后,滑轴将不能滑动 | boolean |true |
77 | | className | 自定义className | string |void |
78 | | sashRender | 自定义滑轴样式函数 | (index: number, active: boolean) => void |void |
79 | | performanceMode | 开启高性能模式,将实时更新尺寸改为滑动完成后更新,避免页面频繁重排 | boolean | false |
80 | | onChange | 尺寸改变时的回调函数 | (sizes: number[]) => void |void |
81 | | onDragStart | 拖拽开始时的回调函数 | () => void |void |
82 | | onDragEnd | 拖拽结束时的回调函数| () => void |void |
83 |
84 | ** Pane **
85 |
86 | | Property | Description | Type | Default |
87 | | ------------------ | ---------------- | :--------: | ------------------ |
88 | | className | 面板 className | string | void |
89 | | minSize | 限制面板的最小尺寸 | string \| number | void |
90 | | maxSize | 限制面板的最大尺寸 | string\|number | void |
91 |
92 | ## themes
93 |
94 | 可以通过sashRender参数,定制需要的主题:
95 |
96 | - 默认的主题是参考的vscode主题
97 | - 同时也内置了一套sublime风格的主题
98 | - 自定义 [主题demos](https://codesandbox.io/s/split-pane-themes-xmsqtt).
99 |
100 |
101 | ## License
102 |
103 | **[split-pane-react](https://github.com/yyllff/split-pane-react)** 使用 [MIT](LICENSE)许可.
104 |
105 | > PS: 如果您有任何使用问题,可以提一个issue,我将尽快修复。这个组件会长期维护,同样的,如果你有什么好的想法,也欢迎提merge。如果这个组件对你有帮助,请留下你的Star,以便于需要它的开发者更容易搜索到它。
106 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "split-pane-react",
3 | "version": "0.1.3",
4 | "description": "Resizable split panes for React.js.",
5 | "module": "./esm/index.js",
6 | "typings": "./esm/index.d.ts",
7 | "files": [
8 | "esm",
9 | "src"
10 | ],
11 | "scripts": {
12 | "dev": "start-storybook -p 6006",
13 | "build-storybook": "build-storybook",
14 | "build": "gulp --gulpfile ./build/gulpfile.js"
15 | },
16 | "keywords": [
17 | "react",
18 | "multipane",
19 | "split pane",
20 | "ui",
21 | "resize",
22 | "resizeable",
23 | "layout",
24 | "flexbox",
25 | "components"
26 | ],
27 | "author": "yyllff",
28 | "license": "MIT",
29 | "devDependencies": {
30 | "@babel/core": "^7.18.0",
31 | "@storybook/addon-actions": "^6.5.3",
32 | "@storybook/addon-essentials": "^6.5.3",
33 | "@storybook/addon-interactions": "^6.5.2",
34 | "@storybook/addon-links": "^6.5.3",
35 | "@storybook/builder-vite": "^0.1.35",
36 | "@storybook/react": "^6.5.3",
37 | "@storybook/testing-library": "^0.0.11",
38 | "@vitejs/plugin-react": "^1.3.0",
39 | "babel-loader": "^8.2.5",
40 | "gulp": "^4.0.2",
41 | "gulp-sass": "^5.1.0",
42 | "gulp-typescript": "^6.0.0-alpha.1",
43 | "rimraf": "^3.0.2",
44 | "sass": "^1.52.0",
45 | "typescript": "^4.6.3",
46 | "vite": "^2.9.9"
47 | },
48 | "repository": "https://github.com/yyllff/split-pane-react",
49 | "publishConfig": {
50 | "registry": "https://registry.npmjs.org/"
51 | },
52 | "peerDependencies": {
53 | "react": "^16.8 || ^17.0 || ^18.0",
54 | "react-dom": "^16.8 || ^17.0 || ^18.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/SashContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { classNames } from './base';
3 | import { ISashContentProps } from './types';
4 |
5 | function SashContent ({
6 | className,
7 | children,
8 | active,
9 | type,
10 | ...others
11 | }: ISashContentProps) {
12 | return (
13 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export default SashContent;
28 |
--------------------------------------------------------------------------------
/src/SplitPane.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useCallback, useRef, useState } from 'react';
2 | import Pane from './pane';
3 | import Sash from './sash';
4 | import SashContent from './SashContent';
5 | import {
6 | classNames,
7 | bodyDisableUserSelect,
8 | paneClassName,
9 | splitClassName,
10 | splitDragClassName,
11 | splitVerticalClassName,
12 | splitHorizontalClassName,
13 | sashDisabledClassName,
14 | sashHorizontalClassName,
15 | sashVerticalClassName,
16 | assertsSize
17 | } from './base';
18 | import { IAxis, ISplitProps, IPaneConfigs, ICacheSizes } from './types';
19 |
20 | const SplitPane = ({
21 | children,
22 | sizes: propSizes,
23 | allowResize = true,
24 | split = 'vertical',
25 | className: wrapClassName,
26 | sashRender = (_, active) => ,
27 | resizerSize = 4,
28 | performanceMode = false,
29 | onChange = () => null,
30 | onDragStart = () => null,
31 | onDragEnd = () => null,
32 | ...others
33 | }: ISplitProps) => {
34 | const axis = useRef({ x: 0, y: 0 });
35 | const wrapper = useRef(null);
36 | const cacheSizes = useRef({ sizes: [], sashPosSizes: [] });
37 | const [wrapperRect, setWrapperRect] = useState({});
38 | const [isDragging, setDragging] = useState(false);
39 |
40 | useEffect(() => {
41 | const resizeObserver = new ResizeObserver(() => {
42 | setWrapperRect(wrapper?.current?.getBoundingClientRect() ?? {});
43 | });
44 | resizeObserver.observe(wrapper.current!);
45 | return () => {
46 | resizeObserver.disconnect();
47 | };
48 | }, []);
49 |
50 | const {
51 | sizeName,
52 | splitPos,
53 | splitAxis
54 | } = useMemo(() => ({
55 | sizeName: split === 'vertical' ? 'width' : 'height',
56 | splitPos: split === 'vertical' ? 'left' : 'top',
57 | splitAxis: split === 'vertical' ? 'x' : 'y'
58 | }), [split]);
59 |
60 | const wrapSize: number = wrapperRect[sizeName] ?? 0;
61 |
62 | // Get limit sizes via children
63 | const paneLimitSizes = useMemo(() => children.map(childNode => {
64 | const limits = [0, Infinity];
65 | if (childNode.type === Pane) {
66 | const { minSize, maxSize } = childNode.props as IPaneConfigs;
67 | limits[0] = assertsSize(minSize, wrapSize, 0);
68 | limits[1] = assertsSize(maxSize, wrapSize);
69 | }
70 | return limits;
71 | }), [children, wrapSize]);
72 |
73 | const sizes = useMemo(function () {
74 | let count = 0;
75 | let curSum = 0;
76 | const res = children.map((_, index) => {
77 | const size = assertsSize(propSizes[index], wrapSize);
78 | size === Infinity ? count++ : curSum += size;
79 | return size;
80 | });
81 |
82 | // resize or illegal size input,recalculate pane sizes
83 | if (curSum > wrapSize || !count && curSum < wrapSize) {
84 | const cacheNum = (curSum - wrapSize) / curSum;
85 | return res.map(size => {
86 | return size === Infinity ? 0 : size - size * cacheNum;
87 | });
88 | }
89 |
90 | if (count > 0) {
91 | const average = (wrapSize - curSum) / count;
92 | return res.map(size => {
93 | return size === Infinity ? average : size;
94 | });
95 | }
96 |
97 | return res;
98 | }, [...propSizes, children.length, wrapSize]);
99 |
100 | const sashPosSizes = useMemo(() => (
101 | sizes.reduce((a, b) => [...a, a[a.length - 1] + b], [0])
102 | ), [...sizes]);
103 |
104 | const dragStart = useCallback(function (e) {
105 | document?.body?.classList?.add(bodyDisableUserSelect);
106 | axis.current = { x: e.pageX, y: e.pageY };
107 | cacheSizes.current = { sizes, sashPosSizes };
108 | setDragging(true);
109 | onDragStart(e);
110 | }, [onDragStart, sizes, sashPosSizes]);
111 |
112 | const dragEnd = useCallback(function (e) {
113 | document?.body?.classList?.remove(bodyDisableUserSelect);
114 | axis.current = { x: e.pageX, y: e.pageY };
115 | cacheSizes.current = { sizes, sashPosSizes };
116 | setDragging(false);
117 | onDragEnd(e);
118 | }, [onDragEnd, sizes, sashPosSizes]);
119 |
120 | const onDragging = useCallback(function (e, i) {
121 | const curAxis = { x: e.pageX, y: e.pageY };
122 | let distanceX = curAxis[splitAxis] - axis.current[splitAxis];
123 |
124 | const leftBorder = -Math.min(
125 | sizes[i] - paneLimitSizes[i][0],
126 | paneLimitSizes[i + 1][1] - sizes[i + 1]
127 | );
128 | const rightBorder = Math.min(
129 | sizes[i + 1] - paneLimitSizes[i + 1][0],
130 | paneLimitSizes[i][1] - sizes[i]
131 | );
132 |
133 | if (distanceX < leftBorder) {
134 | distanceX = leftBorder;
135 | }
136 | if (distanceX > rightBorder) {
137 | distanceX = rightBorder;
138 | }
139 |
140 | const nextSizes = [...sizes];
141 | nextSizes[i] += distanceX;
142 | nextSizes[i + 1] -= distanceX;
143 |
144 | onChange(nextSizes);
145 | }, [paneLimitSizes, onChange]);
146 |
147 | const paneFollow = !(performanceMode && isDragging);
148 | const paneSizes = paneFollow ? sizes : cacheSizes.current.sizes;
149 | const panePoses = paneFollow ? sashPosSizes: cacheSizes.current.sashPosSizes;
150 |
151 | return (
152 |
163 | {children.map((childNode, childIndex) => {
164 | const isPane = childNode.type === Pane;
165 | const paneProps = isPane ? childNode.props : {};
166 |
167 | return (
168 |
177 | {isPane ? paneProps.children : childNode}
178 |
179 | );
180 | })}
181 | {sashPosSizes.slice(1, -1).map((posSize, index) => (
182 |
onDragging(e, index)}
197 | onDragEnd={dragEnd}
198 | />
199 | ))}
200 |
201 | );
202 | };
203 |
204 | export default SplitPane;
205 |
--------------------------------------------------------------------------------
/src/base.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Element names may consist of Latin letters, digits, dashes and underscores.
3 | * CSS class is formed as block name plus two underscores plus element name: .block__elem
4 | * @param block
5 | * @param element
6 | */
7 | function getBEMElement(block: string, element: string) {
8 | return `${block}__${element}`;
9 | }
10 |
11 | /**
12 | * CSS class is formed as block’s or element’s name plus two dashes:
13 | * .block--mod or .block__elem--mod and .block--color-black with .block--color-red.
14 | * Spaces in complicated modifiers are replaced by dash.
15 | * @param blockOrElement
16 | * @param modifier
17 | */
18 | function getBEMModifier(blockOrElement: string, modifier: string) {
19 | return `${blockOrElement}--${modifier}`;
20 | }
21 |
22 | export const splitClassName = 'react-split';
23 | export const splitDragClassName = getBEMModifier(splitClassName, 'dragging');
24 | export const splitVerticalClassName = getBEMModifier(splitClassName, 'vertical');
25 | export const splitHorizontalClassName = getBEMModifier(splitClassName, 'horizontal');
26 |
27 | export const bodyDisableUserSelect = getBEMModifier(splitClassName, 'disabled');
28 | export const paneClassName = getBEMElement(splitClassName, 'pane');
29 | export const sashClassName = getBEMElement(splitClassName, 'sash');
30 |
31 | export const sashVerticalClassName = getBEMModifier(
32 | sashClassName,
33 | 'vertical'
34 | );
35 | export const sashHorizontalClassName = getBEMModifier(
36 | sashClassName,
37 | 'horizontal'
38 | );
39 | export const sashDisabledClassName = getBEMModifier(
40 | sashClassName,
41 | 'disabled'
42 | );
43 | export const sashHoverClassName = getBEMModifier(sashClassName, 'hover');
44 |
45 | export function classNames(...args) {
46 | const classList: string[] = [];
47 | for (const arg of args) {
48 | if (!arg) continue;
49 | const argType = typeof arg;
50 | if (argType === 'string' || argType === 'number') {
51 | classList.push(`${arg}`);
52 | continue;
53 | }
54 | if (argType === 'object') {
55 | if (arg.toString !== Object.prototype.toString) {
56 | classList.push(arg.toString());
57 | continue;
58 | }
59 | for (const key in arg) {
60 | if (Object.hasOwnProperty.call(arg, key) && arg[key]) {
61 | classList.push(key);
62 | }
63 | }
64 | }
65 | }
66 | return classList.join(' ');
67 | }
68 |
69 | /**
70 | * Convert size to absolute number or Infinity
71 | * SplitPane allows sizes in string and number, but the state sizes only support number,
72 | * so convert string and number to number in here
73 | * 'auto' -> divide the remaining space equally
74 | * 'xxxpx' -> xxx
75 | * 'xxx%' -> wrapper.size * xxx/100
76 | * xxx -> xxx
77 | */
78 | export function assertsSize (
79 | size: string | number | undefined,
80 | sum: number,
81 | defaultValue = Infinity
82 | ) {
83 | if (typeof size === 'undefined') return defaultValue;
84 | if (typeof size === 'number') return size;
85 | if (size.endsWith('%')) return sum * (+size.replace('%', '') / 100);
86 | if (size.endsWith('px')) return +size.replace('px', '');
87 | return defaultValue;
88 | }
89 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SplitPane';
2 | export * from './pane';
3 | export * from './SashContent';
4 | export { default } from './SplitPane';
5 | export { default as Pane } from './pane';
6 | export { default as SashContent } from './SashContent';
7 |
--------------------------------------------------------------------------------
/src/pane.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react';
2 | import { HTMLElementProps, IPaneConfigs } from './types';
3 |
4 | export default function Pane({
5 | children,
6 | style,
7 | className,
8 | role,
9 | title
10 | }: PropsWithChildren) {
11 | return (
12 |
18 | {children}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/sash.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { classNames, sashClassName } from './base';
3 | import { ISashProps } from './types';
4 |
5 | export default function Sash({
6 | className,
7 | render,
8 | onDragStart,
9 | onDragging,
10 | onDragEnd,
11 | ...others
12 | }: ISashProps) {
13 | const timeout = useRef(null);
14 | const [active, setActive] = useState(false);
15 | const [draging, setDrag] = useState(false);
16 |
17 | const handleMouseMove = function (e) {
18 | onDragging(e);
19 | };
20 |
21 | const handleMouseUp = function (e) {
22 | setDrag(false);
23 | onDragEnd(e);
24 | window.removeEventListener('mousemove', handleMouseMove);
25 | window.removeEventListener('mouseup', handleMouseUp);
26 | };
27 |
28 | return (
29 | {
36 | timeout.current = setTimeout(() => {
37 | setActive(true);
38 | }, 150);
39 | }}
40 | onMouseLeave={() => {
41 | if (timeout.current) {
42 | setActive(false);
43 | clearTimeout(timeout.current);
44 | }
45 | }}
46 | onMouseDown={e => {
47 | setDrag(true);
48 | onDragStart(e);
49 |
50 | window.addEventListener('mousemove', handleMouseMove);
51 | window.addEventListener('mouseup', handleMouseUp);
52 | }}
53 | {...others}
54 | >
55 | {render(draging || active)}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/themes/default.scss:
--------------------------------------------------------------------------------
1 | .react-split {
2 | flex: 1;
3 | height: 100%;
4 | position: relative;
5 | width: 100%;
6 |
7 | &__pane {
8 | height: 100%;
9 | position: absolute;
10 | white-space: normal;
11 | width: 100%;
12 | overflow: hidden;
13 | }
14 |
15 | &__sash {
16 | height: 100%;
17 | position: absolute;
18 | top: 0;
19 | transition: background-color 0.1s;
20 | width: 100%;
21 | z-index: 2;
22 |
23 | &--disabled {
24 | pointer-events: none;
25 | }
26 |
27 | &--vertical {
28 | cursor: col-resize;
29 | }
30 |
31 | &--horizontal {
32 | cursor: row-resize;
33 | }
34 |
35 | &-content {
36 | width: 100%;
37 | height: 100%;
38 |
39 | &--active {
40 | background-color: #175ede;
41 | }
42 | }
43 | }
44 |
45 | &--dragging {
46 | &.react-split--vertical {
47 | cursor: col-resize;
48 | }
49 | &.react-split--horizontal {
50 | cursor: row-resize;
51 | }
52 | }
53 | }
54 |
55 | body.react-split--disabled {
56 | user-select: none;
57 | }
58 |
59 | .split-sash-content {
60 | width: 100%;
61 | height: 100%;
62 |
63 | &.split-sash-content-vscode {
64 | &.split-sash-content-active {
65 | background-color: #175ede;
66 | }
67 | }
68 |
69 | &.split-sash-content-sublime {
70 |
71 | }
72 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface HTMLElementProps {
4 | title?: string;
5 | style?: React.CSSProperties;
6 | className?: string;
7 | role?: string;
8 | }
9 |
10 | export interface IAxis {
11 | x: number;
12 | y: number;
13 | }
14 |
15 | export interface ICacheSizes {
16 | sizes: (string | number)[];
17 | sashPosSizes: (string | number)[];
18 | }
19 |
20 | export interface ISplitProps extends HTMLElementProps {
21 | children: JSX.Element[];
22 | /**
23 | * Should allowed to resized
24 | *
25 | * default is true
26 | */
27 | allowResize?: boolean;
28 | /**
29 | * How to split the space
30 | *
31 | * default is vertical
32 | */
33 | split?: 'vertical' | 'horizontal';
34 | /**
35 | * Only support controlled mode, so it's required
36 | */
37 | sizes: (string | number)[];
38 | sashRender: (index: number, active: boolean) => React.ReactNode;
39 | onChange: (sizes: number[]) => void;
40 | onDragStart?: (e: MouseEvent) => void;
41 | onDragEnd?: (e: MouseEvent) => void;
42 | className?: string;
43 | sashClassName?: string;
44 | performanceMode?: boolean;
45 | /**
46 | * Specify the size fo resizer
47 | *
48 | * defualt size is 4px
49 | */
50 | resizerSize?: number;
51 | }
52 |
53 | export interface ISashProps {
54 | className?: string;
55 | style: React.CSSProperties;
56 | render: (active: boolean) => void;
57 | onDragStart: React.MouseEventHandler;
58 | onDragging: React.MouseEventHandler;
59 | onDragEnd: React.MouseEventHandler;
60 | }
61 |
62 | export interface ISashContentProps {
63 | className?: string;
64 | type?: string;
65 | active?: boolean;
66 | children?: JSX.Element[];
67 | }
68 |
69 | export interface IPaneConfigs {
70 | maxSize?: number | string;
71 | minSize?: number | string;
72 | }
73 |
--------------------------------------------------------------------------------
/stories/01Vertical.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Basic',
8 | };
9 |
10 | export const BasicVertical = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>([
12 | 100,
13 | 200,
14 | 'auto',
15 | ]);
16 |
17 | const layoutCSS = {
18 | height: '100%',
19 | display: 'flex',
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | };
23 |
24 | return (
25 |
26 |
Split used to drag and drop to modify panel size
27 |
setSizes(sizes)}
30 | >
31 |
32 | pane1
33 |
34 |
35 | pane2
36 |
37 |
38 | pane2
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/stories/02Horizontal.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Basic',
8 | };
9 |
10 | export const BasicHorizontal = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>([
12 | 100,
13 | 200,
14 | 'auto',
15 | ]);
16 |
17 | const layoutCSS = {
18 | height: '100%',
19 | display: 'flex',
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | };
23 |
24 | return (
25 |
26 |
Set size ='horizontal ', switch to horizontal panel
27 |
setSizes(sizes)}
31 | >
32 |
33 | pane1
34 |
35 |
36 | pane2
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/stories/03Complex.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SplitPane from '../src';
3 | import '../src/themes/default.scss';
4 |
5 | export default {
6 | title: 'Basic',
7 | };
8 |
9 | export const ComplexLayout = () => {
10 | const [sizes, setSizes] = useState<(number | string)[]>([250, 'auto']);
11 | const [sizes1, setSizes1] = useState<(number | string)[]>([400, 'auto']);
12 | const [sizes2, setSizes2] = useState<(number | string)[]>([500, 'auto']);
13 |
14 | const layoutCSS = {
15 | height: '100%',
16 | display: 'flex',
17 | alignItems: 'center',
18 | justifyContent: 'center',
19 | };
20 |
21 | return (
22 |
23 |
Split supports complex layouts
24 |
29 |
30 |
31 | Top Pane1
32 |
33 |
34 | Top Pane2
35 |
36 |
37 |
38 |
39 | Bottom Pane1
40 |
41 |
42 | Bottom Pane2
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/stories/04Percentage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Basic',
8 | };
9 |
10 | export const PercentageSize = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>([
12 | '20%',
13 | '30%',
14 | 'auto',
15 | ]);
16 |
17 | const layoutCSS = {
18 | height: '100%',
19 | display: 'flex',
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | };
23 |
24 | return (
25 |
26 |
Size value support percentage
27 |
setSizes(sizes)}
30 | >
31 |
32 | Pane1
33 |
34 |
35 | Pane2
36 |
37 |
38 | Pane3
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/stories/05LimitSize.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Advanced',
8 | };
9 |
10 | export const MinSizeAndMaxSize = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>(['30%', 'auto']);
12 |
13 | const layoutCSS = {
14 | height: '100%',
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | justifyContent: 'center',
19 | };
20 |
21 | return (
22 |
23 |
Set the minimum and maximum values of pane1 through the pane component
24 |
25 |
26 |
27 |
Pane1
28 |
minSize: 100px
29 |
maxSize: 50%
30 |
31 |
32 |
33 | Pane2
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/stories/06AllowResize.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Advanced',
8 | };
9 |
10 | export const AllowResize = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>(['20%', 'auto']);
12 | const [allowResize, setAllowResize] = useState(true);
13 |
14 | const layoutCSS = {
15 | height: '100%',
16 | display: 'flex',
17 | flexDirection: 'column',
18 | alignItems: 'center',
19 | justifyContent: 'center',
20 | };
21 |
22 | return (
23 |
24 |
Enable and disable resize
25 |
26 |
27 |
28 |
29 |
34 |
35 | Pane1
36 |
37 |
38 | Pane2
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/stories/07ResizeStyle.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Advanced',
8 | };
9 |
10 | export const FluidPanes = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>(['20%', 'auto']);
12 | const [sizes1, setSizes1] = useState<(number | string)[]>(['50%', 'auto']);
13 |
14 | const layoutCSS = {
15 | height: '100%',
16 | display: 'flex',
17 | flexDirection: 'column',
18 | alignItems: 'center',
19 | justifyContent: 'center',
20 | };
21 |
22 | return (
23 |
24 |
Support fluid pane
25 |
26 |
27 |
28 | Pane1
29 |
30 | Try sliding the right axis
31 |
32 |
33 |
34 |
35 |
36 | Pane2
37 |
38 |
39 | Pane3
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/stories/08CustomSash.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane, SashContent } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Advanced',
8 | };
9 |
10 | export const CustomSash = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>([200, 200, 'auto']);
12 |
13 | const layoutCSS = {
14 | height: '100%',
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | justifyContent: 'center',
19 | };
20 |
21 | return (
22 |
23 |
Here are three different theme styles
24 |
(
29 |
30 | )}
31 | >
32 |
33 |
34 | pane1
35 |
36 |
37 |
38 |
39 | pane2
40 |
41 |
42 |
43 |
44 | pane3
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/stories/09PerformanceMode.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from '@storybook/react/demo';
3 | import SplitPane, { Pane } from '../src';
4 | import '../src/themes/default.scss';
5 |
6 | export default {
7 | title: 'Advanced',
8 | };
9 |
10 | export const PerformacnceMode = () => {
11 | const [sizes, setSizes] = useState<(number | string)[]>(['30%', 'auto']);
12 |
13 | const layoutCSS = {
14 | height: '100%',
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | justifyContent: 'center',
19 | };
20 |
21 | return (
22 |
23 |
High performance mode can be enabled through performanceMode
24 |
29 |
30 |
31 |
Pane1
32 |
minSize: 100px
33 |
maxSize: 50%
34 |
35 |
36 |
37 | Pane2
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "lib": ["es5", "es6", "es7", "es2017", "dom", "ESNext"],
6 | "sourceMap": true,
7 | "allowJs": true,
8 | "jsx": "react",
9 | "moduleResolution": "node",
10 | "baseUrl": "./",
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitReturns": true,
14 | "noImplicitThis": true,
15 | "noImplicitAny": false,
16 | "strictNullChecks": true,
17 | "skipLibCheck": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": false,
21 | "declaration": true,
22 | "allowSyntheticDefaultImports": true,
23 | "experimentalDecorators": true,
24 | "emitDecoratorMetadata": true,
25 | "resolveJsonModule": true,
26 | "downlevelIteration": true,
27 | "typeRoots": ["node", "node_modules/@types", "src/typings"],
28 | "outDir": "esm",
29 | "module": "es6",
30 | "target": "es6",
31 | "declaration": true,
32 | "preserveConstEnums": true,
33 | "sourceMap": false,
34 | "rootDirs": ["src"]
35 | },
36 | "include": ["src/*"],
37 | "exclude": ["node_modules", "src/__tests__/*"]
38 | }
39 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | publicDir: '/gitPages/'
8 | })
--------------------------------------------------------------------------------