├── .github └── workflows │ └── dependabot-auto-merge.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── demo ├── index.html ├── index.tsx ├── sections │ ├── collapsible-section.tsx │ ├── default-size.tsx │ ├── fixed-size.tsx │ ├── index.ts │ ├── interactive-section.tsx │ ├── max-min-size.tsx │ ├── nesting.tsx │ ├── responsive-size.tsx │ └── simple.tsx └── style.less ├── package.json ├── src ├── Bar │ ├── Bar.styled.tsx │ ├── disablePassive.ts │ └── index.tsx ├── Container │ ├── Container.styled.tsx │ ├── Resizer.ts │ ├── index.tsx │ ├── operators.ts │ └── utils.ts ├── Section │ ├── Section.styled.tsx │ └── index.tsx ├── context.tsx ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto merge 2 | 3 | on: 4 | check_suite: 5 | types: 6 | - completed 7 | pull_request: 8 | types: 9 | - labeled 10 | - unlabeled 11 | - synchronize 12 | - opened 13 | - edited 14 | - ready_for_review 15 | - reopened 16 | - unlocked 17 | 18 | jobs: 19 | auto-merge: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: auto-merge 23 | uses: ridedott/dependabot-auto-merge-action@master 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea 4 | .cache 5 | *.tgz 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | /src 4 | /demo 5 | .prettierrc 6 | tsconfig.json 7 | tsconfig.build.json 8 | tslint.json 9 | yarn.lock 10 | *.tgz 11 | .cache 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "jsxBracketSameLine": false, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LeetCode Open Source 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 | # react-simple-resizer · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/LeetCode-OpenSource/react-simple-resizer/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat)](https://github.com/prettier/prettier) [![npm version](https://img.shields.io/npm/v/react-simple-resizer.svg?style=flat)](https://www.npmjs.com/package/react-simple-resizer) 2 | 3 | 4 | An intuitive React component set for multi-column(row) resizing. You could [customize the behavior of resizing](#customize-resize-behavior) in a very simple way. Works in every modern browser [which](https://caniuse.com/#feat=flexbox) supports [flexible box layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout). 5 | 6 | #### Table of Contents 7 | - [Installation](#installation) 8 | - [Examples](#examples) 9 | - [Components](#components) 10 | - [Container](#container-) 11 | - [Section](#section-) 12 | - [Bar](#bar-) 13 | - [Customize resize behavior](#customize-resize-behavior) 14 | - [Contributing](#contributing) 15 | 16 | ## Installation 17 | Using [yarn](https://yarnpkg.com/): 18 | ```bash 19 | yarn add react-simple-resizer 20 | ``` 21 | 22 | Or via [npm](https://docs.npmjs.com): 23 | ```bash 24 | npm install react-simple-resizer 25 | ``` 26 | 27 | ## Examples 28 | 29 | Here is a minimal example for two-column layout: 30 | ```jsx 31 | import React from 'react'; 32 | import { Container, Section, Bar } from 'react-simple-resizer'; 33 | 34 | export default () => ( 35 | 36 |
37 | 38 |
39 | 40 | ); 41 | ``` 42 | 43 | We have created several demos on CodeSandbox, check demos below: 44 | 45 | - [Simple demo](https://codesandbox.io/s/qkw1rxxq29) 46 | - [Make Section collapsible](https://codesandbox.io/s/1vpy7kz5j3) 47 | - [Multiple Section linkage effects](https://codesandbox.io/s/r51pv3qzpm) 48 | 49 | ## Components 50 | 51 | ### \ 52 | 53 | Literally, it's the container of the other components. 54 | 55 | ```typescript 56 | import { Container } from 'react-simple-resizer'; 57 | ``` 58 | 59 | #### Props 60 | ```typescript 61 | import { HTMLAttributes } from 'react'; 62 | 63 | interface ContainerProps extends HTMLAttributes { 64 | vertical?: boolean; 65 | onActivate?: () => void; 66 | beforeApplyResizer?: (resizer: Resizer) => void; 67 | afterResizing?: () => void; 68 | } 69 | ``` 70 | 71 | ##### `vertical` 72 | 73 | Determine whether using vertical layout or not, default is `false`. 74 | 75 | ##### `onActivate` 76 | 77 | Triggered when any [`Bar`](#bar-) is activated. i.e, [onMouseDown](https://developer.mozilla.org/en/docs/Web/Events/mousedown) or [onTouchStart](https://developer.mozilla.org/en-US/docs/Web/Events/touchstart). 78 | 79 | ##### `beforeApplyResizer` 80 | 81 | Used to [customize resize behavior](#customize-resize-behavior). In this method, you __don't__ need to call [`applyResizer`](#applyresizer) to apply the resize result. Please note that you should not do any side effect on this method. If you want to do something after resizing, see [`afterResizing`](#afterresizing) below. 82 | 83 | ##### `afterResizing` 84 | 85 | Triggered after a __resizing section__ is completed, which means that it will be triggered after [onMouseUp](https://developer.mozilla.org/en-US/docs/Web/Events/mouseup) and [onTouchEnd](https://developer.mozilla.org/en-US/docs/Web/Events/touchend) events. If you want to do something after size of section has changed, use [`onSizeChanged`](#onsizechanged) props on the [`Section`](#section-) instead. 86 | 87 | #### Instance properties 88 | ```typescript 89 | import React from 'react'; 90 | 91 | class Container extends React.PureComponent { 92 | public getResizer(): Resizer 93 | public applyResizer(resizer: Resizer): void 94 | } 95 | ``` 96 | ##### `getResizer` 97 | Used to get newest [`Resizer`](#customize-resize-behavior). But any change won't apply unless you pass the `Resizer` to `applyResizer`. 98 | 99 | ##### `applyResizer` 100 | Apply the passed `Resizer` to `Container`. 101 | 102 | --- 103 | ### \
104 | 105 | ```typescript 106 | import { Section } from 'react-simple-resizer'; 107 | ``` 108 | 109 | #### Props 110 | ```typescript 111 | import { HTMLAttributes, RefObject } from 'react'; 112 | 113 | interface SectionProps extends HTMLAttributes { 114 | size?: number; 115 | defaultSize?: number; 116 | maxSize?: number; 117 | minSize?: number; 118 | disableResponsive?: boolean; 119 | onSizeChanged?: (currentSize: number) => void; 120 | innerRef?: RefObject; 121 | } 122 | ``` 123 | ##### `size` 124 | Used to set `Section`'s size. If `size` is set, `Section` will ignore the value of `defaultSize`, `maxSize` and `minSize`. 125 | 126 | ##### `defaultSize` 127 | Used to set default size of `Section`. 128 | 129 | ##### `maxSize` 130 | Used to set max size of `Section`. 131 | 132 | ##### `minSize` 133 | Used to set min size of `Section`. 134 | 135 | ##### `disableResponsive` 136 | Each `Section` is responsive as default, unless `size` exists. The `responsive` here means that `Section`'s size is related with `Container`'s size, if `Container`'s size turn smaller, the `Section`'s size also turn smaller automatically. Otherwise, changes of `Container` size won't affect the `Section` when `disableResponsive` is `true`. 137 | 138 | ##### `onSizeChanged` 139 | Will be triggered each time its size has changed. 140 | 141 | ##### `innerRef` 142 | Used to get the actual DOM ref of `Section`. 143 | 144 | --- 145 | ### \ 146 | 147 | ```typescript 148 | import { Bar } from 'react-simple-resizer'; 149 | ``` 150 | 151 | #### Props 152 | ```typescript 153 | import { HTMLAttributes, RefObject } from 'react'; 154 | 155 | interface ExpandInteractiveArea { 156 | top?: number; 157 | left?: number; 158 | right?: number; 159 | bottom?: number; 160 | } 161 | 162 | interface BarProps extends HTMLAttributes { 163 | size: number; 164 | expandInteractiveArea?: ExpandInteractiveArea; 165 | onStatusChanged?: (isActive: boolean) => void; 166 | onClick?: () => void; 167 | innerRef?: RefObject; 168 | } 169 | ``` 170 | ##### `size` 171 | Required, used to set the size of `Bar`. 172 | 173 | ##### `expandInteractiveArea` 174 | Used to expand interactive area of `Bar`. 175 | 176 | ##### `onStatusChanged` 177 | Triggered when the state of `Bar` has changed. 178 | 179 | ##### `onClick` 180 | Triggered if there's no "move" events. The main difference between it and original `onClick` event is that __there is no parameters__ on _this_ `onClick`. You could also use it as a touch event on mobile platform, without 300ms click delay. 181 | 182 | ##### `innerRef` 183 | Used to get the actual DOM ref of `Bar`. 184 | 185 | ## Customize resize behavior 186 | If you want to customize behavior of resizing, then you have to know how to use `Resizer`. 187 | 188 | There is two ways to get the `Resizer`. One is [`beforeApplyResizer`](#beforeapplyresizer) defined on the __props__ of `Container`, and the other is [`getResizer`](#getresizer) defined on the __instance__ of `Container`. 189 | 190 | Beware that you need __manually__ calling [`applyResizer`](#applyresizer) every time you want to apply the effect, except in `beforeApplyResizer`. Check demo [Make Section collapsible](https://codesandbox.io/s/1vpy7kz5j3) to see how `applyResizer` is used. 191 | 192 | ```typescript 193 | interface Resizer { 194 | resizeSection: (indexOfSection: number, config: { toSize: number; preferMoveLeftBar?: boolean }) => void; 195 | moveBar: (indexOfBar: number, config: { withOffset: number; }) => void; 196 | discard: () => void; 197 | isSectionResized: (indexOfSection: number) => boolean; 198 | isBarActivated: (indexOfBar: number) => boolean; 199 | getSectionSize: (indexOfSection: number) => number | -1; 200 | getTotalSize: () => number; 201 | } 202 | ``` 203 | 204 | ##### `resizeSection` 205 | Used to set size of the nth `Section`. 206 | In multi-column layout, there're several `Bar`s could change the `Section`'s size. Therefore, you could try to use the left side `Bar` to resizing by setting `preferMoveLeftBar`. 207 | 208 | ##### `moveBar` 209 | Used to move the nth `Bar` to resizing. 210 | If the value of A is negative, move `Bar` to the left. When [`vertical`](#vertical) is `true`, move up. 211 | 212 | ##### `discard` 213 | Discard resizing at this time. 214 | 215 | ##### `isSectionResized` 216 | Used to determine whether the nth `Section`'s size is changed at current [resizing section](#user-content-resizing-section) or not. 217 | 218 | ##### `isBarActivated` 219 | Used to determine whether the nth `Bar` is active or not. 220 | 221 | ##### `getSectionSize` 222 | Used to get size of the nth `Section`. if there is no nth `Section`, return `-1`. 223 | 224 | ##### `getTotalSize` 225 | Used to get total size of all `Section`. 226 | 227 | 228 | 229 | ## Contributing 230 | The main purpose of this repository is to continue to evolve react-simple-resizer, making it faster, smaller and easier to use. We are grateful to the community for contributing bugfixes and improvements. 231 | 232 | #### About Demo 233 | Feel free to let us know that you have created some new customized resize behavior. You could create a PR to let more people see your works. Also, if you find some behaviors that you cannot create, let us know too. 234 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Resizer Demo 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | Simple, 6 | DefaultSize, 7 | ResponsiveSize, 8 | MaxMinSize, 9 | FixedSize, 10 | NestingDemo, 11 | CollapsibleSection, 12 | InteractiveSection, 13 | } from './sections'; 14 | 15 | ReactDOM.render( 16 |
17 |

Default Behavior

18 | 19 | 20 | 21 | 22 | 23 | 24 |

Custom Behavior

25 | 26 | 27 |
, 28 | document.getElementById('app'), 29 | ); 30 | -------------------------------------------------------------------------------- /demo/sections/collapsible-section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container, Section, Bar, Resizer } from '../../src'; 4 | 5 | function beforeApplyResizer(resizer: Resizer): void { 6 | if (resizer.getSectionSize(0) < 150) { 7 | resizer.resizeSection(0, { toSize: 0 }); 8 | } else if (resizer.getSectionSize(0) < 300) { 9 | resizer.resizeSection(0, { toSize: 300 }); 10 | } 11 | } 12 | 13 | export class CollapsibleSection extends React.PureComponent { 14 | readonly containerRef = React.createRef(); 15 | 16 | render() { 17 | return ( 18 |
19 |

Collapsible section demo

20 | 25 |
26 | 27 |
28 | 29 |
30 | ); 31 | } 32 | 33 | private onBarClick = () => { 34 | const container = this.containerRef.current; 35 | 36 | if (container) { 37 | const resizer = container.getResizer(); 38 | 39 | if (resizer.getSectionSize(0) === 0) { 40 | resizer.resizeSection(0, { toSize: 300 }); 41 | } 42 | 43 | container.applyResizer(resizer); 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /demo/sections/default-size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const DefaultSize = () => ( 6 |
7 |

default size demo

8 | 9 | 10 | default is 400px. 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /demo/sections/fixed-size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const FixedSize = () => ( 6 |
7 |

Fixed size demo

8 | 9 | 10 | Fixed size. 11 |
12 | (default is not responsive) 13 |
14 | 15 | 16 | 17 | 18 | max size is 500px. 19 | 20 |
21 | 22 |

Responsive fixed size demo

23 | 24 | 25 | Fixed size. 26 |
27 | (try to resize the browser to see the difference) 28 |
29 | 30 | 31 | 32 | 33 | max size is 500px. 34 | 35 |
36 |
37 | ); 38 | -------------------------------------------------------------------------------- /demo/sections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './simple'; 2 | export * from './default-size'; 3 | export * from './responsive-size'; 4 | export * from './max-min-size'; 5 | export * from './fixed-size'; 6 | export * from './nesting'; 7 | export * from './collapsible-section'; 8 | export * from './interactive-section'; 9 | -------------------------------------------------------------------------------- /demo/sections/interactive-section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container, Section, Bar, Resizer } from '../../src'; 4 | 5 | function onResizing(resizer: Resizer): void { 6 | if (resizer.isBarActivated(0)) { 7 | resizer.resizeSection(2, { toSize: resizer.getSectionSize(0) }); 8 | } else { 9 | resizer.resizeSection(0, { toSize: resizer.getSectionSize(2) }); 10 | } 11 | 12 | if (resizer.getSectionSize(1) < 300) { 13 | const remainingSize = resizer.getTotalSize() - 300; 14 | resizer.resizeSection(0, { toSize: remainingSize / 2 }); 15 | resizer.resizeSection(1, { toSize: 300 }); 16 | resizer.resizeSection(2, { toSize: remainingSize / 2 }); 17 | } 18 | } 19 | 20 | export const InteractiveSection = () => ( 21 |
22 |

Interactive section demo

23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 | ); 32 | -------------------------------------------------------------------------------- /demo/sections/max-min-size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const MaxMinSize = () => ( 6 |
7 |

Max/Min size demo

8 | 9 | 10 | 150px min size. 11 | 12 | 13 | 14 | 600px max size. 15 | 16 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /demo/sections/nesting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const NestingDemo = () => ( 6 |
7 |

Nesting demo

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | -------------------------------------------------------------------------------- /demo/sections/responsive-size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const ResponsiveSize = () => ( 6 |
7 |

responsive size demo

8 | 9 | 10 | default is responsive. 11 | 12 | 13 | 14 | this one is not responsive. 15 |
16 | (resize browser to see the difference) 17 |
18 | 19 | 20 |
21 |
22 | ); 23 | -------------------------------------------------------------------------------- /demo/sections/simple.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as Resizer from '../../src'; 4 | 5 | export const Simple = () => ( 6 |
7 |

Simple demo

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /demo/style.less: -------------------------------------------------------------------------------- 1 | main { 2 | margin: 0 10vw; 3 | } 4 | 5 | section { 6 | margin: 50px 0; 7 | } 8 | 9 | .container { 10 | height: 50vh; 11 | font-size: 16px; 12 | font-family: sans-serif; 13 | text-align: center; 14 | white-space: nowrap; 15 | } 16 | 17 | .section { 18 | background: #d3d3d3; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | .container { 24 | flex: 1; 25 | margin: 0; 26 | } 27 | } 28 | 29 | .bar { 30 | background: #888888; 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-simple-resizer", 3 | "version": "2.1.0", 4 | "description": "An intuitive React component set for multi-column resizing", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "dev": "parcel ./demo/index.html", 9 | "build:demo": "rm -rf ./demo/dist/* && parcel build ./demo/index.html --out-dir ./demo/dist --public-url ./", 10 | "build": "rm -rf ./dist && tsc -p ./tsconfig.build.json", 11 | "prettier": "prettier '@(src|demo)/**/*.@(ts|tsx|html|less)' --write", 12 | "lint": "tslint -p tsconfig.json --fix", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/LeetCode-OpenSource/react-simple-resizer.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "resize", 22 | "resizer", 23 | "multi-column" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/LeetCode-OpenSource/react-simple-resizer/issues" 27 | }, 28 | "homepage": "https://github.com/LeetCode-OpenSource/react-simple-resizer#readme", 29 | "author": "LeetCode front-end team", 30 | "license": "MIT", 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "lint-staged" 34 | } 35 | }, 36 | "lint-staged": { 37 | "*.{ts,tsx}": [ 38 | "prettier --write", 39 | "tslint -p tsconfig.json --fix", 40 | "git add" 41 | ], 42 | "*.{less,html}": [ 43 | "prettier --write", 44 | "git add" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react": "^17.0.0", 49 | "@types/react-dom": "^17.0.0", 50 | "husky": "^4.0.0", 51 | "less": "^4.0.0", 52 | "lint-staged": "^10.0.1", 53 | "normalize.css": "^8.0.1", 54 | "parcel-bundler": "^1.10.3", 55 | "parcel-plugin-bundle-visualiser": "^1.2.0", 56 | "prettier": "2.2.1", 57 | "react": "^17.0.0", 58 | "react-dom": "^17.0.0", 59 | "tslib": "^2.0.0", 60 | "tslint": "^6.0.0", 61 | "tslint-eslint-rules": "^5.3.1", 62 | "tslint-react": "^5.0.0", 63 | "tslint-sonarts": "^1.7.0", 64 | "typescript": "^3.1.6" 65 | }, 66 | "dependencies": { 67 | "rxjs": "^6.3.3" 68 | }, 69 | "peerDependencies": { 70 | "react": "^16.3.0", 71 | "react-dom": "^17.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Bar/Bar.styled.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ExpandInteractiveArea } from '../types'; 4 | 5 | export type StyledBarProps = React.HTMLAttributes & { 6 | size?: number; 7 | }; 8 | 9 | export const StyledBar = React.forwardRef( 10 | ({ size, style, ...props }, ref) => ( 11 |
20 | ), 21 | ); 22 | 23 | export type StyledInteractiveAreaProps = React.HTMLAttributes & 24 | ExpandInteractiveArea & { 25 | vertical: boolean; 26 | }; 27 | 28 | export const StyledInteractiveArea = React.forwardRef< 29 | HTMLDivElement, 30 | StyledInteractiveAreaProps 31 | >( 32 | ( 33 | { top = 0, right = 0, bottom = 0, left = 0, vertical, style, ...props }, 34 | ref, 35 | ) => ( 36 |
51 | ), 52 | ); 53 | -------------------------------------------------------------------------------- /src/Bar/disablePassive.ts: -------------------------------------------------------------------------------- 1 | export let disablePassive: boolean | AddEventListenerOptions = true; 2 | 3 | try { 4 | // @ts-ignore 5 | window.addEventListener('test', null, { 6 | get passive() { 7 | disablePassive = { passive: false }; 8 | return true; 9 | }, 10 | }); 11 | } catch {} 12 | -------------------------------------------------------------------------------- /src/Bar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | BarActionType, 5 | ChildProps, 6 | Coordinate, 7 | ExpandInteractiveArea, 8 | } from '../types'; 9 | import { omit } from '../utils'; 10 | import { withResizerContext } from '../context'; 11 | import { StyledBar, StyledInteractiveArea, StyledBarProps } from './Bar.styled'; 12 | import { disablePassive } from './disablePassive'; 13 | 14 | type Props = React.HTMLAttributes & 15 | Pick & { 16 | size: number; 17 | onClick?: () => void; 18 | expandInteractiveArea?: ExpandInteractiveArea; 19 | onStatusChanged?: (isActive: boolean) => void; 20 | }; 21 | 22 | class BarComponent extends React.PureComponent { 23 | private readonly defaultInnerRef = React.createRef(); 24 | 25 | private readonly interactiveAreaRef = React.createRef(); 26 | 27 | private readonly id = this.props.context.createID(this.props); 28 | 29 | private isValidClick: boolean = true; 30 | 31 | private get ref() { 32 | return this.props.innerRef || this.defaultInnerRef; 33 | } 34 | 35 | private get childProps(): StyledBarProps { 36 | return omit(this.props, [ 37 | 'context', 38 | 'innerRef', 39 | 'onClick', 40 | 'expandInteractiveArea', 41 | 'onStatusChanged', 42 | ]); 43 | } 44 | 45 | private isActivated: boolean = false; 46 | 47 | private onMouseDown = this.triggerMouseAction(BarActionType.ACTIVATE); 48 | private onMouseMove = this.triggerMouseAction(BarActionType.MOVE); 49 | private onMouseUp = this.triggerMouseAction(BarActionType.DEACTIVATE); 50 | 51 | private onTouchStart = this.triggerTouchAction(BarActionType.ACTIVATE); 52 | private onTouchMove = this.triggerTouchAction(BarActionType.MOVE); 53 | private onTouchEnd = this.triggerTouchAction(BarActionType.DEACTIVATE); 54 | private onTouchCancel = this.triggerTouchAction(BarActionType.DEACTIVATE); 55 | 56 | componentDidMount() { 57 | this.props.context.populateInstance(this.id, this.ref); 58 | document.addEventListener('mousemove', this.onMouseMove); 59 | document.addEventListener('mouseup', this.onMouseUp); 60 | document.addEventListener('touchmove', this.onTouchMove, disablePassive); 61 | document.addEventListener('touchend', this.onTouchEnd); 62 | document.addEventListener('touchcancel', this.onTouchCancel); 63 | 64 | if (this.interactiveAreaRef.current) { 65 | this.interactiveAreaRef.current.addEventListener( 66 | 'mousedown', 67 | this.onMouseDown, 68 | ); 69 | this.interactiveAreaRef.current.addEventListener( 70 | 'touchstart', 71 | this.onTouchStart, 72 | disablePassive, 73 | ); 74 | } 75 | } 76 | 77 | componentWillUnmount() { 78 | document.removeEventListener('mousemove', this.onMouseMove); 79 | document.removeEventListener('mouseup', this.onMouseUp); 80 | document.removeEventListener('touchmove', this.onTouchMove); 81 | document.removeEventListener('touchend', this.onTouchEnd); 82 | document.removeEventListener('touchcancel', this.onTouchCancel); 83 | 84 | if (this.interactiveAreaRef.current) { 85 | this.interactiveAreaRef.current.removeEventListener( 86 | 'mousedown', 87 | this.onMouseDown, 88 | ); 89 | this.interactiveAreaRef.current.removeEventListener( 90 | 'touchstart', 91 | this.onTouchStart, 92 | ); 93 | } 94 | } 95 | 96 | render() { 97 | return ( 98 | 99 | {this.props.children} 100 | 105 | 106 | ); 107 | } 108 | 109 | private onStatusChanged(isActivated: boolean) { 110 | if (this.isActivated !== isActivated) { 111 | this.isActivated = isActivated; 112 | 113 | if (typeof this.props.onStatusChanged === 'function') { 114 | this.props.onStatusChanged(isActivated); 115 | } 116 | } 117 | } 118 | 119 | private updateStatusIfNeed(type: BarActionType) { 120 | if (type === BarActionType.ACTIVATE) { 121 | this.onStatusChanged(true); 122 | } else if (type === BarActionType.DEACTIVATE) { 123 | this.onStatusChanged(false); 124 | } 125 | } 126 | 127 | private triggerAction(type: BarActionType, coordinate: Coordinate) { 128 | if (this.isActivated || type === BarActionType.ACTIVATE) { 129 | this.props.context.triggerBarAction({ type, coordinate, barID: this.id }); 130 | } 131 | 132 | if (this.isActivated && type === BarActionType.DEACTIVATE) { 133 | // touch and click 134 | this.onClick(); 135 | } 136 | 137 | this.updateStatusIfNeed(type); 138 | this.updateClickStatus(type); 139 | } 140 | 141 | private triggerMouseAction(type: BarActionType) { 142 | return (event: React.MouseEvent | MouseEvent) => { 143 | this.disableUserSelectIfResizing(event, type); 144 | const { clientX: x, clientY: y } = event; 145 | this.triggerAction(type, { x, y }); 146 | }; 147 | } 148 | 149 | private triggerTouchAction(type: BarActionType) { 150 | return (event: React.TouchEvent | TouchEvent) => { 151 | this.disableUserSelectIfResizing(event, type); 152 | const touch = event.touches[0] || { clientX: 0, clientY: 0 }; 153 | const { clientX: x, clientY: y } = touch; 154 | this.triggerAction(type, { x, y }); 155 | }; 156 | } 157 | 158 | private disableUserSelectIfResizing( 159 | event: React.MouseEvent | MouseEvent | React.TouchEvent | TouchEvent, 160 | type: BarActionType, 161 | ) { 162 | if (this.isActivated || type === BarActionType.ACTIVATE) { 163 | event.preventDefault(); 164 | } 165 | } 166 | 167 | private updateClickStatus(type: BarActionType) { 168 | if (this.isActivated) { 169 | if (type === BarActionType.ACTIVATE) { 170 | this.isValidClick = true; 171 | } else if (type === BarActionType.MOVE) { 172 | this.isValidClick = false; 173 | } 174 | } 175 | } 176 | 177 | private onClick() { 178 | if (this.isValidClick && typeof this.props.onClick === 'function') { 179 | this.isValidClick = false; // avoid trigger twice on mobile. 180 | this.props.onClick(); 181 | } 182 | } 183 | } 184 | 185 | export type BarProps = Omit; 186 | 187 | export const Bar = withResizerContext(BarComponent); 188 | -------------------------------------------------------------------------------- /src/Container/Container.styled.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type StyledContainerProps = React.HTMLAttributes & { 4 | vertical?: boolean; 5 | }; 6 | 7 | export const StyledContainer = React.forwardRef< 8 | HTMLDivElement, 9 | StyledContainerProps 10 | >(({ vertical, style, ...props }, ref) => ( 11 |
20 | )); 21 | -------------------------------------------------------------------------------- /src/Container/Resizer.ts: -------------------------------------------------------------------------------- 1 | import { SizeRelatedInfo } from '../types'; 2 | import { BarActionScanResult } from './operators'; 3 | import { getNextSizeRelatedInfo } from './utils'; 4 | 5 | type ResizeResult = SizeRelatedInfo | BarActionScanResult; 6 | 7 | function getBarID(indexOfBar: number): number { 8 | return indexOfBar * 2 + 1; 9 | } 10 | 11 | function getSectionID(indexOfSection: number): number { 12 | return indexOfSection * 2; 13 | } 14 | 15 | export class Resizer { 16 | private isDiscarded: boolean = false; 17 | 18 | constructor(private resizeResult: ResizeResult) {} 19 | 20 | resizeSection( 21 | indexOfSection: number, 22 | config: { toSize: number; preferMoveLeftBar?: boolean }, 23 | ) { 24 | if (this.isDiscarded) { 25 | return; 26 | } 27 | 28 | const sectionID = getSectionID(indexOfSection); 29 | const currentSize = this.getSize(sectionID); 30 | 31 | if (currentSize >= 0 && config.toSize >= 0) { 32 | const offset = config.toSize - currentSize; 33 | 34 | if ( 35 | sectionID === this.resizeResult.sizeInfoArray.length - 1 || 36 | config.preferMoveLeftBar 37 | ) { 38 | this.moveBar(indexOfSection - 1, { withOffset: -offset }); 39 | } else { 40 | this.moveBar(indexOfSection, { withOffset: offset }); 41 | } 42 | } 43 | } 44 | 45 | moveBar(indexOfBar: number, config: { withOffset: number }) { 46 | if (this.isDiscarded) { 47 | return; 48 | } 49 | 50 | this.resizeResult = getNextSizeRelatedInfo( 51 | getBarID(indexOfBar), 52 | config.withOffset, 53 | this.resizeResult.sizeInfoArray, 54 | ); 55 | } 56 | 57 | discard() { 58 | this.isDiscarded = true; 59 | } 60 | 61 | isSectionResized(indexOfSection: number): boolean { 62 | const sectionID = getSectionID(indexOfSection); 63 | 64 | if ('defaultSizeInfoArray' in this.resizeResult) { 65 | return ( 66 | this.getSize(sectionID) !== 67 | this.resizeResult.defaultSizeInfoArray[sectionID].currentSize 68 | ); 69 | } else { 70 | return false; 71 | } 72 | } 73 | 74 | isBarActivated(indexOfBar: number): boolean { 75 | if ('barID' in this.resizeResult) { 76 | return this.resizeResult.barID === getBarID(indexOfBar); 77 | } else { 78 | return false; 79 | } 80 | } 81 | 82 | getSectionSize(indexOfSection: number) { 83 | return this.getSize(getSectionID(indexOfSection)); 84 | } 85 | 86 | getResult(): SizeRelatedInfo { 87 | return { ...this.resizeResult, discard: this.isDiscarded }; 88 | } 89 | 90 | getTotalSize(): number { 91 | return this.resizeResult.sizeInfoArray 92 | .filter((sizeInfo, index) => sizeInfo && index % 2 === 0) 93 | .reduce((total, { currentSize }) => total + currentSize, 0); 94 | } 95 | 96 | private getSize(index: number): number | -1 { 97 | const sizeInfo = this.resizeResult.sizeInfoArray[index]; 98 | return sizeInfo ? sizeInfo.currentSize : -1; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Container/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { animationFrameScheduler, merge, Subject } from 'rxjs'; 3 | import { filter, map, observeOn, share, tap } from 'rxjs/operators'; 4 | 5 | import { 6 | BarAction, 7 | BarActionType, 8 | ChildProps, 9 | ResizerContext, 10 | SizeRelatedInfo, 11 | } from '../types'; 12 | import { omit } from '../utils'; 13 | import { ResizerProvider } from '../context'; 14 | 15 | import { Resizer } from './Resizer'; 16 | import { BarActionScanResult, scanBarAction } from './operators'; 17 | import { 18 | calculateCoordinateOffset, 19 | collectSizeRelatedInfo, 20 | isDisabledResponsive, 21 | isSolid, 22 | } from './utils'; 23 | import { StyledContainer, StyledContainerProps } from './Container.styled'; 24 | 25 | interface Props extends React.HTMLAttributes { 26 | vertical?: boolean; 27 | onActivate?: () => void; 28 | beforeApplyResizer?: (resizer: Resizer) => void; 29 | afterResizing?: () => void; 30 | } 31 | 32 | class Container extends React.PureComponent { 33 | private readonly childrenProps: ChildProps[] = []; 34 | 35 | private readonly childrenInstance: HTMLElement[] = []; 36 | 37 | private readonly barActions$ = new Subject(); 38 | 39 | private readonly sizeRelatedInfoAction$ = new Subject(); 40 | 41 | private readonly sizeRelatedInfo$ = merge< 42 | SizeRelatedInfo, 43 | BarActionScanResult 44 | >( 45 | this.sizeRelatedInfoAction$, 46 | this.barActions$.pipe( 47 | scanBarAction({ 48 | calculateOffset: (current, original) => 49 | calculateCoordinateOffset(current, original)[this.axis], 50 | getSizeRelatedInfo: () => this.makeSizeInfos(), 51 | }), 52 | tap((scanResult) => this.monitorBarStatusChanges(scanResult)), 53 | ), 54 | ).pipe( 55 | filter(({ discard }) => !discard), 56 | map((resizeResult) => { 57 | if (typeof this.props.beforeApplyResizer === 'function') { 58 | const resizer = new Resizer(resizeResult); 59 | this.props.beforeApplyResizer(resizer); 60 | return resizer.getResult(); 61 | } else { 62 | return resizeResult; 63 | } 64 | }), 65 | filter(({ discard }) => !discard), 66 | observeOn(animationFrameScheduler), 67 | share(), 68 | ); 69 | 70 | private get axis() { 71 | return this.props.vertical ? 'y' : 'x'; 72 | } 73 | 74 | private get dimension() { 75 | return this.props.vertical ? 'height' : 'width'; 76 | } 77 | 78 | private get contextValue(): ResizerContext { 79 | return { 80 | vertical: !!this.props.vertical, 81 | sizeRelatedInfo$: this.sizeRelatedInfo$, 82 | createID: this.createID, 83 | populateInstance: this.populateChildInstance, 84 | triggerBarAction: this.triggerBarAction, 85 | }; 86 | } 87 | 88 | private get containerProps(): StyledContainerProps { 89 | return omit(this.props, [ 90 | 'onActivate', 91 | 'beforeApplyResizer', 92 | 'afterResizing', 93 | ]); 94 | } 95 | 96 | componentDidMount() { 97 | this.refreshSizeInfos(); 98 | } 99 | 100 | render() { 101 | return ( 102 | 103 | 104 | {this.props.children} 105 | 106 | 107 | ); 108 | } 109 | 110 | getResizer(): Resizer { 111 | return new Resizer(this.makeSizeInfos()); 112 | } 113 | 114 | applyResizer(resizer: Resizer): void { 115 | this.sizeRelatedInfoAction$.next(resizer.getResult()); 116 | } 117 | 118 | private monitorBarStatusChanges({ type }: BarActionScanResult) { 119 | switch (type) { 120 | case BarActionType.ACTIVATE: 121 | if (typeof this.props.onActivate === 'function') { 122 | this.props.onActivate(); 123 | } 124 | return; 125 | case BarActionType.DEACTIVATE: 126 | if (typeof this.props.afterResizing === 'function') { 127 | this.props.afterResizing(); 128 | } 129 | return; 130 | default: 131 | return; 132 | } 133 | } 134 | 135 | private triggerBarAction = (action: BarAction) => { 136 | this.barActions$.next(action); 137 | }; 138 | 139 | private createID = (childProps: ChildProps) => { 140 | this.childrenProps.push(childProps); 141 | return this.childrenProps.length - 1; 142 | }; 143 | 144 | private populateChildInstance = ( 145 | id: number, 146 | ref: React.RefObject, 147 | ) => { 148 | if (ref.current) { 149 | this.childrenInstance[id] = ref.current; 150 | } 151 | }; 152 | 153 | private refreshSizeInfos() { 154 | this.sizeRelatedInfoAction$.next(this.makeSizeInfos()); 155 | } 156 | 157 | private makeSizeInfos(): SizeRelatedInfo { 158 | const { collect, getResult } = collectSizeRelatedInfo(); 159 | 160 | this.childrenProps.forEach((childProps, index) => { 161 | const element = this.childrenInstance[index]; 162 | 163 | collect({ 164 | maxSize: childProps.maxSize, 165 | minSize: childProps.minSize, 166 | disableResponsive: isDisabledResponsive(childProps), 167 | isSolid: isSolid(childProps), 168 | currentSize: element.getBoundingClientRect()[this.dimension], 169 | }); 170 | }); 171 | 172 | return getResult(); 173 | } 174 | } 175 | 176 | export { Container, Resizer, Props as ContainerProps }; 177 | -------------------------------------------------------------------------------- /src/Container/operators.ts: -------------------------------------------------------------------------------- 1 | import { scan } from 'rxjs/operators'; 2 | 3 | import { 4 | BarAction, 5 | BarActionType, 6 | Coordinate, 7 | SizeInfo, 8 | SizeRelatedInfo, 9 | } from '../types'; 10 | import { DEFAULT_COORDINATE_OFFSET, getNextSizeRelatedInfo } from './utils'; 11 | 12 | export interface BarActionScanResult extends SizeRelatedInfo { 13 | barID: number; 14 | offset: number; 15 | type: BarActionType; 16 | originalCoordinate: Coordinate; 17 | defaultSizeInfoArray: SizeInfo[]; 18 | } 19 | 20 | interface ScanBarActionConfig { 21 | getSizeRelatedInfo: () => SizeRelatedInfo; 22 | calculateOffset: (current: Coordinate, original: Coordinate) => number; 23 | } 24 | 25 | const DEFAULT_BAR_ACTION_SCAN_RESULT: BarActionScanResult = { 26 | barID: -1, 27 | offset: 0, 28 | type: BarActionType.DEACTIVATE, 29 | originalCoordinate: DEFAULT_COORDINATE_OFFSET, 30 | defaultSizeInfoArray: [], 31 | sizeInfoArray: [], 32 | discard: true, 33 | flexGrowRatio: 0, 34 | }; 35 | 36 | export function scanBarAction(config: ScanBarActionConfig) { 37 | return scan((prevResult, action) => { 38 | const result = { 39 | barID: action.barID, 40 | type: action.type, 41 | }; 42 | 43 | switch (action.type) { 44 | case BarActionType.ACTIVATE: 45 | const { sizeInfoArray, flexGrowRatio } = config.getSizeRelatedInfo(); 46 | 47 | return { 48 | ...DEFAULT_BAR_ACTION_SCAN_RESULT, 49 | ...result, 50 | originalCoordinate: action.coordinate, 51 | defaultSizeInfoArray: sizeInfoArray, 52 | sizeInfoArray, 53 | flexGrowRatio, 54 | }; 55 | case BarActionType.MOVE: 56 | const offset = config.calculateOffset( 57 | action.coordinate, 58 | prevResult.originalCoordinate, 59 | ); 60 | 61 | return { 62 | ...result, 63 | ...getNextSizeRelatedInfo( 64 | action.barID, 65 | offset, 66 | prevResult.defaultSizeInfoArray, 67 | ), 68 | offset, 69 | originalCoordinate: prevResult.originalCoordinate, 70 | defaultSizeInfoArray: prevResult.defaultSizeInfoArray, 71 | discard: false, 72 | }; 73 | case BarActionType.DEACTIVATE: 74 | return DEFAULT_BAR_ACTION_SCAN_RESULT; 75 | } 76 | }, DEFAULT_BAR_ACTION_SCAN_RESULT); 77 | } 78 | -------------------------------------------------------------------------------- /src/Container/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChildProps, 3 | Coordinate, 4 | SizeInfo, 5 | SizeRelatedInfo, 6 | Trend, 7 | } from '../types'; 8 | import { isValidNumber } from '../utils'; 9 | 10 | export const DEFAULT_COORDINATE_OFFSET: Coordinate = { x: 0, y: 0 }; 11 | 12 | function filterSize( 13 | nextSize: number, 14 | { maxSize, minSize = 0 }: SizeInfo, 15 | ): { nextSize: number; remainingOffset: number } { 16 | if (nextSize < minSize) { 17 | return { 18 | nextSize: minSize, 19 | remainingOffset: nextSize - minSize, 20 | }; 21 | } 22 | 23 | if (isValidNumber(maxSize) && nextSize > maxSize) { 24 | return { 25 | nextSize: maxSize, 26 | remainingOffset: nextSize - maxSize, 27 | }; 28 | } 29 | 30 | return { 31 | nextSize, 32 | remainingOffset: 0, 33 | }; 34 | } 35 | 36 | export function isSolid({ size }: ChildProps): boolean { 37 | return isValidNumber(size); 38 | } 39 | 40 | export function isDisabledResponsive(childProps: ChildProps): boolean { 41 | const { disableResponsive } = childProps; 42 | 43 | if (isSolid(childProps) && disableResponsive === undefined) { 44 | return true; 45 | } else { 46 | return !!disableResponsive; 47 | } 48 | } 49 | 50 | export function calculateCoordinateOffset( 51 | current: Coordinate, 52 | previous: Coordinate | null, 53 | ): Coordinate { 54 | if (previous) { 55 | return { 56 | x: current.x - previous.x, 57 | y: current.y - previous.y, 58 | }; 59 | } else { 60 | return DEFAULT_COORDINATE_OFFSET; 61 | } 62 | } 63 | 64 | export function collectSizeRelatedInfo() { 65 | const sizeInfoArray: SizeInfo[] = []; 66 | let responsiveChildCount = 0; 67 | let responsiveContainerSize = 0; 68 | 69 | return { 70 | collect(sizeInfo: SizeInfo) { 71 | sizeInfoArray.push(sizeInfo); 72 | 73 | if (!sizeInfo.disableResponsive) { 74 | responsiveChildCount += 1; 75 | responsiveContainerSize += sizeInfo.currentSize; 76 | } 77 | }, 78 | 79 | getResult(): SizeRelatedInfo { 80 | return { 81 | sizeInfoArray, 82 | flexGrowRatio: responsiveChildCount / responsiveContainerSize, 83 | }; 84 | }, 85 | }; 86 | } 87 | 88 | function doResize( 89 | offset: number, 90 | sizeInfo: SizeInfo, 91 | ): { remainingOffset: number; sizeInfo: SizeInfo } { 92 | if (sizeInfo.isSolid) { 93 | return { 94 | remainingOffset: offset, 95 | sizeInfo, 96 | }; 97 | } 98 | 99 | const { nextSize, remainingOffset } = filterSize( 100 | sizeInfo.currentSize + offset, 101 | sizeInfo, 102 | ); 103 | 104 | return { 105 | sizeInfo: { ...sizeInfo, currentSize: nextSize }, 106 | remainingOffset, 107 | }; 108 | } 109 | 110 | function resize( 111 | barID: number, 112 | offset: number, 113 | trend: Trend, 114 | sizeInfoArray: SizeInfo[], 115 | ): { sizeInfoArray: SizeInfo[]; remainingOffset: number } { 116 | const newSizeInfoArray: SizeInfo[] = []; 117 | let prevRemainingOffset = offset; 118 | 119 | for ( 120 | let sectionID = barID + trend; 121 | isValidSectionID(sectionID); 122 | sectionID += trend 123 | ) { 124 | if (prevRemainingOffset) { 125 | const { sizeInfo, remainingOffset } = doResize( 126 | prevRemainingOffset, 127 | sizeInfoArray[sectionID], 128 | ); 129 | 130 | prevRemainingOffset = remainingOffset; 131 | collect(sizeInfo); 132 | } else { 133 | collect(sizeInfoArray[sectionID]); 134 | } 135 | } 136 | 137 | function collect(sizeInfo: SizeInfo) { 138 | if (trend === -1) { 139 | newSizeInfoArray.unshift(sizeInfo); 140 | } else { 141 | newSizeInfoArray.push(sizeInfo); 142 | } 143 | } 144 | 145 | function isValidSectionID(sectionID: number): boolean { 146 | if (trend === -1) { 147 | return sectionID >= 0; 148 | } else { 149 | return sectionID <= sizeInfoArray.length - 1; 150 | } 151 | } 152 | 153 | return { 154 | sizeInfoArray: newSizeInfoArray, 155 | remainingOffset: prevRemainingOffset, 156 | }; 157 | } 158 | 159 | export function getNextSizeRelatedInfo( 160 | barID: number, 161 | offset: number, 162 | sizeInfoArray: SizeInfo[], 163 | ): SizeRelatedInfo { 164 | const { collect, getResult } = collectSizeRelatedInfo(); 165 | 166 | const leftResult = resize(barID, offset, -1, sizeInfoArray); 167 | const rightResult = resize(barID, -offset, 1, sizeInfoArray); 168 | 169 | const leftUsedOffset = offset - leftResult.remainingOffset; 170 | const rightUsedOffset = -offset - rightResult.remainingOffset; 171 | 172 | function collectAll(left: SizeInfo[], right: SizeInfo[]) { 173 | left.forEach(collect); 174 | collect(sizeInfoArray[barID]); 175 | right.forEach(collect); 176 | } 177 | 178 | if (leftUsedOffset === -rightUsedOffset) { 179 | collectAll(leftResult.sizeInfoArray, rightResult.sizeInfoArray); 180 | } else if (Math.abs(leftUsedOffset) < Math.abs(rightUsedOffset)) { 181 | // left side sections was limited 182 | const newRightResult = resize(barID, -leftUsedOffset, 1, sizeInfoArray); 183 | collectAll(leftResult.sizeInfoArray, newRightResult.sizeInfoArray); 184 | } else { 185 | // right side sections was limited 186 | const newLeftResult = resize(barID, -rightUsedOffset, -1, sizeInfoArray); 187 | collectAll(newLeftResult.sizeInfoArray, rightResult.sizeInfoArray); 188 | } 189 | 190 | return getResult(); 191 | } 192 | -------------------------------------------------------------------------------- /src/Section/Section.styled.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ChildProps } from '../types'; 4 | 5 | export type StyledSectionProps = React.HTMLAttributes & 6 | Pick & { 7 | flexGrow: number; 8 | flexShrink: number; 9 | flexBasis: number; 10 | }; 11 | 12 | export const StyledSection = React.forwardRef< 13 | HTMLDivElement, 14 | StyledSectionProps 15 | >( 16 | ( 17 | { 18 | context, 19 | maxSize, 20 | minSize, 21 | flexGrow, 22 | flexShrink, 23 | flexBasis, 24 | style, 25 | ...props 26 | }, 27 | ref, 28 | ) => ( 29 |
42 | ), 43 | ); 44 | -------------------------------------------------------------------------------- /src/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Subscription } from 'rxjs'; 3 | import { filter, map, tap } from 'rxjs/operators'; 4 | 5 | import { ChildProps, SizeInfo } from '../types'; 6 | import { withResizerContext } from '../context'; 7 | import { isValidNumber, omit } from '../utils'; 8 | import { StyledSection, StyledSectionProps } from './Section.styled'; 9 | 10 | type Props = ChildProps & 11 | React.HTMLAttributes & { 12 | onSizeChanged?: (currentSize: number) => void; 13 | }; 14 | 15 | class SectionComponent extends React.PureComponent { 16 | private readonly defaultInnerRef = React.createRef(); 17 | 18 | private readonly id = this.props.context.createID(this.props); 19 | 20 | private readonly subscription = new Subscription(); 21 | 22 | private sizeInfo?: SizeInfo; 23 | 24 | private flexGrowRatio: number = 0; 25 | 26 | private resize$ = this.props.context.sizeRelatedInfo$.pipe( 27 | map(({ sizeInfoArray, flexGrowRatio }) => ({ 28 | sizeInfo: sizeInfoArray[this.id], 29 | flexGrowRatio, 30 | })), 31 | filter(({ sizeInfo }) => !!sizeInfo), 32 | tap(({ sizeInfo, flexGrowRatio }) => { 33 | this.sizeInfo = sizeInfo; 34 | this.flexGrowRatio = flexGrowRatio; 35 | if (this.ref.current) { 36 | const { flexGrow, flexShrink, flexBasis } = this.getStyle( 37 | sizeInfo, 38 | flexGrowRatio, 39 | ); 40 | 41 | this.ref.current.style.flexGrow = `${flexGrow}`; 42 | this.ref.current.style.flexShrink = `${flexShrink}`; 43 | this.ref.current.style.flexBasis = `${flexBasis}px`; 44 | 45 | this.onSizeChanged(sizeInfo.currentSize); 46 | } 47 | }), 48 | ); 49 | 50 | private get ref() { 51 | return this.props.innerRef || this.defaultInnerRef; 52 | } 53 | 54 | private get childProps(): StyledSectionProps { 55 | return { 56 | ...omit(this.props, [ 57 | 'defaultSize', 58 | 'size', 59 | 'disableResponsive', 60 | 'innerRef', 61 | 'onSizeChanged', 62 | ]), 63 | ...this.getStyle(), 64 | }; 65 | } 66 | 67 | componentDidMount() { 68 | this.subscription.add(this.resize$.subscribe()); 69 | this.props.context.populateInstance(this.id, this.ref); 70 | } 71 | 72 | componentWillUnmount(): void { 73 | this.subscription.unsubscribe(); 74 | } 75 | 76 | render() { 77 | return ; 78 | } 79 | 80 | private onSizeChanged(currentSize: number) { 81 | if (typeof this.props.onSizeChanged === 'function') { 82 | this.props.onSizeChanged(currentSize); 83 | } 84 | } 85 | 86 | private getFlexShrink() { 87 | if (isValidNumber(this.props.size)) { 88 | return 0; 89 | } else { 90 | return this.props.disableResponsive ? 1 : 0; 91 | } 92 | } 93 | 94 | private getStyle( 95 | sizeInfo: SizeInfo | undefined = this.sizeInfo, 96 | flexGrowRatio: number = this.flexGrowRatio, 97 | ) { 98 | const flexShrink = this.getFlexShrink(); 99 | 100 | if (sizeInfo) { 101 | const { disableResponsive, currentSize } = sizeInfo; 102 | 103 | return { 104 | flexShrink, 105 | flexGrow: disableResponsive ? 0 : flexGrowRatio * currentSize, 106 | flexBasis: disableResponsive ? currentSize : 0, 107 | }; 108 | } else { 109 | const size = this.props.size || this.props.defaultSize; 110 | 111 | if (isValidNumber(size)) { 112 | return { 113 | flexShrink, 114 | flexGrow: 0, 115 | flexBasis: size, 116 | }; 117 | } else { 118 | return { 119 | flexShrink, 120 | flexGrow: 1, 121 | flexBasis: 0, 122 | }; 123 | } 124 | } 125 | } 126 | } 127 | 128 | export type SectionProps = Pick; 129 | 130 | export const Section = withResizerContext(SectionComponent); 131 | -------------------------------------------------------------------------------- /src/context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { EMPTY } from 'rxjs'; 3 | 4 | import { ChildProps, ResizerContext } from './types'; 5 | import { noop } from './utils'; 6 | 7 | export const { 8 | Provider: ResizerProvider, 9 | Consumer: ResizerConsumer, 10 | } = React.createContext({ 11 | createID: () => -1, 12 | populateInstance: noop, 13 | triggerBarAction: noop, 14 | vertical: false, 15 | sizeRelatedInfo$: EMPTY, 16 | }); 17 | 18 | export function withResizerContext( 19 | Target: React.ComponentType, 20 | ) { 21 | return (props: Omit) => ( 22 | 23 | {(context) => { 24 | const finalProps = { ...props, context } as T; 25 | return ; 26 | }} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Container'; 2 | export * from './Section'; 3 | export * from './Bar'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export interface Coordinate { 5 | x: number; 6 | y: number; 7 | } 8 | 9 | export type Trend = -1 | 0 | 1; 10 | 11 | export enum BarActionType { 12 | ACTIVATE = 'activate', 13 | MOVE = 'move', 14 | DEACTIVATE = 'deactivate', 15 | } 16 | 17 | export interface BarAction { 18 | type: BarActionType; 19 | coordinate: Coordinate; 20 | barID: number; 21 | } 22 | 23 | export interface SizeInfo { 24 | isSolid: boolean; 25 | currentSize: number; 26 | maxSize?: number; 27 | minSize?: number; 28 | disableResponsive?: boolean; 29 | } 30 | 31 | export interface SizeRelatedInfo { 32 | discard?: boolean; 33 | sizeInfoArray: SizeInfo[]; 34 | flexGrowRatio: number; 35 | } 36 | 37 | export interface ChildProps { 38 | size?: number; 39 | defaultSize?: number; 40 | maxSize?: number; 41 | minSize?: number; 42 | context: ResizerContext; 43 | disableResponsive?: boolean; 44 | innerRef?: RefObject; 45 | } 46 | 47 | export interface ResizerContext { 48 | vertical: boolean; 49 | createID: (props: ChildProps) => number; 50 | populateInstance: (id: number, ref: RefObject) => void; 51 | triggerBarAction: (action: BarAction) => void; 52 | sizeRelatedInfo$: Observable; 53 | } 54 | 55 | export interface ExpandInteractiveArea { 56 | top?: number; 57 | left?: number; 58 | right?: number; 59 | bottom?: number; 60 | } 61 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isValidNumber(num?: number): num is number { 2 | return typeof num === 'number' && num === num; 3 | } 4 | 5 | export function noop() {} 6 | 7 | export function omit

( 8 | props: P, 9 | ignoreKeys: K[], 10 | ): Omit { 11 | type IgnoreKeyMap = Partial>; 12 | 13 | const ignoreKeyMap = ignoreKeys.reduce( 14 | (map, key) => { 15 | map[key] = true; 16 | return map; 17 | }, 18 | {} as IgnoreKeyMap, 19 | ); 20 | 21 | return (Object.keys(props) as (keyof P)[]).reduce( 22 | (newProps, key) => { 23 | if (ignoreKeyMap[key]) { 24 | return newProps; 25 | } else { 26 | newProps[key] = props[key]; 27 | return newProps; 28 | } 29 | }, 30 | {} as P, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "declaration": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "allowSyntheticDefaultImports": true, 8 | "experimentalDecorators": true, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "noImplicitAny": true, 12 | "strict": true, 13 | "noImplicitReturns": true, 14 | "moduleResolution": "node", 15 | "lib": ["dom", "es2015"], 16 | "jsx": "react", 17 | "target": "es5" 18 | }, 19 | "include": ["src", "demo"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react", 4 | "tslint-eslint-rules", 5 | "tslint-sonarts" 6 | ], 7 | "defaultSeverity": "error", 8 | "rules": { 9 | "array-bracket-spacing": [true, "never", { 10 | "arraysInArrays": false, 11 | "singleValue": false, 12 | "objectsInArrays": false 13 | }], 14 | "jsx-boolean-value": false, 15 | "block-spacing": [true, "always"], 16 | "import-spacing": true, 17 | "no-boolean-literal-compare": true, 18 | "no-console": [true, "log", "time", "trace"], 19 | "no-duplicate-variable": true, 20 | "no-multi-spaces": true, 21 | "no-return-await": true, 22 | "no-string-literal": true, 23 | "no-string-throw": true, 24 | "no-trailing-whitespace": true, 25 | "no-unnecessary-initializer": true, 26 | "no-var-keyword": true, 27 | "object-curly-spacing": [true, "always"], 28 | "one-variable-per-declaration": [true, "ignore-for-loop"], 29 | "prefer-const": true, 30 | "quotemark": [true, "single", "jsx-double"], 31 | "ter-arrow-spacing": [true, { "before": true, "after": true }], 32 | "triple-equals": [true, "allow-null-check", "allow-undefined-check"], 33 | "whitespace": [ 34 | true, 35 | "check-branch", 36 | "check-decl", 37 | "check-operator", 38 | "check-module", 39 | "check-separator", 40 | "check-rest-spread", 41 | "check-type", 42 | "check-typecast", 43 | "check-type-operator", 44 | "check-preblock" 45 | ], 46 | "interface-over-type-literal": true, 47 | "no-consecutive-blank-lines": true, 48 | "space-before-function-paren": [true, { 49 | "anonymous": "never", 50 | "named": "never", 51 | "asyncArrow": "always" 52 | }], 53 | "space-within-parens": [true, 0], 54 | "jsx-curly-spacing": [true, "never"], 55 | "jsx-no-multiline-js": false, 56 | "jsx-equals-spacing": false, 57 | "jsx-no-bind": true, 58 | "jsx-key": true, 59 | "jsx-no-lambda": true, 60 | "jsx-no-string-ref": true, 61 | "jsx-wrap-multiline": true, 62 | "jsx-self-close": true, 63 | "cognitive-complexity": false, 64 | "no-duplicate-string": false, 65 | "no-big-function": false, 66 | "no-small-switch": false, 67 | "max-union-size": [true, 20], 68 | "parameters-max-number": false 69 | } 70 | } 71 | --------------------------------------------------------------------------------