resizeGroup.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import React, { createRef, ReactNode, RefObject } from 'react';
  2. import classNames from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { ResizeGroupFoundation, ResizeGroupAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
  5. import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
  6. import BaseComponent from '../../_base/baseComponent';
  7. import { ResizeContext, ResizeContextProps } from './resizeContext';
  8. import { ResizeCallback, ResizeEventType, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/types';
  9. import "@douyinfe/semi-foundation/resizable/resizable.scss";
  10. const prefixCls = cssClasses.PREFIX;
  11. export interface ResizeGroupProps {
  12. children: ReactNode;
  13. direction: 'horizontal' | 'vertical';
  14. className?: string
  15. }
  16. export interface ResizeGroupState {
  17. isResizing: boolean;
  18. originalPosition: {
  19. x: number;
  20. y: number;
  21. lastItemSize: number;
  22. nextItemSize: number;
  23. lastOffset: number;
  24. nextOffset: number
  25. };
  26. backgroundStyle: React.CSSProperties;
  27. curHandler: number;
  28. contextValue: ResizeContextProps
  29. }
  30. class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
  31. static propTypes = {
  32. };
  33. static defaultProps: Partial<ResizeGroupProps> = {
  34. direction: 'horizontal'
  35. };
  36. constructor(props: ResizeGroupProps) {
  37. super(props);
  38. this.groupRef = createRef();
  39. this.foundation = new ResizeGroupFoundation(this.adapter);
  40. this.state = {
  41. isResizing: false,
  42. originalPosition: {
  43. x: 0,
  44. y: 0,
  45. lastItemSize: 0,
  46. nextItemSize: 0,
  47. lastOffset: 0,
  48. nextOffset: 0,
  49. },
  50. backgroundStyle: {
  51. cursor: 'auto',
  52. },
  53. curHandler: null,
  54. contextValue: {
  55. direction: props.direction,
  56. registerItem: this.registerItem,
  57. registerHandler: this.registerHandler,
  58. notifyResizeStart: this.foundation.onResizeStart,
  59. getGroupSize: this.getGroupSize,
  60. },
  61. };
  62. }
  63. foundation: ResizeGroupFoundation;
  64. groupRef: React.RefObject<HTMLDivElement>;
  65. groupSize: number;
  66. availableSize: number;
  67. static contextType = ResizeContext;
  68. context: ResizeGroupProps;
  69. // 在context中使用的属性需要考虑在strictMode下会执行两次,所以用Map来维护
  70. itemRefs: Map<number, RefObject<HTMLDivElement>> = new Map();
  71. itemMinMap: Map<number, string> = new Map();
  72. itemMaxMap: Map<number, string> = new Map();
  73. itemMinusMap: Map<number, number> = new Map();
  74. itemDefaultSizeList: Map<number, (string|number)> = new Map();
  75. itemResizeStart: Map<number, ResizeStartCallback> = new Map();
  76. itemResizing: Map<number, ResizeCallback> = new Map();
  77. itemResizeEnd: Map<number, ResizeCallback> = new Map();
  78. handlerRefs: Map<number, RefObject<HTMLDivElement>> = new Map();
  79. componentDidMount() {
  80. this.foundation.init();
  81. // 监听窗口大小变化,保证一些限制仍生效
  82. window.addEventListener('resize', this.foundation.ensureConstraint);
  83. }
  84. componentDidUpdate(prevProps: ResizeGroupProps) {
  85. // 支持动态调整伸缩direction
  86. if (this.props.direction !== prevProps.direction) {
  87. this.setState((prevState) => ({
  88. ...prevState, // 保留其他状态
  89. contextValue: {
  90. ...prevState.contextValue, // 保留其他上下文值
  91. direction: this.props.direction,
  92. }
  93. }));
  94. this.foundation.direction = this.props.direction;
  95. }
  96. }
  97. componentWillUnmount() {
  98. this.foundation.destroy();
  99. window.removeEventListener('resize', this.foundation.ensureConstraint);
  100. }
  101. get adapter(): ResizeGroupAdapter<ResizeGroupProps, ResizeGroupState> {
  102. return {
  103. ...super.adapter,
  104. getGroupRef: () => this.groupRef.current,
  105. getItem: (id: number) => this.itemRefs.get(id).current,
  106. getItemCount: () => this.itemRefs.size,
  107. getHandler: (id: number) => this.handlerRefs.get(id).current,
  108. getHandlerCount: () => this.handlerRefs.size,
  109. getItemMin: (index) => {
  110. return this.itemMinMap.get(index);
  111. },
  112. getItemMax: (index) => {
  113. return this.itemMaxMap.get(index);
  114. },
  115. getItemChange: (index) => {
  116. return this.itemResizing.get(index);
  117. },
  118. getItemEnd: (index) => {
  119. return this.itemResizeEnd.get(index);
  120. },
  121. getItemStart: (index) => {
  122. return this.itemResizeStart.get(index);
  123. },
  124. getItemDefaultSize: (index) => {
  125. return this.itemDefaultSizeList.get(index);
  126. },
  127. registerEvents: this.registerEvent,
  128. unregisterEvents: this.unregisterEvent,
  129. };
  130. }
  131. get window(): Window | null {
  132. return this.groupRef.current.ownerDocument.defaultView as Window ?? null;
  133. }
  134. registerEvent = (type: ResizeEventType = 'mouse') => {
  135. if (this.window) {
  136. if (type === 'mouse') {
  137. this.window.addEventListener('mousemove', this.foundation.onMouseMove);
  138. this.window.addEventListener('mouseup', this.foundation.onResizeEnd);
  139. this.window.addEventListener('mouseleave', this.foundation.onResizeEnd);
  140. } else {
  141. this.window.addEventListener('touchmove', this.foundation.onTouchMove, { passive: false });
  142. this.window.addEventListener('touchend', this.foundation.onResizeEnd);
  143. this.window.addEventListener('touchcancel', this.foundation.onResizeEnd);
  144. }
  145. }
  146. }
  147. unregisterEvent = (type: ResizeEventType = 'mouse') => {
  148. if (this.window) {
  149. if (type === 'mouse') {
  150. this.window.removeEventListener('mousemove', this.foundation.onMouseMove);
  151. this.window.removeEventListener('mouseup', this.foundation.onResizeEnd);
  152. this.window.removeEventListener('mouseleave', this.foundation.onResizeEnd);
  153. } else {
  154. this.window.removeEventListener('touchmove', this.foundation.onTouchMove, { passive: false } as any);
  155. this.window.removeEventListener('touchend', this.foundation.onResizeEnd);
  156. this.window.removeEventListener('touchcancel', this.foundation.onResizeEnd);
  157. }
  158. }
  159. }
  160. registerItem = (ref: RefObject<HTMLDivElement>,
  161. min: string, max: string, defaultSize: string|number,
  162. onResizeStart: ResizeStartCallback, onChange: ResizeCallback, onResizeEnd: ResizeCallback
  163. ) => {
  164. if (Array.from(this.itemRefs.values()).some(r => r === ref)) {
  165. return -1;
  166. }
  167. let index = this.itemRefs.size;
  168. this.itemRefs.set(index, ref);
  169. this.itemMinMap.set(index, min);
  170. this.itemMaxMap.set(index, max);
  171. this.itemDefaultSizeList.set(index, defaultSize);
  172. this.itemResizeStart.set(index, onResizeStart);
  173. this.itemResizing.set(index, onChange);
  174. this.itemResizeEnd.set(index, onResizeEnd);
  175. return index;
  176. }
  177. registerHandler = (ref: RefObject<HTMLDivElement>) => {
  178. if (Array.from(this.handlerRefs.values()).some(r => r === ref)) {
  179. return -1;
  180. }
  181. let index = this.handlerRefs.size;
  182. this.handlerRefs.set(index, ref);
  183. return index;
  184. }
  185. getGroupSize = () => {
  186. return this.groupSize;
  187. }
  188. render() {
  189. const { children, direction, className, ...rest } = this.props;
  190. return (
  191. <ResizeContext.Provider value={this.state.contextValue}>
  192. <div
  193. style={{
  194. flexDirection: direction === 'vertical' ? 'column' : 'row',
  195. }}
  196. ref={this.groupRef}
  197. className={classNames(className, prefixCls + '-group')}
  198. {...rest}
  199. >
  200. {this.state.isResizing && <div style={this.state.backgroundStyle} className={classNames(className, prefixCls + '-background')}/>}
  201. {children}
  202. </div>
  203. </ResizeContext.Provider>
  204. );
  205. }
  206. }
  207. export default ResizeGroup;