├── .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 | }) --------------------------------------------------------------------------------